PageRenderTime 59ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Tracker/Visitor.php

https://github.com/CodeYellowBV/piwik
PHP | 296 lines | 241 code | 22 blank | 33 comment | 20 complexity | 6cd18ac38116b7774e29f9eb71e599cf MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Tracker;
  10. use Piwik\Common;
  11. use Piwik\Config;
  12. use Piwik\Plugins\CustomVariables\CustomVariables;
  13. use Piwik\Piwik;
  14. use Piwik\Tracker;
  15. class Visitor
  16. {
  17. function __construct(Request $request, Tracker\Settings $settings, $visitorInfo = array(), $customVariables = null)
  18. {
  19. $this->request = $request;
  20. $this->visitorInfo = $visitorInfo;
  21. $this->customVariables = $customVariables;
  22. $this->userInfo = $settings->getInfo();
  23. }
  24. /**
  25. * This methods tries to see if the visitor has visited the website before.
  26. *
  27. * We have to split the visitor into one of the category
  28. * - Known visitor
  29. * - New visitor
  30. */
  31. function recognize()
  32. {
  33. $this->visitorKnown = false;
  34. $configId = $this->userInfo['config_id'];
  35. $idVisitor = $this->request->getVisitorId();
  36. $isVisitorIdToLookup = !empty($idVisitor);
  37. if ($isVisitorIdToLookup) {
  38. $this->visitorInfo['idvisitor'] = $idVisitor;
  39. Common::printDebug("Matching visitors with: visitorId=" . bin2hex($this->visitorInfo['idvisitor']) . " OR configId=" . bin2hex($configId));
  40. } else {
  41. Common::printDebug("Visitor doesn't have the piwik cookie...");
  42. }
  43. $selectCustomVariables = '';
  44. // No custom var were found in the request, so let's copy the previous one in a potential conversion later
  45. if (!$this->customVariables) {
  46. $maxCustomVariables = CustomVariables::getMaxCustomVariables();
  47. for ($index = 1; $index <= $maxCustomVariables; $index++) {
  48. $selectCustomVariables .= ', custom_var_k' . $index . ', custom_var_v' . $index;
  49. }
  50. }
  51. $persistedVisitAttributes = self::getVisitFieldsPersist();
  52. $selectFields = implode(", ", $persistedVisitAttributes);
  53. $select = "SELECT
  54. visit_last_action_time,
  55. visit_first_action_time,
  56. $selectFields
  57. $selectCustomVariables
  58. ";
  59. $from = "FROM " . Common::prefixTable('log_visit');
  60. list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit();
  61. $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup);
  62. // Two use cases:
  63. // 1) there is no visitor ID so we try to match only on config_id (heuristics)
  64. // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed,
  65. // importing server access logs with import_logs.py, etc.
  66. // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign
  67. // this page view to the wrong visitor, but this is better than creating artificial visits.
  68. // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API),
  69. // and in these cases, we force to look up this visitor id
  70. $whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?";
  71. $bindSql = array(
  72. $timeLookBack,
  73. $timeLookAhead,
  74. $this->request->getIdSite()
  75. );
  76. if ($shouldMatchOneFieldOnly) {
  77. if ($isVisitorIdToLookup) {
  78. $whereCommon .= ' AND idvisitor = ?';
  79. $bindSql[] = $this->visitorInfo['idvisitor'];
  80. } else {
  81. $whereCommon .= ' AND config_id = ?';
  82. $bindSql[] = $configId;
  83. }
  84. $sql = "$select
  85. $from
  86. WHERE " . $whereCommon . "
  87. ORDER BY visit_last_action_time DESC
  88. LIMIT 1";
  89. } // We have a config_id AND a visitor_id. We match on either of these.
  90. // Why do we also match on config_id?
  91. // we do not trust the visitor ID only. Indeed, some browsers, or browser addons,
  92. // cause the visitor id from the 1st party cookie to be different on each page view!
  93. // It is not acceptable to create a new visit every time such browser does a page view,
  94. // so we also backup by searching for matching config_id.
  95. // We use a UNION here so that each sql query uses its own INDEX
  96. else {
  97. // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time)
  98. $where = ' AND config_id = ?';
  99. $bindSql[] = $configId;
  100. $sqlConfigId = "$select ,
  101. 0 as priority
  102. $from
  103. WHERE $whereCommon $where
  104. ORDER BY visit_last_action_time DESC
  105. LIMIT 1
  106. ";
  107. // will use INDEX index_idsite_idvisitor (idsite, idvisitor)
  108. $bindSql[] = $timeLookBack;
  109. $bindSql[] = $timeLookAhead;
  110. $bindSql[] = $this->request->getIdSite();
  111. $where = ' AND idvisitor = ?';
  112. $bindSql[] = $this->visitorInfo['idvisitor'];
  113. $sqlVisitorId = "$select ,
  114. 1 as priority
  115. $from
  116. WHERE $whereCommon $where
  117. ORDER BY visit_last_action_time DESC
  118. LIMIT 1
  119. ";
  120. // We join both queries and favor the one matching the visitor_id if it did match
  121. $sql = " ( $sqlConfigId )
  122. UNION
  123. ( $sqlVisitorId )
  124. ORDER BY priority DESC
  125. LIMIT 1";
  126. }
  127. $visitRow = Tracker::getDatabase()->fetch($sql, $bindSql);
  128. $isNewVisitForced = $this->request->getParam('new_visit');
  129. $isNewVisitForced = !empty($isNewVisitForced);
  130. $newVisitEnforcedAPI = $isNewVisitForced
  131. && ($this->request->isAuthenticated()
  132. || !Config::getInstance()->Tracker['new_visit_api_requires_admin']);
  133. $enforceNewVisit = $newVisitEnforcedAPI || Config::getInstance()->Debug['tracker_always_new_visitor'];
  134. if (!$enforceNewVisit
  135. && $visitRow
  136. && count($visitRow) > 0
  137. ) {
  138. // These values will be used throughout the request
  139. $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']);
  140. $this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']);
  141. foreach($persistedVisitAttributes as $field) {
  142. $this->visitorInfo[$field] = $visitRow[$field];
  143. }
  144. // Custom Variables copied from Visit in potential later conversion
  145. if (!empty($selectCustomVariables)) {
  146. $maxCustomVariables = CustomVariables::getMaxCustomVariables();
  147. for ($i = 1; $i <= $maxCustomVariables; $i++) {
  148. if (isset($visitRow['custom_var_k' . $i])
  149. && strlen($visitRow['custom_var_k' . $i])
  150. ) {
  151. $this->visitorInfo['custom_var_k' . $i] = $visitRow['custom_var_k' . $i];
  152. }
  153. if (isset($visitRow['custom_var_v' . $i])
  154. && strlen($visitRow['custom_var_v' . $i])
  155. ) {
  156. $this->visitorInfo['custom_var_v' . $i] = $visitRow['custom_var_v' . $i];
  157. }
  158. }
  159. }
  160. $this->visitorKnown = true;
  161. Common::printDebug("The visitor is known (idvisitor = " . bin2hex($this->visitorInfo['idvisitor']) . ",
  162. config_id = " . bin2hex($configId) . ",
  163. idvisit = {$this->visitorInfo['idvisit']},
  164. last action = " . date("r", $this->visitorInfo['visit_last_action_time']) . ",
  165. first action = " . date("r", $this->visitorInfo['visit_first_action_time']) . ",
  166. visit_goal_buyer' = " . $this->visitorInfo['visit_goal_buyer'] . ")");
  167. //Common::printDebug($this->visitorInfo);
  168. } else {
  169. Common::printDebug("The visitor was not matched with an existing visitor...");
  170. }
  171. }
  172. /**
  173. * By default, we look back 30 minutes to find a previous visitor (for performance reasons).
  174. * In some cases, it is useful to look back and count unique visitors more accurately. You can set custom lookback window in
  175. * [Tracker] window_look_back_for_visitor
  176. *
  177. * The returned value is the window range (Min, max) that the matched visitor should fall within
  178. *
  179. * @return array( datetimeMin, datetimeMax )
  180. */
  181. protected function getWindowLookupThisVisit()
  182. {
  183. $visitStandardLength = Config::getInstance()->Tracker['visit_standard_length'];
  184. $lookBackNSecondsCustom = Config::getInstance()->Tracker['window_look_back_for_visitor'];
  185. $lookAheadNSeconds = $visitStandardLength;
  186. $lookBackNSeconds = $visitStandardLength;
  187. if ($lookBackNSecondsCustom > $lookBackNSeconds) {
  188. $lookBackNSeconds = $lookBackNSecondsCustom;
  189. }
  190. $timeLookBack = date('Y-m-d H:i:s', $this->request->getCurrentTimestamp() - $lookBackNSeconds);
  191. $timeLookAhead = date('Y-m-d H:i:s', $this->request->getCurrentTimestamp() + $lookAheadNSeconds);
  192. return array($timeLookBack, $timeLookAhead);
  193. }
  194. protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup)
  195. {
  196. // This setting would be enabled for Intranet websites, to ensure that visitors using all the same computer config, same IP
  197. // are not counted as 1 visitor. In this case, we want to enforce and trust the visitor ID from the cookie.
  198. $trustCookiesOnly = Config::getInstance()->Tracker['trust_visitors_cookies'];
  199. // If a &cid= was set, we force to select this visitor (or create a new one)
  200. $isForcedVisitorIdMustMatch = ($this->request->getForcedVisitorId() != null);
  201. $shouldMatchOneFieldOnly = (($isVisitorIdToLookup && $trustCookiesOnly)
  202. || $isForcedVisitorIdMustMatch
  203. || !$isVisitorIdToLookup);
  204. return $shouldMatchOneFieldOnly;
  205. }
  206. /**
  207. * @return array
  208. */
  209. public static function getVisitFieldsPersist()
  210. {
  211. $fields = array(
  212. 'idvisitor',
  213. 'idvisit',
  214. 'visit_exit_idaction_url',
  215. 'visit_exit_idaction_name',
  216. 'visitor_returning',
  217. 'visitor_days_since_first',
  218. 'visitor_days_since_order',
  219. 'visitor_count_visits',
  220. 'visit_goal_buyer',
  221. 'location_country',
  222. 'location_region',
  223. 'location_city',
  224. 'location_latitude',
  225. 'location_longitude',
  226. 'referer_name',
  227. 'referer_keyword',
  228. 'referer_type',
  229. );
  230. /**
  231. * Triggered when checking if the current action being tracked belongs to an existing visit.
  232. *
  233. * This event collects a list of [visit entity]() properties that should be loaded when reading
  234. * the existing visit. Properties that appear in this list will be available in other tracking
  235. * events such as {@hook Tracker.newConversionInformation} and {@hook Tracker.newVisitorInformation}.
  236. *
  237. * Plugins can use this event to load additional visit entity properties for later use during tracking.
  238. * When you add fields to this $fields array, they will be later available in Tracker.newConversionInformation
  239. *
  240. * **Example**
  241. *
  242. * Piwik::addAction('Tracker.getVisitFieldsToPersist', function (&$fields) {
  243. * $fields[] = 'custom_visit_property';
  244. * });
  245. *
  246. * @param array &$fields The list of visit properties to load.
  247. */
  248. Piwik::postEvent('Tracker.getVisitFieldsToPersist', array(&$fields));
  249. return $fields;
  250. }
  251. function getVisitorInfo()
  252. {
  253. return $this->visitorInfo;
  254. }
  255. function isVisitorKnown()
  256. {
  257. return $this->visitorKnown === true;
  258. }
  259. }