requestProcessors = $requestProcessors->getRequestProcessors(); $this->visitorRecognizer = StaticContainer::get('Piwik\Tracker\VisitorRecognizer'); $this->visitProperties = null; $this->userSettings = StaticContainer::get('Piwik\Tracker\Settings'); $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator'); } /** * @param Request $request */ 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() { foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()..."); $processor->manipulateRequest($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'); // 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->visitProperties->getProperty('idvisitor')); foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::recordLogs()..."); $processor->recordLogs($this->visitProperties, $this->request); } $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished(); } /** * 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 Visitor $visitor * @param Action $action * @param $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) { $processor->onExistingVisit($valuesToUpdate, $this->visitProperties, $this->request); } $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 Visitor $visitor * @param Action $action * @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'); } /** * Gets the UserSettings object * * @return Settings */ protected function getSettingsObject() { return $this->userSettings; } // 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 = Common::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 = (int)$this->visitProperties->getProperty('idvisit'); $wasInserted = $this->getModel()->updateVisit($idSite, $idVisit, $valuesToUpdate); // Debug output if (isset($valuesToUpdate['idvisitor'])) { $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); } if ($wasInserted) { Common::printDebug('Updated existing visit: ' . var_export($valuesToUpdate, true)); } else { 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"); } } private function printVisitorInformation() { $debugVisitInfo = $this->visitProperties->getProperties(); $debugVisitInfo['idvisitor'] = bin2hex($debugVisitInfo['idvisitor']); $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 Visitor $visitor * @param Action|null $action * @param array|null $valuesToUpdate If null, $this->visitorInfo 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 $visitor * @param $valuesToUpdate * @return mixed */ private function setIdVisitorForExistingVisit($valuesToUpdate) { // Might update the idvisitor when it was forced or overwritten for this visit if (strlen($this->visitProperties->getProperty('idvisitor')) == Tracker::LENGTH_BINARY_ID) { $binIdVisitor = $this->visitProperties->getProperty('idvisitor'); $valuesToUpdate['idvisitor'] = $binIdVisitor; } // 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 markArchivedReportsAsInvalidIfArchiveAlreadyFinished() { $idSite = (int)$this->request->getIdSite(); $time = $this->request->getCurrentTimestamp(); $timezone = $this->getTimezoneForSite($idSite); if (!isset($timezone)) { return; } $date = Date::factory((int)$time, $timezone); if (!$date->isToday()) { // we don't have to handle in case date is in future as it is not allowed by tracker $this->invalidator->rememberToInvalidateArchivedReportsLater($idSite, $date); } } private function getTimezoneForSite($idSite) { try { $site = Cache::getCacheWebsiteAttributes($idSite); } catch (UnexpectedWebsiteFoundException $e) { return null; } if (!empty($site['timezone'])) { return $site['timezone']; } } private function makeVisitorFacade() { return Visitor::makeFromVisitProperties($this->visitProperties, $this->request); } }