PDF rausgenommen
This commit is contained in:
582
msd2/tracking/piwik/core/Tracker/Visit.php
Normal file
582
msd2/tracking/piwik/core/Tracker/Visit.php
Normal file
@ -0,0 +1,582 @@
|
||||
<?php
|
||||
/**
|
||||
* Piwik - free/libre analytics platform
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\Tracker;
|
||||
|
||||
use Piwik\Archive\ArchiveInvalidator;
|
||||
use Piwik\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Container\StaticContainer;
|
||||
use Piwik\Date;
|
||||
use Piwik\Exception\UnexpectedWebsiteFoundException;
|
||||
use Piwik\Network\IPUtils;
|
||||
use Piwik\Plugin\Dimension\VisitDimension;
|
||||
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
|
||||
{
|
||||
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
|
||||
*/
|
||||
protected $visitProperties;
|
||||
|
||||
/**
|
||||
* @var ArchiveInvalidator
|
||||
*/
|
||||
private $invalidator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$requestProcessors = StaticContainer::get('Piwik\Plugin\RequestProcessors');
|
||||
$this->requestProcessors = $requestProcessors->getRequestProcessors();
|
||||
$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;
|
||||
}
|
||||
|
||||
private function checkSiteExists(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->request->getIdSite();
|
||||
} catch (UnexpectedWebsiteFoundException $e) {
|
||||
// we allow 0... the request will fail anyway as the site won't exist... allowing 0 will help us
|
||||
// reporting this tracking problem as it is a common issue. Otherwise we would not be able to report
|
||||
// this problem in tracking failures
|
||||
StaticContainer::get(Failures::class)->logFailure(Failures::FAILURE_ID_INVALID_SITE, $request);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->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->request->getVisitorIdForThirdPartyCookie());
|
||||
|
||||
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 = $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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user