PageRenderTime 100ms CodeModel.GetById 49ms RepoModel.GetById 4ms app.codeStats 1ms

/core/Tracker/Action.php

https://github.com/quarkness/piwik
PHP | 526 lines | 376 code | 56 blank | 94 comment | 30 complexity | 7bc774c5d20e4218c2285a2be59372b3 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * Interface of the Action object.
  14. * New Action classes can be defined in plugins and used instead of the default one.
  15. *
  16. * @package Piwik
  17. * @subpackage Piwik_Tracker
  18. */
  19. interface Piwik_Tracker_Action_Interface {
  20. const TYPE_ACTION_URL = 1;
  21. const TYPE_OUTLINK = 2;
  22. const TYPE_DOWNLOAD = 3;
  23. const TYPE_ACTION_NAME = 4;
  24. const TYPE_ECOMMERCE_ITEM_SKU = 5;
  25. const TYPE_ECOMMERCE_ITEM_NAME = 6;
  26. const TYPE_ECOMMERCE_ITEM_CATEGORY = 7;
  27. public function setRequest($requestArray);
  28. public function setIdSite( $idSite );
  29. public function init();
  30. public function getActionUrl();
  31. public function getActionName();
  32. public function getActionType();
  33. public function record( $idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction );
  34. public function getIdActionUrl();
  35. public function getIdActionName();
  36. public function getIdLinkVisitAction();
  37. }
  38. /**
  39. * Handles an action (page view, download or outlink) by the visitor.
  40. * Parses the action name and URL from the request array, then records the action in the log table.
  41. *
  42. * @package Piwik
  43. * @subpackage Piwik_Tracker
  44. */
  45. class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface
  46. {
  47. private $request;
  48. private $idSite;
  49. private $timestamp;
  50. private $idLinkVisitAction;
  51. private $idActionName = false;
  52. private $idActionUrl = false;
  53. private $actionName;
  54. private $actionType;
  55. private $actionUrl;
  56. static private $queryParametersToExclude = array('phpsessid', 'jsessionid', 'sessionid', 'aspsessionid');
  57. public function setRequest($requestArray)
  58. {
  59. $this->request = $requestArray;
  60. }
  61. public function getRequest()
  62. {
  63. return $this->request;
  64. }
  65. /**
  66. * Returns URL of the page currently being tracked, or the file being downloaded, or the outlink being clicked
  67. * @return string
  68. */
  69. public function getActionUrl()
  70. {
  71. return $this->actionUrl;
  72. }
  73. public function getActionName()
  74. {
  75. return $this->actionName;
  76. }
  77. public function getActionType()
  78. {
  79. return $this->actionType;
  80. }
  81. public function getActionNameType()
  82. {
  83. $actionNameType = null;
  84. // we can add here action types for names of other actions than page views (like downloads, outlinks)
  85. switch( $this->getActionType() )
  86. {
  87. case Piwik_Tracker_Action_Interface::TYPE_ACTION_URL:
  88. $actionNameType = Piwik_Tracker_Action_Interface::TYPE_ACTION_NAME;
  89. break;
  90. }
  91. return $actionNameType;
  92. }
  93. public function getIdActionUrl()
  94. {
  95. return $this->idActionUrl;
  96. }
  97. public function getIdActionName()
  98. {
  99. return $this->idActionName;
  100. }
  101. protected function setActionName($name)
  102. {
  103. $name = self::cleanupString($name);
  104. $this->actionName = $name;
  105. }
  106. protected function setActionType($type)
  107. {
  108. $this->actionType = $type;
  109. }
  110. protected function setActionUrl($url)
  111. {
  112. $url = self::excludeQueryParametersFromUrl($url, $this->idSite);
  113. $this->actionUrl = $url;
  114. }
  115. static public function excludeQueryParametersFromUrl($originalUrl, $idSite)
  116. {
  117. $website = Piwik_Common::getCacheWebsiteAttributes( $idSite );
  118. $originalUrl = Piwik_Common::unsanitizeInputValue($originalUrl);
  119. $originalUrl = self::cleanupString($originalUrl);
  120. $parsedUrl = @parse_url($originalUrl);
  121. if(empty($parsedUrl['query']))
  122. {
  123. return $originalUrl;
  124. }
  125. $campaignTrackingParameters = Piwik_Common::getCampaignParameters();
  126. $campaignTrackingParameters = array_merge(
  127. $campaignTrackingParameters[0], // campaign name parameters
  128. $campaignTrackingParameters[1] // campaign keyword parameters
  129. );
  130. $excludedParameters = isset($website['excluded_parameters'])
  131. ? $website['excluded_parameters']
  132. : array();
  133. $parametersToExclude = array_merge( $excludedParameters,
  134. self::$queryParametersToExclude,
  135. $campaignTrackingParameters);
  136. $parametersToExclude = array_map('strtolower', $parametersToExclude);
  137. $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['query']);
  138. $validQuery = '';
  139. $separator = '&';
  140. foreach($queryParameters as $name => $value)
  141. {
  142. if(!in_array(strtolower($name), $parametersToExclude))
  143. {
  144. if (is_array($value))
  145. {
  146. foreach ($value as $param)
  147. {
  148. if($param === false)
  149. {
  150. $validQuery .= $name.'[]'.$separator;
  151. }
  152. else
  153. {
  154. $validQuery .= $name.'[]='.$param.$separator;
  155. }
  156. }
  157. }
  158. else if($value === false)
  159. {
  160. $validQuery .= $name.$separator;
  161. }
  162. else
  163. {
  164. $validQuery .= $name.'='.$value.$separator;
  165. }
  166. }
  167. }
  168. $parsedUrl['query'] = substr($validQuery,0,-strlen($separator));
  169. $url = Piwik_Common::getParseUrlReverse($parsedUrl);
  170. printDebug('Excluding parameters "'.implode(',',$excludedParameters).'" from URL');
  171. if($originalUrl != $url)
  172. {
  173. printDebug(' Before was "'.$originalUrl.'"');
  174. printDebug(' After is "'.$url.'"');
  175. }
  176. return $url;
  177. }
  178. public function init()
  179. {
  180. $info = $this->extractUrlAndActionNameFromRequest();
  181. $this->setActionName($info['name']);
  182. $this->setActionType($info['type']);
  183. $this->setActionUrl($info['url']);
  184. }
  185. static public function getSqlSelectActionId()
  186. {
  187. $sql = "SELECT idaction, type, name
  188. FROM ".Piwik_Common::prefixTable('log_action')
  189. ." WHERE "
  190. ." ( hash = CRC32(?) AND name = ? AND type = ? ) ";
  191. return $sql;
  192. }
  193. /**
  194. * This function will find the idaction from the lookup table piwik_log_action,
  195. * given an Action name and type.
  196. *
  197. * This is used to record Page URLs, Page Titles, Ecommerce items SKUs, item names, item categories
  198. *
  199. * If the action name does not exist in the lookup table, it will INSERT it
  200. * @param array $actionNamesAndTypes Array of one or many (name,type)
  201. * @return array Returns the input array, with the idaction appended ie. Array of one or many (name,type,idaction)
  202. */
  203. static public function loadActionId( $actionNamesAndTypes )
  204. {
  205. // First, we try and select the actions that are already recorded
  206. $sql = self::getSqlSelectActionId();
  207. $bind = array();
  208. $i = 0;
  209. foreach($actionNamesAndTypes as &$actionNameType)
  210. {
  211. list($name,$type) = $actionNameType;
  212. if(empty($name))
  213. {
  214. $actionNameType[] = false;
  215. continue;
  216. }
  217. if($i > 0)
  218. {
  219. $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) ";
  220. }
  221. $bind[] = $name;
  222. $bind[] = $name;
  223. $bind[] = $type;
  224. $i++;
  225. }
  226. // Case URL & Title are empty
  227. if(empty($bind))
  228. {
  229. return $actionNamesAndTypes;
  230. }
  231. $actionIds = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind);
  232. // For the Actions found in the lookup table, add the idaction in the array,
  233. // If not found in lookup table, queue for INSERT
  234. $actionsToInsert = array();
  235. foreach($actionNamesAndTypes as $index => &$actionNameType)
  236. {
  237. list($name,$type) = $actionNameType;
  238. if(empty($name)) { continue; }
  239. $found = false;
  240. foreach($actionIds as $row)
  241. {
  242. if($name == $row['name']
  243. && $type == $row['type'])
  244. {
  245. $found = true;
  246. $actionNameType[] = $row['idaction'];
  247. continue;
  248. }
  249. }
  250. if(!$found)
  251. {
  252. $actionsToInsert[] = $index;
  253. }
  254. }
  255. $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_action').
  256. "( name, hash, type ) VALUES (?,CRC32(?),?)";
  257. // Then, we insert all new actions in the lookup table
  258. foreach($actionsToInsert as $actionToInsert)
  259. {
  260. list($name,$type) = $actionNamesAndTypes[$actionToInsert];
  261. Piwik_Tracker::getDatabase()->query($sql, array($name, $name, $type));
  262. $actionId = Piwik_Tracker::getDatabase()->lastInsertId();
  263. printDebug("Recorded a new action (".self::getActionTypeName($type).") in the lookup table: ". $name . " (idaction = ".$actionId.")");
  264. $actionNamesAndTypes[$actionToInsert][] = $actionId;
  265. }
  266. return $actionNamesAndTypes;
  267. }
  268. static public function getActionTypeName($type)
  269. {
  270. switch($type)
  271. {
  272. case self::TYPE_ACTION_URL: return 'Page URL'; break;
  273. case self::TYPE_OUTLINK: return 'Outlink URL'; break;
  274. case self::TYPE_DOWNLOAD: return 'Download URL'; break;
  275. case self::TYPE_ACTION_NAME: return 'Page Title'; break;
  276. case self::TYPE_ECOMMERCE_ITEM_SKU: return 'Ecommerce Item SKU'; break;
  277. case self::TYPE_ECOMMERCE_ITEM_NAME: return 'Ecommerce Item Name'; break;
  278. case self::TYPE_ECOMMERCE_ITEM_CATEGORY: return 'Ecommerce Item Category'; break;
  279. default: throw new Exception("Unexpected action type ".$type); break;
  280. }
  281. }
  282. /**
  283. * Loads the idaction of the current action name and the current action url.
  284. * These idactions are used in the visitor logging table to link the visit information
  285. * (entry action, exit action) to the actions.
  286. * These idactions are also used in the table that links the visits and their actions.
  287. *
  288. * The methods takes care of creating a new record(s) in the action table if the existing
  289. * action name and action url doesn't exist yet.
  290. */
  291. function loadIdActionNameAndUrl()
  292. {
  293. if( $this->idActionUrl !== false
  294. && $this->idActionName !== false )
  295. {
  296. return;
  297. }
  298. $actions = array();
  299. $action = array($this->getActionName(), $this->getActionNameType());
  300. if(!is_null($action[1]))
  301. {
  302. $actions[] = $action;
  303. }
  304. $action = array($this->getActionUrl(), $this->getActionType());
  305. if(!is_null($action[1]))
  306. {
  307. $actions[] = $action;
  308. }
  309. $loadedActionIds = self::loadActionId($actions);
  310. foreach($loadedActionIds as $loadedActionId)
  311. {
  312. list($name, $type, $actionId) = $loadedActionId;
  313. if($type == $this->getActionType())
  314. {
  315. $this->idActionUrl = $actionId;
  316. }
  317. elseif($type == $this->getActionNameType())
  318. {
  319. $this->idActionName = $actionId;
  320. }
  321. }
  322. }
  323. /**
  324. * @param int $idSite
  325. */
  326. function setIdSite($idSite)
  327. {
  328. $this->idSite = $idSite;
  329. }
  330. function setTimestamp($timestamp)
  331. {
  332. $this->timestamp = $timestamp;
  333. }
  334. /**
  335. * Records in the DB the association between the visit and this action.
  336. *
  337. * @param int idVisit is the ID of the current visit in the DB table log_visit
  338. * @param int idRefererActionUrl is the ID of the last action done by the current visit.
  339. * @param int timeSpentRefererAction is the number of seconds since the last action was done.
  340. * It is directly related to idRefererActionUrl.
  341. */
  342. public function record( $idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction)
  343. {
  344. $this->loadIdActionNameAndUrl();
  345. $idActionName = in_array($this->getActionType(), array(Piwik_Tracker_Action::TYPE_ACTION_NAME, Piwik_Tracker_Action::TYPE_ACTION_URL))
  346. ? (int)$this->getIdActionName()
  347. : null;
  348. $insert = array(
  349. 'idvisit' => $idVisit,
  350. 'idsite' => $this->idSite,
  351. 'idvisitor' => $visitorIdCookie,
  352. 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->timestamp),
  353. 'idaction_url' => (int)$this->getIdActionUrl(),
  354. 'idaction_name' => $idActionName,
  355. 'idaction_url_ref' => $idRefererActionUrl,
  356. 'idaction_name_ref' => $idRefererActionName,
  357. 'time_spent_ref_action' => $timeSpentRefererAction
  358. );
  359. $customVariables = Piwik_Tracker_Visit::getCustomVariables($scope = 'page', $this->request);
  360. $insert = array_merge($insert, $customVariables);
  361. // Mysqli apparently does not like NULL inserts?
  362. $insertWithoutNulls = array();
  363. foreach($insert as $column => $value)
  364. {
  365. if(!is_null($value))
  366. {
  367. $insertWithoutNulls[$column] = $value;
  368. }
  369. }
  370. $fields = implode(", ", array_keys($insertWithoutNulls));
  371. $bind = array_values($insertWithoutNulls);
  372. $values = Piwik_Common::getSqlStringFieldsArray($insertWithoutNulls);
  373. $sql = "INSERT INTO ".Piwik_Common::prefixTable('log_link_visit_action'). " ($fields) VALUES ($values)";
  374. Piwik_Tracker::getDatabase()->query( $sql, $bind );
  375. $this->idLinkVisitAction = Piwik_Tracker::getDatabase()->lastInsertId();
  376. $info = array(
  377. 'idSite' => $this->idSite,
  378. 'idLinkVisitAction' => $this->idLinkVisitAction,
  379. 'idVisit' => $idVisit,
  380. 'idRefererActionUrl' => $idRefererActionUrl,
  381. 'idRefererActionName' => $idRefererActionName,
  382. 'timeSpentRefererAction' => $timeSpentRefererAction,
  383. );
  384. printDebug($insertWithoutNulls);
  385. /*
  386. * send the Action object ($this) and the list of ids ($info) as arguments to the event
  387. */
  388. Piwik_PostEvent('Tracker.Action.record', $this, $info);
  389. }
  390. /**
  391. * Returns the ID of the newly created record in the log_link_visit_action table
  392. *
  393. * @return int | false
  394. */
  395. public function getIdLinkVisitAction()
  396. {
  397. return $this->idLinkVisitAction;
  398. }
  399. /**
  400. * Generates the name of the action from the URL or the specified name.
  401. * Sets the name as $this->actionName
  402. *
  403. * @return array
  404. */
  405. protected function extractUrlAndActionNameFromRequest()
  406. {
  407. $actionName = null;
  408. // download?
  409. $downloadUrl = Piwik_Common::getRequestVar( 'download', '', 'string', $this->request);
  410. if(!empty($downloadUrl))
  411. {
  412. $actionType = self::TYPE_DOWNLOAD;
  413. $url = $downloadUrl;
  414. }
  415. // outlink?
  416. if(empty($actionType))
  417. {
  418. $outlinkUrl = Piwik_Common::getRequestVar( 'link', '', 'string', $this->request);
  419. if(!empty($outlinkUrl))
  420. {
  421. $actionType = self::TYPE_OUTLINK;
  422. $url = $outlinkUrl;
  423. }
  424. }
  425. // handle encoding
  426. $actionName = Piwik_Common::getRequestVar( 'action_name', '', 'string', $this->request);
  427. // defaults to page view
  428. if(empty($actionType))
  429. {
  430. $actionType = self::TYPE_ACTION_URL;
  431. $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request);
  432. // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067)
  433. $actionCategoryDelimiter = isset(Piwik_Tracker_Config::getInstance()->General['action_category_delimiter'])
  434. ? Piwik_Tracker_Config::getInstance()->General['action_category_delimiter']
  435. : Piwik_Tracker_Config::getInstance()->General['action_url_category_delimiter'];
  436. // create an array of the categories delimited by the delimiter
  437. $split = explode($actionCategoryDelimiter, $actionName);
  438. // trim every category
  439. $split = array_map('trim', $split);
  440. // remove empty categories
  441. $split = array_filter($split, 'strlen');
  442. // rebuild the name from the array of cleaned categories
  443. $actionName = implode($actionCategoryDelimiter, $split);
  444. }
  445. $url = self::cleanupString($url);
  446. if(!Piwik_Common::isLookLikeUrl($url))
  447. {
  448. $url = '';
  449. }
  450. $actionName = self::cleanupString($actionName);
  451. return array(
  452. 'name' => empty($actionName) ? '' : $actionName,
  453. 'type' => $actionType,
  454. 'url' => $url,
  455. );
  456. }
  457. /**
  458. * Clean up string contents (filter, truncate, ...)
  459. *
  460. * @param string $string Dirty string
  461. * @return string
  462. */
  463. protected static function cleanupString($string)
  464. {
  465. $string = trim($string);
  466. $string = str_replace(array("\n", "\r", "\0"), '', $string);
  467. $limit = Piwik_Tracker_Config::getInstance()->Tracker['page_maximum_length'];
  468. return substr($string, 0, $limit);
  469. }
  470. }