PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Tracker / Visit.php
matomo / app / core / Tracker Last commit date
Config 4 months ago Db 3 months ago Handler 2 years ago Visit 1 month ago Action.php 3 months ago ActionPageview.php 2 years ago BotRequest.php 3 months ago BotRequestProcessor.php 1 month ago Cache.php 6 months ago Db.php 1 year ago Failures.php 6 months ago FingerprintSalt.php 1 year ago GoalManager.php 1 month ago Handler.php 2 years ago IgnoreCookie.php 1 year ago LogTable.php 1 year ago Model.php 6 months ago PageUrl.php 2 weeks ago Request.php 1 month ago RequestHandlerTrait.php 4 months ago RequestProcessor.php 1 month ago RequestSet.php 6 months ago Response.php 3 months ago ScheduledTasksRunner.php 1 year ago Settings.php 3 months ago TableLogAction.php 6 months ago TrackerCodeGenerator.php 1 year ago TrackerConfig.php 1 month ago Visit.php 3 months ago VisitExcluded.php 3 months ago VisitInterface.php 3 months ago Visitor.php 1 month ago VisitorNotFoundInDb.php 1 month ago VisitorRecognizer.php 1 year ago
Visit.php
455 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Tracker;
10
11 use Piwik\Common;
12 use Piwik\Config;
13 use Piwik\Container\StaticContainer;
14 use Matomo\Network\IPUtils;
15 use Piwik\Plugin\Dimension\VisitDimension;
16 use Piwik\Plugins\Actions\Tracker\ActionsRequestProcessor;
17 use Piwik\Tracker;
18 use Piwik\Tracker\Visit\VisitProperties;
19 /**
20 * Class used to handle a Visit.
21 * A visit is either NEW or KNOWN.
22 * - If a visit is NEW then we process the visitor information (settings, referrers, etc.) and save
23 * a new line in the log_visit table.
24 * - If a visit is KNOWN then we update the visit row in the log_visit table, updating the number of pages
25 * views, time spent, etc.
26 *
27 * Whether a visit is NEW or KNOWN we also save the action in the DB.
28 * One request to the matomo.php script is associated to one action.
29 *
30 */
31 class Visit implements \Piwik\Tracker\VisitInterface
32 {
33 use \Piwik\Tracker\RequestHandlerTrait;
34 public const UNKNOWN_CODE = 'xx';
35 /**
36 * @var GoalManager
37 */
38 protected $goalManager;
39 /**
40 * @var Request
41 */
42 protected $request;
43 /**
44 * @var Settings
45 */
46 protected $userSettings;
47 public static $dimensions;
48 /**
49 * @var RequestProcessor[]
50 */
51 protected $requestProcessors;
52 /**
53 * @var VisitProperties
54 */
55 protected $visitProperties;
56 /**
57 * @var VisitProperties
58 */
59 protected $previousVisitProperties;
60 public function __construct()
61 {
62 $requestProcessors = StaticContainer::get('Piwik\\Plugin\\RequestProcessors');
63 $this->requestProcessors = $requestProcessors->getRequestProcessors();
64 $this->visitProperties = null;
65 $this->userSettings = StaticContainer::get('Piwik\\Tracker\\Settings');
66 }
67 public function setRequest(\Piwik\Tracker\Request $request)
68 {
69 $this->request = $request;
70 }
71 /**
72 * Main algorithm to handle the visit.
73 *
74 * Once we have the visitor information, we have to determine if the visit is a new or a known visit.
75 *
76 * 1) When the last action was done more than 30min ago,
77 * or if the visitor is new, then this is a new visit.
78 *
79 * 2) If the last action is less than 30min ago, then the same visit is going on.
80 * Because the visit goes on, we can get the time spent during the last action.
81 *
82 * NB:
83 * - In the case of a new visit, then the time spent
84 * during the last action of the previous visit is unknown.
85 *
86 * - In the case of a new visit but with a known visitor,
87 * we can set the 'returning visitor' flag.
88 *
89 * In all the cases we set a cookie to the visitor with the new information.
90 */
91 public function handle()
92 {
93 $this->checkSiteExists($this->request);
94 foreach ($this->requestProcessors as $processor) {
95 Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()...");
96 $processor->manipulateRequest($this->request);
97 }
98 $this->validateRequest($this->request);
99 $this->visitProperties = new VisitProperties();
100 foreach ($this->requestProcessors as $processor) {
101 Common::printDebug("Executing " . get_class($processor) . "::processRequestParams()...");
102 $abort = $processor->processRequestParams($this->visitProperties, $this->request);
103 if ($abort) {
104 Common::printDebug("-> aborting due to processRequestParams method");
105 return;
106 }
107 }
108 $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit');
109 if (!$isNewVisit) {
110 $isNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions(), 'shouldForceNewVisit');
111 $this->request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit);
112 }
113 foreach ($this->requestProcessors as $processor) {
114 Common::printDebug("Executing " . get_class($processor) . "::afterRequestProcessed()...");
115 $abort = $processor->afterRequestProcessed($this->visitProperties, $this->request);
116 if ($abort) {
117 Common::printDebug("-> aborting due to afterRequestProcessed method");
118 return;
119 }
120 }
121 $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit');
122 $this->previousVisitProperties = new VisitProperties($this->request->getMetadata('CoreHome', 'lastKnownVisit') ?: []);
123 // Known visit when:
124 // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor
125 // OR
126 // - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor()
127 // )
128 // AND
129 // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit()
130 if (!$isNewVisit) {
131 try {
132 $this->handleExistingVisit($this->request->getMetadata('Goals', 'visitIsConverted'));
133 } catch (\Piwik\Tracker\VisitorNotFoundInDb $e) {
134 $this->request->setMetadata('CoreHome', 'visitorNotFoundInDb', \true);
135 // TODO: perhaps we should just abort here?
136 }
137 }
138 // New visit when:
139 // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit()
140 // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor()
141 // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB
142 if ($isNewVisit) {
143 $this->handleNewVisit($this->request->getMetadata('Goals', 'visitIsConverted'));
144 }
145 // update the cookie with the new visit information
146 $this->request->setThirdPartyCookie($this->request->getVisitorIdForThirdPartyCookie());
147 foreach ($this->requestProcessors as $processor) {
148 if (!$isNewVisit && $processor instanceof ActionsRequestProcessor) {
149 // already processed earlier when handling exisitng visit see {@link self::handleExistingVisit()}
150 continue;
151 }
152 Common::printDebug("Executing " . get_class($processor) . "::recordLogs()...");
153 $processor->recordLogs($this->visitProperties, $this->request);
154 }
155 $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished($this->request);
156 }
157 /**
158 * In the case of a known visit, we have to do the following actions:
159 *
160 * 1) Insert the new action
161 * 2) Update the visit information
162 *
163 * @param bool $visitIsConverted
164 * @throws VisitorNotFoundInDb
165 */
166 protected function handleExistingVisit($visitIsConverted)
167 {
168 Common::printDebug("Visit is known (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")");
169 // TODO it should be its own dimension
170 $this->visitProperties->setProperty('time_spent_ref_action', $this->getTimeSpentReferrerAction());
171 $valuesToUpdate = $this->getExistingVisitFieldsToUpdate($visitIsConverted);
172 // update visitorInfo
173 foreach ($valuesToUpdate as $name => $value) {
174 $this->visitProperties->setProperty($name, $value);
175 }
176 foreach ($this->requestProcessors as $processor) {
177 // for improving performance we create a log_link_visit_action entry before updating the visit.
178 // this way we save one extra update on log_visit in custom dimensions.
179 // Refs https://github.com/matomo-org/matomo/issues/17173
180 if ($processor instanceof ActionsRequestProcessor) {
181 Common::printDebug("Executing " . get_class($processor) . "::recordLogs()...");
182 $processor->recordLogs($this->visitProperties, $this->request);
183 }
184 }
185 foreach ($this->requestProcessors as $processor) {
186 $processor->onExistingVisit($valuesToUpdate, $this->visitProperties, $this->request);
187 }
188 // we we remove values that haven't actually changed and are still the same when comparing to the initially
189 // selected visit row. In best case this avoids the update completely. Eg when there is a bulk tracking request
190 // of many content impressions. Then it will update the visit in the first request of the bulk request, and
191 // all other visits that have same visit_last_action_time etc will be ignored and won't issue an update SQL
192 // statement at all avoiding potential lock wait time when too many requests try to update the same visit at
193 // same time
194 $visitorRecognizer = StaticContainer::get(\Piwik\Tracker\VisitorRecognizer::class);
195 $valuesToUpdate = $visitorRecognizer->removeUnchangedValues($valuesToUpdate, $this->previousVisitProperties);
196 $this->updateExistingVisit($valuesToUpdate);
197 $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp());
198 }
199 /**
200 * @return int Time in seconds
201 */
202 protected function getTimeSpentReferrerAction()
203 {
204 $timeSpent = $this->request->getCurrentTimestamp() - $this->visitProperties->getProperty('visit_last_action_time');
205 if ($timeSpent < 0) {
206 $timeSpent = 0;
207 }
208 $visitStandardLength = $this->getVisitStandardLength();
209 if ($timeSpent > $visitStandardLength) {
210 $timeSpent = $visitStandardLength;
211 }
212 return $timeSpent;
213 }
214 /**
215 * In the case of a new visit, we have to do the following actions:
216 *
217 * 1) Insert the new action
218 *
219 * 2) Insert the visit information
220 *
221 * @param bool $visitIsConverted
222 */
223 protected function handleNewVisit($visitIsConverted)
224 {
225 Common::printDebug("New Visit (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")");
226 $this->setNewVisitorInformation();
227 $dimensions = $this->getAllVisitDimensions();
228 $this->triggerHookOnDimensions($dimensions, 'onNewVisit');
229 if ($visitIsConverted) {
230 $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit');
231 }
232 foreach ($this->requestProcessors as $processor) {
233 $processor->onNewVisit($this->visitProperties, $this->request);
234 }
235 $this->printVisitorInformation();
236 $idVisit = $this->insertNewVisit($this->visitProperties->getProperties());
237 $this->visitProperties->setProperty('idvisit', $idVisit);
238 $this->visitProperties->setProperty('visit_first_action_time', $this->request->getCurrentTimestamp());
239 $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp());
240 }
241 private function getModel()
242 {
243 return new \Piwik\Tracker\Model();
244 }
245 /**
246 * Returns visitor cookie
247 *
248 * @return string binary
249 */
250 protected function getVisitorIdcookie()
251 {
252 $isKnown = $this->request->getMetadata('CoreHome', 'isVisitorKnown');
253 if ($isKnown) {
254 return $this->visitProperties->getProperty('idvisitor');
255 }
256 // If the visitor had a first party ID cookie, then we use this value
257 $idVisitor = $this->visitProperties->getProperty('idvisitor');
258 if (!empty($idVisitor) && Tracker::LENGTH_BINARY_ID == strlen($this->visitProperties->getProperty('idvisitor'))) {
259 return $this->visitProperties->getProperty('idvisitor');
260 }
261 return Common::hex2bin($this->generateUniqueVisitorId());
262 }
263 /**
264 * @return string returns random 16 chars hex string
265 */
266 public static function generateUniqueVisitorId()
267 {
268 return substr(Common::generateUniqId(), 0, Tracker::LENGTH_HEX_ID_STRING);
269 }
270 /**
271 * Returns the visitor's IP address
272 *
273 * @return string
274 */
275 protected function getVisitorIp()
276 {
277 return $this->visitProperties->getProperty('location_ip');
278 }
279 // is the host any of the registered URLs for this website?
280 public static function isHostKnownAliasHost($urlHost, $idSite)
281 {
282 $websiteData = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($idSite);
283 if (isset($websiteData['hosts'])) {
284 $canonicalHosts = array();
285 foreach ($websiteData['hosts'] as $host) {
286 $canonicalHosts[] = self::toCanonicalHost($host);
287 }
288 $canonicalHost = self::toCanonicalHost($urlHost);
289 if (in_array($canonicalHost, $canonicalHosts)) {
290 return \true;
291 }
292 }
293 return \false;
294 }
295 private static function toCanonicalHost($host)
296 {
297 $hostLower = mb_strtolower($host);
298 return str_replace('www.', '', $hostLower);
299 }
300 /**
301 * @param $valuesToUpdate
302 * @throws VisitorNotFoundInDb
303 */
304 protected function updateExistingVisit($valuesToUpdate)
305 {
306 if (empty($valuesToUpdate)) {
307 Common::printDebug('There are no values to be updated for this visit');
308 return;
309 }
310 $idSite = $this->request->getIdSite();
311 $idVisit = $this->visitProperties->getProperty('idvisit');
312 $wasInserted = $this->getModel()->updateVisit($idSite, $idVisit, $valuesToUpdate);
313 // Debug output
314 if (isset($valuesToUpdate['idvisitor'])) {
315 $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']);
316 }
317 if ($wasInserted) {
318 Common::printDebug('Updated existing visit: ' . var_export($valuesToUpdate, \true));
319 } elseif (!$this->getModel()->hasVisit($idSite, $idVisit)) {
320 // mostly for WordPress. see https://github.com/matomo-org/matomo/pull/15587
321 // as WP doesn't set `MYSQLI_CLIENT_FOUND_ROWS` and therefore when the update succeeded but no value changed
322 // it would still return 0 vs OnPremise would return 1 or 2.
323 throw new \Piwik\Tracker\VisitorNotFoundInDb("The visitor with idvisitor=" . bin2hex($this->visitProperties->getProperty('idvisitor')) . " and idvisit=" . @$this->visitProperties->getProperty('idvisit') . " wasn't found in the DB, we fallback to a new visitor");
324 }
325 }
326 private function printVisitorInformation()
327 {
328 $debugVisitInfo = $this->visitProperties->getProperties();
329 $debugVisitInfo['idvisitor'] = isset($debugVisitInfo['idvisitor']) ? bin2hex($debugVisitInfo['idvisitor']) : '';
330 $debugVisitInfo['config_id'] = isset($debugVisitInfo['config_id']) ? bin2hex($debugVisitInfo['config_id']) : '';
331 $debugVisitInfo['location_ip'] = IPUtils::binaryToStringIP($debugVisitInfo['location_ip']);
332 Common::printDebug($debugVisitInfo);
333 }
334 private function setNewVisitorInformation()
335 {
336 $idVisitor = $this->getVisitorIdcookie();
337 $visitorIp = $this->getVisitorIp();
338 $configId = $this->request->getMetadata('CoreHome', 'visitorId');
339 $this->visitProperties->clearProperties();
340 $this->visitProperties->setProperty('idvisitor', $idVisitor);
341 $this->visitProperties->setProperty('config_id', $configId);
342 $this->visitProperties->setProperty('location_ip', $visitorIp);
343 }
344 /**
345 * Gather fields=>values that needs to be updated for the existing visit in log_visit
346 *
347 * @param $visitIsConverted
348 * @return array
349 */
350 private function getExistingVisitFieldsToUpdate($visitIsConverted)
351 {
352 $valuesToUpdate = array();
353 $valuesToUpdate = $this->setIdVisitorForExistingVisit($valuesToUpdate);
354 $dimensions = $this->getAllVisitDimensions();
355 $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onExistingVisit', $valuesToUpdate);
356 if ($visitIsConverted) {
357 $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit', $valuesToUpdate);
358 }
359 // Custom Variables overwrite previous values on each page view
360 return $valuesToUpdate;
361 }
362 /**
363 * @param VisitDimension[] $dimensions
364 * @param string $hook
365 * @param array|null $valuesToUpdate If null, $this->visitorInfo will be updated
366 *
367 * @return array|null The updated $valuesToUpdate or null if no $valuesToUpdate given
368 */
369 private function triggerHookOnDimensions($dimensions, $hook, $valuesToUpdate = null)
370 {
371 $visitor = $this->makeVisitorFacade();
372 /** @var Action $action */
373 $action = $this->request->getMetadata('Actions', 'action');
374 foreach ($dimensions as $dimension) {
375 $value = $dimension->{$hook}($this->request, $visitor, $action);
376 if ($value !== \false) {
377 $fieldName = $dimension->getColumnName();
378 $visitor->setVisitorColumn($fieldName, $value);
379 if (is_float($value)) {
380 $value = Common::forceDotAsSeparatorForDecimalPoint($value);
381 }
382 if ($valuesToUpdate !== null) {
383 $valuesToUpdate[$fieldName] = $value;
384 } else {
385 $this->visitProperties->setProperty($fieldName, $value);
386 }
387 }
388 }
389 return $valuesToUpdate;
390 }
391 private function triggerPredicateHookOnDimensions($dimensions, $hook)
392 {
393 $visitor = $this->makeVisitorFacade();
394 /** @var Action $action */
395 $action = $this->request->getMetadata('Actions', 'action');
396 foreach ($dimensions as $dimension) {
397 if ($dimension->{$hook}($this->request, $visitor, $action)) {
398 return \true;
399 }
400 }
401 return \false;
402 }
403 protected function getAllVisitDimensions()
404 {
405 if (is_null(self::$dimensions)) {
406 self::$dimensions = VisitDimension::getAllDimensions();
407 $dimensionNames = array();
408 foreach (self::$dimensions as $dimension) {
409 $dimensionNames[] = $dimension->getColumnName();
410 }
411 Common::printDebug("Following dimensions have been collected from plugins: " . implode(", ", $dimensionNames));
412 }
413 return self::$dimensions;
414 }
415 private function getVisitStandardLength()
416 {
417 return Config::getInstance()->Tracker['visit_standard_length'];
418 }
419 /**
420 * @param array $valuesToUpdate
421 * @return array
422 */
423 private function setIdVisitorForExistingVisit($valuesToUpdate)
424 {
425 $idVisitor = $this->visitProperties->getProperty('idvisitor');
426 if (!empty($idVisitor) && Tracker::LENGTH_BINARY_ID == strlen($idVisitor)) {
427 $valuesToUpdate['idvisitor'] = $idVisitor;
428 }
429 $visitorId = $this->request->getVisitorId();
430 if ($visitorId && strlen($visitorId) === Tracker::LENGTH_BINARY_ID) {
431 // Might update the idvisitor when it was forced or overwritten for this visit
432 $valuesToUpdate['idvisitor'] = $this->request->getVisitorId();
433 }
434 if (\Piwik\Tracker\TrackerConfig::getConfigValue('enable_userid_overwrites_visitorid', $this->request->getIdSiteIfExists())) {
435 // User ID takes precedence and overwrites idvisitor value
436 $userId = $this->request->getForcedUserId();
437 if ($userId) {
438 $userIdHash = $this->request->getUserIdHashed($userId);
439 $binIdVisitor = Common::hex2bin($userIdHash);
440 $this->visitProperties->setProperty('idvisitor', $binIdVisitor);
441 $valuesToUpdate['idvisitor'] = $binIdVisitor;
442 }
443 }
444 return $valuesToUpdate;
445 }
446 protected function insertNewVisit($visit)
447 {
448 return $this->getModel()->createVisit($visit);
449 }
450 private function makeVisitorFacade()
451 {
452 return \Piwik\Tracker\Visitor::makeFromVisitProperties($this->visitProperties, $this->request, $this->previousVisitProperties);
453 }
454 }
455