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