Download core/Tracker/Visit.php from Leon4gr45/matomo: direct link, hf CLI and curl.
- Browser
- Download file 21.4 kB
-
https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/core/Tracker/Visit.php
- Command line
-
hf download hf://spaces/Leon4gr45/matomo/core/Tracker/Visit.php
-
curl -L -o Visit.php https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/core/Tracker/Visit.php
21.4 kB
| /** | |
| * Matomo - free/libre analytics platform | |
| * | |
| * @link https://matomo.org | |
| * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | |
| */ | |
| namespace Piwik\Tracker; | |
| use Piwik\Common; | |
| use Piwik\Config; | |
| use Piwik\Container\StaticContainer; | |
| use Matomo\Network\IPUtils; | |
| use Piwik\Plugin\Dimension\VisitDimension; | |
| use Piwik\Plugin\LogTablesProvider; | |
| use Piwik\Plugins\Actions\Tracker\ActionsRequestProcessor; | |
| use Piwik\Tracker; | |
| use Piwik\Tracker\Visit\VisitProperties; | |
| /** | |
| * Class used to handle a Visit. | |
| * A visit is either NEW or KNOWN. | |
| * - If a visit is NEW then we process the visitor information (settings, referrers, etc.) and save | |
| * a new line in the log_visit table. | |
| * - If a visit is KNOWN then we update the visit row in the log_visit table, updating the number of pages | |
| * views, time spent, etc. | |
| * | |
| * Whether a visit is NEW or KNOWN we also save the action in the DB. | |
| * One request to the matomo.php script is associated to one action. | |
| * | |
| */ | |
| class Visit implements VisitInterface | |
| { | |
| use RequestHandlerTrait; | |
| public const UNKNOWN_CODE = 'xx'; | |
| /** | |
| * @var GoalManager | |
| */ | |
| protected $goalManager; | |
| /** | |
| * @var Request | |
| */ | |
| protected $request; | |
| /** | |
| * @var Settings | |
| */ | |
| protected $userSettings; | |
| public static $dimensions; | |
| /** | |
| * @var RequestProcessor[] | |
| */ | |
| protected $requestProcessors; | |
| /** | |
| * @var VisitProperties|null | |
| */ | |
| protected $visitProperties; | |
| /** | |
| * @var VisitProperties | |
| */ | |
| protected $previousVisitProperties; | |
| public function __construct() | |
| { | |
| $requestProcessors = StaticContainer::get('Piwik\Plugin\RequestProcessors'); | |
| $this->requestProcessors = $requestProcessors->getRequestProcessors(); | |
| $this->visitProperties = null; | |
| $this->userSettings = StaticContainer::get('Piwik\Tracker\Settings'); | |
| } | |
| public function setRequest(Request $request) | |
| { | |
| $this->request = $request; | |
| } | |
| /** | |
| * Main algorithm to handle the visit. | |
| * | |
| * Once we have the visitor information, we have to determine if the visit is a new or a known visit. | |
| * | |
| * 1) When the last action was done more than 30min ago, | |
| * or if the visitor is new, then this is a new visit. | |
| * | |
| * 2) If the last action is less than 30min ago, then the same visit is going on. | |
| * Because the visit goes on, we can get the time spent during the last action. | |
| * | |
| * NB: | |
| * - In the case of a new visit, then the time spent | |
| * during the last action of the previous visit is unknown. | |
| * | |
| * - In the case of a new visit but with a known visitor, | |
| * we can set the 'returning visitor' flag. | |
| * | |
| * In all the cases we set a cookie to the visitor with the new information. | |
| */ | |
| public function handle() | |
| { | |
| $this->checkSiteExists($this->request); | |
| foreach ($this->requestProcessors as $processor) { | |
| Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()..."); | |
| $processor->manipulateRequest($this->request); | |
| } | |
| $this->validateRequest($this->request); | |
| $this->visitProperties = new VisitProperties(); | |
| foreach ($this->requestProcessors as $processor) { | |
| Common::printDebug("Executing " . get_class($processor) . "::processRequestParams()..."); | |
| $abort = $processor->processRequestParams($this->visitProperties, $this->request); | |
| if ($abort) { | |
| Common::printDebug("-> aborting due to processRequestParams method"); | |
| return; | |
| } | |
| } | |
| $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); | |
| if (!$isNewVisit) { | |
| $isNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions(), 'shouldForceNewVisit'); | |
| $this->request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit); | |
| } | |
| foreach ($this->requestProcessors as $processor) { | |
| Common::printDebug("Executing " . get_class($processor) . "::afterRequestProcessed()..."); | |
| $abort = $processor->afterRequestProcessed($this->visitProperties, $this->request); | |
| if ($abort) { | |
| Common::printDebug("-> aborting due to afterRequestProcessed method"); | |
| return; | |
| } | |
| } | |
| $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); | |
| $this->previousVisitProperties = new VisitProperties($this->request->getMetadata('CoreHome', 'lastKnownVisit') ?: []); | |
| // Known visit when: | |
| // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor | |
| // OR | |
| // - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor() | |
| // ) | |
| // AND | |
| // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() | |
| if (!$isNewVisit) { | |
| try { | |
| $this->handleExistingVisit($this->request->getMetadata('Goals', 'visitIsConverted')); | |
| } catch (VisitorNotFoundInDb $e) { | |
| $this->request->setMetadata('CoreHome', 'visitorNotFoundInDb', true); // TODO: perhaps we should just abort here? | |
| } | |
| } | |
| // New visit when: | |
| // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit() | |
| // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor() | |
| // - 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 | |
| if ($isNewVisit) { | |
| $this->handleNewVisit($this->request->getMetadata('Goals', 'visitIsConverted')); | |
| } | |
| // update the cookie with the new visit information | |
| $this->request->setThirdPartyCookie($this->request->getVisitorIdForThirdPartyCookie()); | |
| foreach ($this->requestProcessors as $processor) { | |
| if (!$isNewVisit && $processor instanceof ActionsRequestProcessor) { | |
| // already processed earlier when handling exisitng visit see {@link self::handleExistingVisit()} | |
| continue; | |
| } | |
| Common::printDebug("Executing " . get_class($processor) . "::recordLogs()..."); | |
| $processor->recordLogs($this->visitProperties, $this->request); | |
| } | |
| $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished($this->request); | |
| } | |
| /** | |
| * In the case of a known visit, we have to do the following actions: | |
| * | |
| * 1) Insert the new action | |
| * 2) Update the visit information | |
| * | |
| * @param bool $visitIsConverted | |
| * @throws VisitorNotFoundInDb | |
| */ | |
| protected function handleExistingVisit($visitIsConverted) | |
| { | |
| Common::printDebug("Visit is known (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")"); | |
| // TODO it should be its own dimension | |
| $this->visitProperties->setProperty('time_spent_ref_action', $this->getTimeSpentReferrerAction()); | |
| $valuesToUpdate = $this->getExistingVisitFieldsToUpdate($visitIsConverted); | |
| // update visitorInfo | |
| foreach ($valuesToUpdate as $name => $value) { | |
| $this->visitProperties->setProperty($name, $value); | |
| } | |
| foreach ($this->requestProcessors as $processor) { | |
| // for improving performance we create a log_link_visit_action entry before updating the visit. | |
| // this way we save one extra update on log_visit in custom dimensions. | |
| // Refs https://github.com/matomo-org/matomo/issues/17173 | |
| if ($processor instanceof ActionsRequestProcessor) { | |
| Common::printDebug("Executing " . get_class($processor) . "::recordLogs()..."); | |
| $processor->recordLogs($this->visitProperties, $this->request); | |
| } | |
| } | |
| foreach ($this->requestProcessors as $processor) { | |
| $processor->onExistingVisit($valuesToUpdate, $this->visitProperties, $this->request); | |
| } | |
| // we we remove values that haven't actually changed and are still the same when comparing to the initially | |
| // selected visit row. In best case this avoids the update completely. Eg when there is a bulk tracking request | |
| // of many content impressions. Then it will update the visit in the first request of the bulk request, and | |
| // all other visits that have same visit_last_action_time etc will be ignored and won't issue an update SQL | |
| // statement at all avoiding potential lock wait time when too many requests try to update the same visit at | |
| // same time | |
| $visitorRecognizer = StaticContainer::get(VisitorRecognizer::class); | |
| $valuesToUpdate = $visitorRecognizer->removeUnchangedValues($valuesToUpdate, $this->previousVisitProperties); | |
| $this->updateExistingVisit($valuesToUpdate); | |
| $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp()); | |
| } | |
| /** | |
| * @return int Time in seconds | |
| */ | |
| protected function getTimeSpentReferrerAction() | |
| { | |
| $timeSpent = $this->request->getCurrentTimestamp() - | |
| $this->visitProperties->getProperty('visit_last_action_time'); | |
| if ($timeSpent < 0) { | |
| $timeSpent = 0; | |
| } | |
| $visitStandardLength = $this->getVisitStandardLength(); | |
| if ($timeSpent > $visitStandardLength) { | |
| $timeSpent = $visitStandardLength; | |
| } | |
| return $timeSpent; | |
| } | |
| /** | |
| * In the case of a new visit, we have to do the following actions: | |
| * | |
| * 1) Insert the new action | |
| * | |
| * 2) Insert the visit information | |
| * | |
| * @param bool $visitIsConverted | |
| */ | |
| protected function handleNewVisit($visitIsConverted) | |
| { | |
| Common::printDebug("New Visit (IP = " . IPUtils::binaryToStringIP($this->getVisitorIp()) . ")"); | |
| $this->setNewVisitorInformation(); | |
| $dimensions = $this->getAllVisitDimensions(); | |
| $this->triggerHookOnDimensions($dimensions, 'onNewVisit'); | |
| if ($visitIsConverted) { | |
| $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit'); | |
| } | |
| foreach ($this->requestProcessors as $processor) { | |
| $processor->onNewVisit($this->visitProperties, $this->request); | |
| } | |
| $this->printVisitorInformation(); | |
| $idVisit = $this->insertNewVisit($this->visitProperties->getProperties()); | |
| $this->visitProperties->setProperty('idvisit', $idVisit); | |
| $this->visitProperties->setProperty('visit_first_action_time', $this->request->getCurrentTimestamp()); | |
| $this->visitProperties->setProperty('visit_last_action_time', $this->request->getCurrentTimestamp()); | |
| } | |
| private function getModel() | |
| { | |
| return new Model(); | |
| } | |
| /** | |
| * Returns visitor cookie | |
| * | |
| * @return string binary | |
| */ | |
| protected function getVisitorIdcookie() | |
| { | |
| $isKnown = $this->request->getMetadata('CoreHome', 'isVisitorKnown'); | |
| if ($isKnown) { | |
| return $this->visitProperties->getProperty('idvisitor'); | |
| } | |
| // If the visitor had a first party ID cookie, then we use this value | |
| $idVisitor = $this->visitProperties->getProperty('idvisitor'); | |
| if ( | |
| !empty($idVisitor) | |
| && Tracker::LENGTH_BINARY_ID == strlen($this->visitProperties->getProperty('idvisitor')) | |
| ) { | |
| return $this->visitProperties->getProperty('idvisitor'); | |
| } | |
| return Common::hex2bin($this->generateUniqueVisitorId()); | |
| } | |
| /** | |
| * @return string returns random 16 chars hex string | |
| */ | |
| public static function generateUniqueVisitorId() | |
| { | |
| return substr(Common::generateUniqId(), 0, Tracker::LENGTH_HEX_ID_STRING); | |
| } | |
| /** | |
| * Returns the visitor's IP address | |
| * | |
| * @return string | |
| */ | |
| protected function getVisitorIp() | |
| { | |
| return $this->visitProperties->getProperty('location_ip'); | |
| } | |
| // is the host any of the registered URLs for this website? | |
| public static function isHostKnownAliasHost($urlHost, $idSite) | |
| { | |
| $websiteData = Cache::getCacheWebsiteAttributes($idSite); | |
| if (isset($websiteData['hosts'])) { | |
| $canonicalHosts = array(); | |
| foreach ($websiteData['hosts'] as $host) { | |
| $canonicalHosts[] = self::toCanonicalHost($host); | |
| } | |
| $canonicalHost = self::toCanonicalHost($urlHost); | |
| if (in_array($canonicalHost, $canonicalHosts)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private static function toCanonicalHost($host) | |
| { | |
| $hostLower = mb_strtolower($host); | |
| return str_replace('www.', '', $hostLower); | |
| } | |
| /** | |
| * @param $valuesToUpdate | |
| * @throws VisitorNotFoundInDb | |
| */ | |
| protected function updateExistingVisit($valuesToUpdate) | |
| { | |
| if (empty($valuesToUpdate)) { | |
| Common::printDebug('There are no values to be updated for this visit'); | |
| return; | |
| } | |
| $idSite = $this->request->getIdSite(); | |
| $idVisit = $this->visitProperties->getProperty('idvisit'); | |
| $wasInserted = $this->getModel()->updateVisit($idSite, $idVisit, $valuesToUpdate); | |
| if (isset($valuesToUpdate['idvisitor'])) { | |
| $this->updateIdVisitorAcrossLogTables($valuesToUpdate['idvisitor']); | |
| Common::printDebug('Updating idvisitor across tables for idvisit = ' . $idVisit); | |
| //For debug output below | |
| $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); | |
| } | |
| if ($wasInserted) { | |
| Common::printDebug('Updated existing visit: ' . var_export($valuesToUpdate, true)); | |
| } elseif (!$this->getModel()->hasVisit($idSite, $idVisit)) { | |
| // mostly for WordPress. see https://github.com/matomo-org/matomo/pull/15587 | |
| // as WP doesn't set `MYSQLI_CLIENT_FOUND_ROWS` and therefore when the update succeeded but no value changed | |
| // it would still return 0 vs OnPremise would return 1 or 2. | |
| throw new 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" | |
| ); | |
| } | |
| } | |
| protected function updateIdVisitorAcrossLogTables(string $idVisitor): void | |
| { | |
| $allLogTables = StaticContainer::get(LogTablesProvider::class)->getAllLogTables(); | |
| foreach ($allLogTables as $logTable) { | |
| if (!$logTable->hasIdVisitorColumn() || $logTable instanceof \Piwik\Plugins\CoreHome\Tracker\LogTable\Visit) { | |
| // skip all log tables that do not contain the idvisitor column, and `log_visit`, as it is already handled | |
| continue; | |
| } | |
| $conditions = ['idvisitor' => $this->previousVisitProperties->getProperty('idvisitor')]; | |
| if ($logTable->getIdColumn() !== 'idvisitor' && $logTable->getColumnToJoinOnIdVisit()) { | |
| $conditions[$logTable->getColumnToJoinOnIdVisit()] = $this->visitProperties->getProperty('idvisit'); | |
| } | |
| $this->getModel()->updateIdVisitorInLogTable($logTable->getName(), $idVisitor, $conditions); | |
| } | |
| } | |
| private function printVisitorInformation() | |
| { | |
| $debugVisitInfo = $this->visitProperties->getProperties(); | |
| $debugVisitInfo['idvisitor'] = isset($debugVisitInfo['idvisitor']) ? bin2hex($debugVisitInfo['idvisitor']) : ''; | |
| $debugVisitInfo['config_id'] = isset($debugVisitInfo['config_id']) ? bin2hex($debugVisitInfo['config_id']) : ''; | |
| $debugVisitInfo['location_ip'] = IPUtils::binaryToStringIP($debugVisitInfo['location_ip']); | |
| Common::printDebug($debugVisitInfo); | |
| } | |
| private function setNewVisitorInformation() | |
| { | |
| $idVisitor = $this->getVisitorIdcookie(); | |
| $visitorIp = $this->getVisitorIp(); | |
| $configId = $this->request->getMetadata('CoreHome', 'visitorId'); | |
| $this->visitProperties->clearProperties(); | |
| $this->visitProperties->setProperty('idvisitor', $idVisitor); | |
| $this->visitProperties->setProperty('config_id', $configId); | |
| $this->visitProperties->setProperty('location_ip', $visitorIp); | |
| } | |
| /** | |
| * Gather fields=>values that needs to be updated for the existing visit in log_visit | |
| * | |
| * @param $visitIsConverted | |
| * @return array | |
| */ | |
| private function getExistingVisitFieldsToUpdate($visitIsConverted) | |
| { | |
| $valuesToUpdate = array(); | |
| $valuesToUpdate = $this->setIdVisitorForExistingVisit($valuesToUpdate); | |
| $dimensions = $this->getAllVisitDimensions(); | |
| $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onExistingVisit', $valuesToUpdate); | |
| if ($visitIsConverted) { | |
| $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onConvertedVisit', $valuesToUpdate); | |
| } | |
| // Custom Variables overwrite previous values on each page view | |
| return $valuesToUpdate; | |
| } | |
| /** | |
| * @param VisitDimension[] $dimensions | |
| * @param string $hook | |
| * @param array|null $valuesToUpdate If null, $this->visitProperties will be updated | |
| * | |
| * @return array|null The updated $valuesToUpdate or null if no $valuesToUpdate given | |
| */ | |
| private function triggerHookOnDimensions($dimensions, $hook, $valuesToUpdate = null) | |
| { | |
| $visitor = $this->makeVisitorFacade(); | |
| /** @var Action $action */ | |
| $action = $this->request->getMetadata('Actions', 'action'); | |
| foreach ($dimensions as $dimension) { | |
| $value = $dimension->$hook($this->request, $visitor, $action); | |
| if ($value !== false) { | |
| $fieldName = $dimension->getColumnName(); | |
| $visitor->setVisitorColumn($fieldName, $value); | |
| if (is_float($value)) { | |
| $value = Common::forceDotAsSeparatorForDecimalPoint($value); | |
| } | |
| if ($valuesToUpdate !== null) { | |
| $valuesToUpdate[$fieldName] = $value; | |
| } else { | |
| $this->visitProperties->setProperty($fieldName, $value); | |
| } | |
| } | |
| } | |
| return $valuesToUpdate; | |
| } | |
| private function triggerPredicateHookOnDimensions($dimensions, $hook) | |
| { | |
| $visitor = $this->makeVisitorFacade(); | |
| /** @var Action $action */ | |
| $action = $this->request->getMetadata('Actions', 'action'); | |
| foreach ($dimensions as $dimension) { | |
| if ($dimension->$hook($this->request, $visitor, $action)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| protected function getAllVisitDimensions() | |
| { | |
| if (is_null(self::$dimensions)) { | |
| self::$dimensions = VisitDimension::getAllDimensions(); | |
| $dimensionNames = array(); | |
| foreach (self::$dimensions as $dimension) { | |
| $dimensionNames[] = $dimension->getColumnName(); | |
| } | |
| Common::printDebug("Following dimensions have been collected from plugins: " . implode( | |
| ", ", | |
| $dimensionNames | |
| )); | |
| } | |
| return self::$dimensions; | |
| } | |
| private function getVisitStandardLength() | |
| { | |
| return Config::getInstance()->Tracker['visit_standard_length']; | |
| } | |
| /** | |
| * @param array $valuesToUpdate | |
| * @return array | |
| */ | |
| private function setIdVisitorForExistingVisit($valuesToUpdate) | |
| { | |
| $idVisitor = $this->visitProperties->getProperty('idvisitor'); | |
| if (!empty($idVisitor) && Tracker::LENGTH_BINARY_ID == strlen($idVisitor)) { | |
| $valuesToUpdate['idvisitor'] = $idVisitor; | |
| } | |
| $visitorId = $this->request->getVisitorId(); | |
| if ($visitorId && strlen($visitorId) === Tracker::LENGTH_BINARY_ID) { | |
| // Might update the idvisitor when it was forced or overwritten for this visit | |
| $valuesToUpdate['idvisitor'] = $this->request->getVisitorId(); | |
| } | |
| if (TrackerConfig::getConfigValue('enable_userid_overwrites_visitorid', $this->request->getIdSiteIfExists())) { | |
| // User ID takes precedence and overwrites idvisitor value | |
| $userId = $this->request->getForcedUserId(); | |
| if ($userId) { | |
| $userIdHash = $this->request->getUserIdHashed($userId); | |
| $binIdVisitor = Common::hex2bin($userIdHash); | |
| $this->visitProperties->setProperty('idvisitor', $binIdVisitor); | |
| $valuesToUpdate['idvisitor'] = $binIdVisitor; | |
| } | |
| } | |
| return $valuesToUpdate; | |
| } | |
| protected function insertNewVisit($visit) | |
| { | |
| return $this->getModel()->createVisit($visit); | |
| } | |
| private function makeVisitorFacade() | |
| { | |
| return Visitor::makeFromVisitProperties($this->visitProperties, $this->request, $this->previousVisitProperties); | |
| } | |
| } | |