PDF rausgenommen
This commit is contained in:
439
msd2/tracking/piwik/core/Tracker/Action.php
Normal file
439
msd2/tracking/piwik/core/Tracker/Action.php
Normal file
@ -0,0 +1,439 @@
|
||||
<?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 Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugin\Dimension\ActionDimension;
|
||||
use Piwik\Plugin\Manager;
|
||||
use Piwik\Tracker;
|
||||
|
||||
/**
|
||||
* An action
|
||||
*
|
||||
*/
|
||||
abstract class Action
|
||||
{
|
||||
const TYPE_PAGE_URL = 1;
|
||||
const TYPE_OUTLINK = 2;
|
||||
const TYPE_DOWNLOAD = 3;
|
||||
const TYPE_PAGE_TITLE = 4;
|
||||
const TYPE_ECOMMERCE_ITEM_SKU = 5;
|
||||
const TYPE_ECOMMERCE_ITEM_NAME = 6;
|
||||
const TYPE_ECOMMERCE_ITEM_CATEGORY = 7;
|
||||
const TYPE_SITE_SEARCH = 8;
|
||||
|
||||
const TYPE_EVENT = 10; // Alias TYPE_EVENT_CATEGORY
|
||||
const TYPE_EVENT_CATEGORY = 10;
|
||||
const TYPE_EVENT_ACTION = 11;
|
||||
const TYPE_EVENT_NAME = 12;
|
||||
|
||||
const TYPE_CONTENT = 13; // Alias TYPE_CONTENT_NAME
|
||||
const TYPE_CONTENT_NAME = 13;
|
||||
const TYPE_CONTENT_PIECE = 14;
|
||||
const TYPE_CONTENT_TARGET = 15;
|
||||
const TYPE_CONTENT_INTERACTION = 16;
|
||||
|
||||
const DB_COLUMN_CUSTOM_FLOAT = 'custom_float';
|
||||
|
||||
private static $factoryPriority = array(
|
||||
self::TYPE_PAGE_URL,
|
||||
self::TYPE_SITE_SEARCH,
|
||||
self::TYPE_CONTENT,
|
||||
self::TYPE_EVENT,
|
||||
self::TYPE_OUTLINK,
|
||||
self::TYPE_DOWNLOAD
|
||||
);
|
||||
|
||||
/**
|
||||
* Public so that events listener can access it
|
||||
*
|
||||
* @var Request
|
||||
*/
|
||||
public $request;
|
||||
|
||||
private $idLinkVisitAction;
|
||||
private $actionIdsCached = array();
|
||||
private $customFields = array();
|
||||
private $actionName;
|
||||
private $actionType;
|
||||
|
||||
/**
|
||||
* URL with excluded Query parameters
|
||||
*/
|
||||
private $actionUrl;
|
||||
|
||||
/**
|
||||
* Raw URL (will contain excluded URL query parameters)
|
||||
*/
|
||||
private $rawActionUrl;
|
||||
|
||||
/**
|
||||
* Makes the correct Action object based on the request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Action
|
||||
*/
|
||||
public static function factory(Request $request)
|
||||
{
|
||||
/** @var Action[] $actions */
|
||||
$actions = self::getAllActions($request);
|
||||
|
||||
foreach ($actions as $actionType) {
|
||||
if (empty($action)) {
|
||||
$action = $actionType;
|
||||
continue;
|
||||
}
|
||||
|
||||
$posPrevious = self::getPriority($action);
|
||||
$posCurrent = self::getPriority($actionType);
|
||||
|
||||
if ($posCurrent > $posPrevious) {
|
||||
$action = $actionType;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
return $action;
|
||||
}
|
||||
|
||||
return new ActionPageview($request);
|
||||
}
|
||||
|
||||
private static function getPriority(Action $actionType)
|
||||
{
|
||||
$key = array_search($actionType->getActionType(), self::$factoryPriority);
|
||||
|
||||
if (false === $key) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
public static function shouldHandle(Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function getAllActions(Request $request)
|
||||
{
|
||||
static $actions;
|
||||
|
||||
if (is_null($actions)) {
|
||||
$actions = Manager::getInstance()->findMultipleComponents('Actions', '\\Piwik\\Tracker\\Action');
|
||||
}
|
||||
|
||||
$instances = array();
|
||||
|
||||
foreach ($actions as $action) {
|
||||
/** @var \Piwik\Tracker\Action $action */
|
||||
if ($action::shouldHandle($request)) {
|
||||
$instances[] = new $action($request);
|
||||
}
|
||||
}
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
public function __construct($type, Request $request)
|
||||
{
|
||||
$this->actionType = $type;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL of the page currently being tracked, or the file being downloaded, or the outlink being clicked
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getActionUrl()
|
||||
{
|
||||
return $this->actionUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL of page being tracked, including all original Query parameters
|
||||
*/
|
||||
public function getActionUrlRaw()
|
||||
{
|
||||
return $this->rawActionUrl;
|
||||
}
|
||||
|
||||
public function getActionName()
|
||||
{
|
||||
return $this->actionName;
|
||||
}
|
||||
|
||||
public function getActionType()
|
||||
{
|
||||
return $this->actionType;
|
||||
}
|
||||
|
||||
public function getCustomVariables()
|
||||
{
|
||||
return $this->request->getCustomVariables($scope = 'page');
|
||||
}
|
||||
|
||||
// custom_float column
|
||||
public function getCustomFloatValue()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function setActionName($name)
|
||||
{
|
||||
$this->actionName = PageUrl::cleanupString((string)$name);
|
||||
}
|
||||
|
||||
protected function setActionUrl($url)
|
||||
{
|
||||
$this->rawActionUrl = PageUrl::getUrlIfLookValid($url);
|
||||
$url2 = PageUrl::excludeQueryParametersFromUrl($url, $this->request->getIdSite());
|
||||
|
||||
$this->actionUrl = PageUrl::getUrlIfLookValid($url2);
|
||||
|
||||
if ($url != $this->rawActionUrl) {
|
||||
Common::printDebug(' Before was "' . $this->rawActionUrl . '"');
|
||||
Common::printDebug(' After is "' . $url2 . '"');
|
||||
}
|
||||
}
|
||||
|
||||
protected function setActionUrlWithoutExcludingParameters($url)
|
||||
{
|
||||
$url = PageUrl::getUrlIfLookValid($url);
|
||||
$this->rawActionUrl = $url;
|
||||
$this->actionUrl = $url;
|
||||
}
|
||||
|
||||
abstract protected function getActionsToLookup();
|
||||
|
||||
protected function getUrlAndType()
|
||||
{
|
||||
$url = $this->getActionUrl();
|
||||
|
||||
if (!empty($url)) {
|
||||
// normalize urls by stripping protocol and www
|
||||
$url = PageUrl::normalizeUrl($url);
|
||||
return array($url['url'], self::TYPE_PAGE_URL, $url['prefixId']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setCustomField($field, $value)
|
||||
{
|
||||
$this->customFields[$field] = $value;
|
||||
}
|
||||
|
||||
public function getCustomField($field)
|
||||
{
|
||||
if (isset($this->customFields[$field])) {
|
||||
return $this->customFields[$field];
|
||||
}
|
||||
}
|
||||
|
||||
public function getCustomFields()
|
||||
{
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
public function getIdActionUrl()
|
||||
{
|
||||
$idUrl = isset($this->actionIdsCached['idaction_url']) ? $this->actionIdsCached['idaction_url'] : 0;
|
||||
// note; idaction_url = 0 is displayed as "Page URL Not Defined"
|
||||
return (int)$idUrl;
|
||||
}
|
||||
|
||||
public function getIdActionUrlForEntryAndExitIds()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getIdActionNameForEntryAndExitIds()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getIdActionName()
|
||||
{
|
||||
if (!isset($this->actionIdsCached['idaction_name'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->actionIdsCached['idaction_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the newly created record in the log_link_visit_action table
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIdLinkVisitAction()
|
||||
{
|
||||
return $this->idLinkVisitAction;
|
||||
}
|
||||
|
||||
public static function getTypeAsString($type)
|
||||
{
|
||||
$class = new \ReflectionClass("\\Piwik\\Tracker\\Action");
|
||||
$constants = $class->getConstants();
|
||||
|
||||
$typeId = array_search($type, $constants);
|
||||
|
||||
if (false === $typeId) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return str_replace('TYPE_', '', $typeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the idaction of the current action name and the current action url.
|
||||
* These idactions are used in the visitor logging table to link the visit information
|
||||
* (entry action, exit action) to the actions.
|
||||
* These idactions are also used in the table that links the visits and their actions.
|
||||
*
|
||||
* The methods takes care of creating a new record(s) in the action table if the existing
|
||||
* action name and action url doesn't exist yet.
|
||||
*/
|
||||
public function loadIdsFromLogActionTable()
|
||||
{
|
||||
if (!empty($this->actionIdsCached)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ActionDimension[] $dimensions */
|
||||
$dimensions = ActionDimension::getAllDimensions();
|
||||
$actions = $this->getActionsToLookup();
|
||||
|
||||
foreach ($dimensions as $dimension) {
|
||||
$value = $dimension->onLookupAction($this->request, $this);
|
||||
|
||||
if (false !== $value) {
|
||||
if (is_float($value)) {
|
||||
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
|
||||
}
|
||||
|
||||
$field = $dimension->getColumnName();
|
||||
|
||||
if (empty($field)) {
|
||||
$dimensionClass = get_class($dimension);
|
||||
throw new Exception('Dimension ' . $dimensionClass . ' does not define a field name');
|
||||
}
|
||||
|
||||
$actionId = $dimension->getActionId();
|
||||
$actions[$field] = array($value, $actionId);
|
||||
Common::printDebug("$field = $value");
|
||||
}
|
||||
}
|
||||
|
||||
$actions = array_filter($actions);
|
||||
|
||||
if (empty($actions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loadedActionIds = TableLogAction::loadIdsAction($actions);
|
||||
|
||||
$this->actionIdsCached = $loadedActionIds;
|
||||
return $this->actionIdsCached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records in the DB the association between the visit and this action.
|
||||
*
|
||||
* @param int $idReferrerActionUrl is the ID of the last action done by the current visit.
|
||||
* @param $idReferrerActionName
|
||||
* @param Visitor $visitor
|
||||
*/
|
||||
public function record(Visitor $visitor, $idReferrerActionUrl, $idReferrerActionName)
|
||||
{
|
||||
$this->loadIdsFromLogActionTable();
|
||||
|
||||
$visitAction = array(
|
||||
'idvisit' => $visitor->getVisitorColumn('idvisit'),
|
||||
'idsite' => $this->request->getIdSite(),
|
||||
'idvisitor' => $visitor->getVisitorColumn('idvisitor'),
|
||||
'idaction_url' => $this->getIdActionUrl(),
|
||||
'idaction_url_ref' => $idReferrerActionUrl,
|
||||
'idaction_name_ref' => $idReferrerActionName
|
||||
);
|
||||
|
||||
/** @var ActionDimension[] $dimensions */
|
||||
$dimensions = ActionDimension::getAllDimensions();
|
||||
|
||||
foreach ($dimensions as $dimension) {
|
||||
$value = $dimension->onNewAction($this->request, $visitor, $this);
|
||||
|
||||
if ($value !== false) {
|
||||
if (is_float($value)) {
|
||||
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
|
||||
}
|
||||
|
||||
$visitAction[$dimension->getColumnName()] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// idaction_name is NULLable. we only set it when applicable
|
||||
if ($this->isActionHasActionName()) {
|
||||
$visitAction['idaction_name'] = (int)$this->getIdActionName();
|
||||
}
|
||||
|
||||
foreach ($this->actionIdsCached as $field => $idAction) {
|
||||
$visitAction[$field] = ($idAction === false) ? 0 : $idAction;
|
||||
}
|
||||
|
||||
$customValue = $this->getCustomFloatValue();
|
||||
if ($customValue !== false && $customValue !== null && $customValue !== '') {
|
||||
$visitAction[self::DB_COLUMN_CUSTOM_FLOAT] = Common::forceDotAsSeparatorForDecimalPoint($customValue);
|
||||
}
|
||||
|
||||
$visitAction = array_merge($visitAction, $this->customFields);
|
||||
|
||||
$this->idLinkVisitAction = $this->getModel()->createAction($visitAction);
|
||||
|
||||
$visitAction['idlink_va'] = $this->idLinkVisitAction;
|
||||
|
||||
Common::printDebug("Inserted new action:");
|
||||
$visitActionDebug = $visitAction;
|
||||
$visitActionDebug['idvisitor'] = bin2hex($visitActionDebug['idvisitor']);
|
||||
Common::printDebug($visitActionDebug);
|
||||
}
|
||||
|
||||
public function writeDebugInfo()
|
||||
{
|
||||
$type = self::getTypeAsString($this->getActionType());
|
||||
$name = $this->getActionName();
|
||||
$url = $this->getActionUrl();
|
||||
|
||||
Common::printDebug("Action is a $type,
|
||||
Action name = " . $name . ",
|
||||
Action URL = " . $url);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getModel()
|
||||
{
|
||||
return new Model();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function isActionHasActionName()
|
||||
{
|
||||
$types = array(self::TYPE_PAGE_TITLE, self::TYPE_PAGE_URL, self::TYPE_SITE_SEARCH);
|
||||
|
||||
return in_array($this->getActionType(), $types);
|
||||
}
|
||||
}
|
104
msd2/tracking/piwik/core/Tracker/ActionPageview.php
Normal file
104
msd2/tracking/piwik/core/Tracker/ActionPageview.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?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\Config;
|
||||
|
||||
/**
|
||||
* This class represents a page view, tracking URL, page title and generation time.
|
||||
*
|
||||
*/
|
||||
class ActionPageview extends Action
|
||||
{
|
||||
protected $timeGeneration = false;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
parent::__construct(Action::TYPE_PAGE_URL, $request);
|
||||
|
||||
$url = $request->getParam('url');
|
||||
$this->setActionUrl($url);
|
||||
|
||||
$actionName = $request->getParam('action_name');
|
||||
$actionName = $this->cleanupActionName($actionName);
|
||||
$this->setActionName($actionName);
|
||||
|
||||
$this->timeGeneration = $this->request->getPageGenerationTime();
|
||||
}
|
||||
|
||||
protected function getActionsToLookup()
|
||||
{
|
||||
return array(
|
||||
'idaction_name' => array($this->getActionName(), Action::TYPE_PAGE_TITLE),
|
||||
'idaction_url' => $this->getUrlAndType()
|
||||
);
|
||||
}
|
||||
|
||||
public function getCustomFloatValue()
|
||||
{
|
||||
return $this->request->getPageGenerationTime();
|
||||
}
|
||||
|
||||
public static function shouldHandle(Request $request)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getIdActionUrlForEntryAndExitIds()
|
||||
{
|
||||
return $this->getIdActionUrl();
|
||||
}
|
||||
|
||||
public function getIdActionNameForEntryAndExitIds()
|
||||
{
|
||||
return $this->getIdActionName();
|
||||
}
|
||||
|
||||
private function cleanupActionName($actionName)
|
||||
{
|
||||
// get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067)
|
||||
$actionCategoryDelimiter = $this->getActionCategoryDelimiter();
|
||||
|
||||
if ($actionCategoryDelimiter === '') {
|
||||
return $actionName;
|
||||
}
|
||||
|
||||
// create an array of the categories delimited by the delimiter
|
||||
$split = explode($actionCategoryDelimiter, $actionName);
|
||||
$split = $this->trimEveryCategory($split);
|
||||
$split = $this->removeEmptyCategories($split);
|
||||
|
||||
return $this->rebuildNameOfCleanedCategories($actionCategoryDelimiter, $split);
|
||||
}
|
||||
|
||||
private function rebuildNameOfCleanedCategories($actionCategoryDelimiter, $split)
|
||||
{
|
||||
return implode($actionCategoryDelimiter, $split);
|
||||
}
|
||||
|
||||
private function removeEmptyCategories($split)
|
||||
{
|
||||
return array_filter($split, 'strlen');
|
||||
}
|
||||
|
||||
private function trimEveryCategory($split)
|
||||
{
|
||||
return array_map('trim', $split);
|
||||
}
|
||||
|
||||
private function getActionCategoryDelimiter()
|
||||
{
|
||||
if (isset(Config::getInstance()->General['action_category_delimiter'])) {
|
||||
return Config::getInstance()->General['action_category_delimiter'];
|
||||
}
|
||||
|
||||
return Config::getInstance()->General['action_title_category_delimiter'];
|
||||
}
|
||||
}
|
218
msd2/tracking/piwik/core/Tracker/Cache.php
Normal file
218
msd2/tracking/piwik/core/Tracker/Cache.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?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\Access;
|
||||
use Piwik\ArchiveProcessor\Rules;
|
||||
use Piwik\Cache as PiwikCache;
|
||||
use Piwik\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Option;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Tracker;
|
||||
|
||||
/**
|
||||
* Simple cache mechanism used in Tracker to avoid requesting settings from mysql on every request
|
||||
*
|
||||
*/
|
||||
class Cache
|
||||
{
|
||||
private static $cacheIdGeneral = 'general';
|
||||
|
||||
/**
|
||||
* Public for tests only
|
||||
* @var \Piwik\Cache\Lazy
|
||||
*/
|
||||
public static $cache;
|
||||
|
||||
/**
|
||||
* @return \Piwik\Cache\Lazy
|
||||
*/
|
||||
private static function getCache()
|
||||
{
|
||||
if (is_null(self::$cache)) {
|
||||
self::$cache = PiwikCache::getLazyCache();
|
||||
}
|
||||
|
||||
return self::$cache;
|
||||
}
|
||||
|
||||
private static function getTtl()
|
||||
{
|
||||
return Config::getInstance()->Tracker['tracker_cache_file_ttl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array containing data about the website: goals, URLs, etc.
|
||||
*
|
||||
* @param int $idSite
|
||||
* @return array
|
||||
*/
|
||||
public static function getCacheWebsiteAttributes($idSite)
|
||||
{
|
||||
if ('all' == $idSite) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$idSite = (int) $idSite;
|
||||
if ($idSite <= 0) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$cache = self::getCache();
|
||||
$cacheId = $idSite;
|
||||
$cacheContent = $cache->fetch($cacheId);
|
||||
|
||||
if (false !== $cacheContent) {
|
||||
return $cacheContent;
|
||||
}
|
||||
|
||||
Tracker::initCorePiwikInTrackerMode();
|
||||
|
||||
$content = array();
|
||||
Access::doAsSuperUser(function () use (&$content, $idSite) {
|
||||
/**
|
||||
* Triggered to get the attributes of a site entity that might be used by the
|
||||
* Tracker.
|
||||
*
|
||||
* Plugins add new site attributes for use in other tracking events must
|
||||
* use this event to put those attributes in the Tracker Cache.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* public function getSiteAttributes($content, $idSite)
|
||||
* {
|
||||
* $sql = "SELECT info FROM " . Common::prefixTable('myplugin_extra_site_info') . " WHERE idsite = ?";
|
||||
* $content['myplugin_site_data'] = Db::fetchOne($sql, array($idSite));
|
||||
* }
|
||||
*
|
||||
* @param array &$content Array mapping of site attribute names with values.
|
||||
* @param int $idSite The site ID to get attributes for.
|
||||
*/
|
||||
Piwik::postEvent('Tracker.Cache.getSiteAttributes', array(&$content, $idSite));
|
||||
Common::printDebug("Website $idSite tracker cache was re-created.");
|
||||
});
|
||||
|
||||
// if nothing is returned from the plugins, we don't save the content
|
||||
// this is not expected: all websites are expected to have at least one URL
|
||||
if (!empty($content)) {
|
||||
$cache->save($cacheId, $content, self::getTtl());
|
||||
}
|
||||
|
||||
Tracker::restoreTrackerPlugins();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear general (global) cache
|
||||
*/
|
||||
public static function clearCacheGeneral()
|
||||
{
|
||||
self::getCache()->delete(self::$cacheIdGeneral);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns contents of general (global) cache.
|
||||
* If the cache file tmp/cache/tracker/general.php does not exist yet, create it
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCacheGeneral()
|
||||
{
|
||||
$cache = self::getCache();
|
||||
$cacheContent = $cache->fetch(self::$cacheIdGeneral);
|
||||
|
||||
if (false !== $cacheContent) {
|
||||
return $cacheContent;
|
||||
}
|
||||
|
||||
Tracker::initCorePiwikInTrackerMode();
|
||||
$cacheContent = array(
|
||||
'isBrowserTriggerEnabled' => Rules::isBrowserTriggerEnabled(),
|
||||
'lastTrackerCronRun' => Option::get('lastTrackerCronRun'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Triggered before the [general tracker cache](/guides/all-about-tracking#the-tracker-cache)
|
||||
* is saved to disk. This event can be used to add extra content to the cache.
|
||||
*
|
||||
* Data that is used during tracking but is expensive to compute/query should be
|
||||
* cached to keep tracking efficient. One example of such data are options
|
||||
* that are stored in the option table. Querying data for each tracking
|
||||
* request means an extra unnecessary database query for each visitor action. Using
|
||||
* a cache solves this problem.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* public function setTrackerCacheGeneral(&$cacheContent)
|
||||
* {
|
||||
* $cacheContent['MyPlugin.myCacheKey'] = Option::get('MyPlugin_myOption');
|
||||
* }
|
||||
*
|
||||
* @param array &$cacheContent Array of cached data. Each piece of data must be
|
||||
* mapped by name.
|
||||
*/
|
||||
Piwik::postEvent('Tracker.setTrackerCacheGeneral', array(&$cacheContent));
|
||||
self::setCacheGeneral($cacheContent);
|
||||
Common::printDebug("General tracker cache was re-created.");
|
||||
|
||||
Tracker::restoreTrackerPlugins();
|
||||
|
||||
return $cacheContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store data in general (global cache)
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public static function setCacheGeneral($value)
|
||||
{
|
||||
$cache = self::getCache();
|
||||
|
||||
return $cache->save(self::$cacheIdGeneral, $value, self::getTtl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate Tracker cache files
|
||||
*
|
||||
* @param array|int $idSites Array of idSites to clear cache for
|
||||
*/
|
||||
public static function regenerateCacheWebsiteAttributes($idSites = array())
|
||||
{
|
||||
if (!is_array($idSites)) {
|
||||
$idSites = array($idSites);
|
||||
}
|
||||
|
||||
foreach ($idSites as $idSite) {
|
||||
self::deleteCacheWebsiteAttributes($idSite);
|
||||
self::getCacheWebsiteAttributes($idSite);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete existing Tracker cache
|
||||
*
|
||||
* @param string $idSite (website ID of the site to clear cache for
|
||||
*/
|
||||
public static function deleteCacheWebsiteAttributes($idSite)
|
||||
{
|
||||
self::getCache()->delete((int) $idSite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all Tracker cache files
|
||||
*/
|
||||
public static function deleteTrackerCache()
|
||||
{
|
||||
self::getCache()->flushAll();
|
||||
}
|
||||
}
|
293
msd2/tracking/piwik/core/Tracker/Db.php
Normal file
293
msd2/tracking/piwik/core/Tracker/Db.php
Normal file
@ -0,0 +1,293 @@
|
||||
<?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 Exception;
|
||||
use PDOStatement;
|
||||
use Piwik\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Timer;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\Tracker\Db\DbException;
|
||||
use Piwik\Tracker\Db\Mysqli;
|
||||
use Piwik\Tracker\Db\Pdo\Mysql;
|
||||
|
||||
/**
|
||||
* Simple database wrapper.
|
||||
* We can't afford to have a dependency with the Zend_Db module in Tracker.
|
||||
* We wrote this simple class
|
||||
*
|
||||
*/
|
||||
abstract class Db
|
||||
{
|
||||
protected static $profiling = false;
|
||||
|
||||
protected $queriesProfiling = array();
|
||||
|
||||
protected $connection = null;
|
||||
|
||||
/**
|
||||
* Enables the SQL profiling.
|
||||
* For each query, saves in the DB the time spent on this query.
|
||||
* Very useful to see the slow query under heavy load.
|
||||
* You can then use Piwik::displayDbTrackerProfile();
|
||||
* to display the SQLProfiling report and see which queries take time, etc.
|
||||
*/
|
||||
public static function enableProfiling()
|
||||
{
|
||||
self::$profiling = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the SQL profiling logging.
|
||||
*/
|
||||
public static function disableProfiling()
|
||||
{
|
||||
self::$profiling = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the SQL profiler is enabled
|
||||
* Only used by the unit test that tests that the profiler is off on a production server
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isProfilingEnabled()
|
||||
{
|
||||
return self::$profiling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize profiler
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
protected function initProfiler()
|
||||
{
|
||||
return new Timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record query profile
|
||||
*
|
||||
* @param string $query
|
||||
* @param Timer $timer
|
||||
*/
|
||||
protected function recordQueryProfile($query, $timer)
|
||||
{
|
||||
if (!isset($this->queriesProfiling[$query])) {
|
||||
$this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0);
|
||||
}
|
||||
|
||||
$time = $timer->getTimeMs(2);
|
||||
$time += $this->queriesProfiling[$query]['sum_time_ms'];
|
||||
$count = $this->queriesProfiling[$query]['count'] + 1;
|
||||
|
||||
$this->queriesProfiling[$query] = array('sum_time_ms' => $time, 'count' => $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* When destroyed, if SQL profiled enabled, logs the SQL profiling information
|
||||
*/
|
||||
public function recordProfiling()
|
||||
{
|
||||
if (is_null($this->connection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// turn off the profiler so we don't profile the following queries
|
||||
self::$profiling = false;
|
||||
|
||||
foreach ($this->queriesProfiling as $query => $info) {
|
||||
$time = $info['sum_time_ms'];
|
||||
$time = Common::forceDotAsSeparatorForDecimalPoint($time);
|
||||
$count = $info['count'];
|
||||
|
||||
$queryProfiling = "INSERT INTO " . Common::prefixTable('log_profiling') . "
|
||||
(query,count,sum_time_ms) VALUES (?,$count,$time)
|
||||
ON DUPLICATE KEY UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time";
|
||||
$this->query($queryProfiling, array($query));
|
||||
}
|
||||
|
||||
// turn back on profiling
|
||||
self::$profiling = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the DB
|
||||
*
|
||||
* @throws \Piwik\Tracker\Db\DbException if there was an error connecting the DB
|
||||
*/
|
||||
abstract public function connect();
|
||||
|
||||
/**
|
||||
* Disconnects from the server
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all the rows of a query result, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @see query()
|
||||
* @throws \Piwik\Tracker\Db\DbException if an exception occurred
|
||||
*/
|
||||
abstract public function fetchAll($query, $parameters = array());
|
||||
|
||||
/**
|
||||
* Returns the first row of a query result, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @see also query()
|
||||
*
|
||||
* @throws DbException if an exception occurred
|
||||
*/
|
||||
abstract public function fetch($query, $parameters = array());
|
||||
|
||||
/**
|
||||
* This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface
|
||||
*
|
||||
* @see fetch()
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return
|
||||
*/
|
||||
public function fetchRow($query, $parameters = array())
|
||||
{
|
||||
return $this->fetch($query, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface
|
||||
*
|
||||
* @see fetch()
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function fetchOne($query, $parameters = array())
|
||||
{
|
||||
$result = $this->fetch($query, $parameters);
|
||||
return is_array($result) && !empty($result) ? reset($result) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is a proxy to fetch(), used to maintain compatibility with Zend_Db + PDO interface
|
||||
*
|
||||
* @see fetch()
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return
|
||||
*/
|
||||
public function exec($query, $parameters = array())
|
||||
{
|
||||
return $this->fetch($query, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of affected rows in last query
|
||||
*
|
||||
* @param mixed $queryResult Result from query()
|
||||
* @return int
|
||||
*/
|
||||
abstract public function rowCount($queryResult);
|
||||
|
||||
/**
|
||||
* Executes a query, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind array('idsite'=> 1)
|
||||
*
|
||||
* @return PDOStatement or false if failed
|
||||
* @throws DbException if an exception occurred
|
||||
*/
|
||||
abstract public function query($query, $parameters = array());
|
||||
|
||||
/**
|
||||
* Returns the last inserted ID in the DB
|
||||
* Wrapper of PDO::lastInsertId()
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function lastInsertId();
|
||||
|
||||
/**
|
||||
* Test error number
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param string $errno
|
||||
* @return bool True if error number matches; false otherwise
|
||||
*/
|
||||
abstract public function isErrNo($e, $errno);
|
||||
|
||||
/**
|
||||
* Factory to create database objects
|
||||
*
|
||||
* @param array $configDb Database configuration
|
||||
* @throws Exception
|
||||
* @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql
|
||||
*/
|
||||
public static function factory($configDb)
|
||||
{
|
||||
/**
|
||||
* Triggered before a connection to the database is established by the Tracker.
|
||||
*
|
||||
* This event can be used to change the database connection settings used by the Tracker.
|
||||
*
|
||||
* @param array $dbInfos Reference to an array containing database connection info,
|
||||
* including:
|
||||
*
|
||||
* - **host**: The host name or IP address to the MySQL database.
|
||||
* - **username**: The username to use when connecting to the
|
||||
* database.
|
||||
* - **password**: The password to use when connecting to the
|
||||
* database.
|
||||
* - **dbname**: The name of the Piwik MySQL database.
|
||||
* - **port**: The MySQL database port to use.
|
||||
* - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'`
|
||||
* - **type**: The MySQL engine to use, for instance 'InnoDB'
|
||||
*/
|
||||
Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb));
|
||||
|
||||
switch ($configDb['adapter']) {
|
||||
case 'PDO\MYSQL':
|
||||
case 'PDO_MYSQL': // old format pre Piwik 2
|
||||
require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php';
|
||||
return new Mysql($configDb);
|
||||
|
||||
case 'MYSQLI':
|
||||
require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php';
|
||||
return new Mysqli($configDb);
|
||||
}
|
||||
|
||||
throw new Exception('Unsupported database adapter ' . $configDb['adapter']);
|
||||
}
|
||||
|
||||
public static function connectPiwikTrackerDb()
|
||||
{
|
||||
$db = null;
|
||||
$configDb = Config::getInstance()->database;
|
||||
|
||||
if (!isset($configDb['port'])) {
|
||||
// before 0.2.4 there is no port specified in config file
|
||||
$configDb['port'] = '3306';
|
||||
}
|
||||
|
||||
$db = self::factory($configDb);
|
||||
$db->connect();
|
||||
|
||||
return $db;
|
||||
}
|
||||
}
|
20
msd2/tracking/piwik/core/Tracker/Db/DbException.php
Normal file
20
msd2/tracking/piwik/core/Tracker/Db/DbException.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?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\Db;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Database Exception
|
||||
*
|
||||
*/
|
||||
class DbException extends Exception
|
||||
{
|
||||
}
|
388
msd2/tracking/piwik/core/Tracker/Db/Mysqli.php
Normal file
388
msd2/tracking/piwik/core/Tracker/Db/Mysqli.php
Normal file
@ -0,0 +1,388 @@
|
||||
<?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\Db;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Tracker\Db;
|
||||
|
||||
/**
|
||||
* mysqli wrapper
|
||||
*
|
||||
*/
|
||||
class Mysqli extends Db
|
||||
{
|
||||
protected $connection = null;
|
||||
protected $host;
|
||||
protected $port;
|
||||
protected $socket;
|
||||
protected $dbname;
|
||||
protected $username;
|
||||
protected $password;
|
||||
protected $charset;
|
||||
protected $activeTransaction = false;
|
||||
|
||||
protected $enable_ssl;
|
||||
protected $ssl_key;
|
||||
protected $ssl_cert;
|
||||
protected $ssl_ca;
|
||||
protected $ssl_ca_path;
|
||||
protected $ssl_cipher;
|
||||
protected $ssl_no_verify;
|
||||
|
||||
/**
|
||||
* Builds the DB object
|
||||
*
|
||||
* @param array $dbInfo
|
||||
* @param string $driverName
|
||||
*/
|
||||
public function __construct($dbInfo, $driverName = 'mysql')
|
||||
{
|
||||
if (isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') {
|
||||
$this->host = null;
|
||||
$this->port = null;
|
||||
$this->socket = $dbInfo['unix_socket'];
|
||||
} elseif ($dbInfo['port'][0] == '/') {
|
||||
$this->host = null;
|
||||
$this->port = null;
|
||||
$this->socket = $dbInfo['port'];
|
||||
} else {
|
||||
$this->host = $dbInfo['host'];
|
||||
$this->port = (int)$dbInfo['port'];
|
||||
$this->socket = null;
|
||||
}
|
||||
$this->dbname = $dbInfo['dbname'];
|
||||
$this->username = $dbInfo['username'];
|
||||
$this->password = $dbInfo['password'];
|
||||
$this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null;
|
||||
|
||||
|
||||
if(!empty($dbInfo['enable_ssl'])){
|
||||
$this->enable_ssl = $dbInfo['enable_ssl'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_key'])){
|
||||
$this->ssl_key = $dbInfo['ssl_key'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_cert'])){
|
||||
$this->ssl_cert = $dbInfo['ssl_cert'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_ca'])){
|
||||
$this->ssl_ca = $dbInfo['ssl_ca'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_ca_path'])){
|
||||
$this->ssl_ca_path = $dbInfo['ssl_ca_path'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_cipher'])){
|
||||
$this->ssl_cipher = $dbInfo['ssl_cipher'];
|
||||
}
|
||||
if(!empty($dbInfo['ssl_no_verify'])){
|
||||
$this->ssl_no_verify = $dbInfo['ssl_no_verify'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the DB
|
||||
*
|
||||
* @throws Exception|DbException if there was an error connecting the DB
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
$this->connection = mysqli_init();
|
||||
|
||||
|
||||
if($this->enable_ssl){
|
||||
mysqli_ssl_set($this->connection, $this->ssl_key, $this->ssl_cert, $this->ssl_ca, $this->ssl_ca_path, $this->ssl_cipher);
|
||||
}
|
||||
|
||||
// Make sure MySQL returns all matched rows on update queries including
|
||||
// rows that actually didn't have to be updated because the values didn't
|
||||
// change. This matches common behaviour among other database systems.
|
||||
// See #6296 why this is important in tracker
|
||||
$flags = MYSQLI_CLIENT_FOUND_ROWS;
|
||||
if ($this->enable_ssl){
|
||||
$flags = $flags | MYSQLI_CLIENT_SSL;
|
||||
}
|
||||
if ($this->ssl_no_verify && defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT')){
|
||||
$flags = $flags | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
|
||||
}
|
||||
mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket, $flags);
|
||||
if (!$this->connection || mysqli_connect_errno()) {
|
||||
throw new DbException("Connect failed: " . mysqli_connect_error());
|
||||
}
|
||||
|
||||
if ($this->charset && !mysqli_set_charset($this->connection, $this->charset)) {
|
||||
throw new DbException("Set Charset failed: " . mysqli_error($this->connection));
|
||||
}
|
||||
|
||||
$this->password = '';
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile('connect', $timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the server
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
mysqli_close($this->connection);
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all the rows of a query result, using optional bound parameters.
|
||||
*
|
||||
* @see query()
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return array
|
||||
* @throws Exception|DbException if an exception occurred
|
||||
*/
|
||||
public function fetchAll($query, $parameters = array())
|
||||
{
|
||||
try {
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
$rows = array();
|
||||
$query = $this->prepare($query, $parameters);
|
||||
$rs = mysqli_query($this->connection, $query);
|
||||
if (is_bool($rs)) {
|
||||
throw new DbException('fetchAll() failed: ' . mysqli_error($this->connection) . ' : ' . $query);
|
||||
}
|
||||
|
||||
while ($row = mysqli_fetch_array($rs, MYSQLI_ASSOC)) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
mysqli_free_result($rs);
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile($query, $timer);
|
||||
}
|
||||
return $rows;
|
||||
} catch (Exception $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first row of a query result, using optional bound parameters.
|
||||
*
|
||||
* @see query()
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws DbException if an exception occurred
|
||||
*/
|
||||
public function fetch($query, $parameters = array())
|
||||
{
|
||||
try {
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
$query = $this->prepare($query, $parameters);
|
||||
$rs = mysqli_query($this->connection, $query);
|
||||
if (is_bool($rs)) {
|
||||
throw new DbException('fetch() failed: ' . mysqli_error($this->connection) . ' : ' . $query);
|
||||
}
|
||||
|
||||
$row = mysqli_fetch_array($rs, MYSQLI_ASSOC);
|
||||
mysqli_free_result($rs);
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile($query, $timer);
|
||||
}
|
||||
return $row;
|
||||
} catch (Exception $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array|string $parameters Parameters to bind array('idsite'=> 1)
|
||||
*
|
||||
* @return bool|resource false if failed
|
||||
* @throws DbException if an exception occurred
|
||||
*/
|
||||
public function query($query, $parameters = array())
|
||||
{
|
||||
if (is_null($this->connection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
$query = $this->prepare($query, $parameters);
|
||||
$result = mysqli_query($this->connection, $query);
|
||||
if (!is_bool($result)) {
|
||||
mysqli_free_result($result);
|
||||
}
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile($query, $timer);
|
||||
}
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage() . "
|
||||
In query: $query
|
||||
Parameters: " . var_export($parameters, true), $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last inserted ID in the DB
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return mysqli_insert_id($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input is a prepared SQL statement and parameters
|
||||
* Returns the SQL statement
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $parameters
|
||||
* @return string
|
||||
*/
|
||||
private function prepare($query, $parameters)
|
||||
{
|
||||
if (!$parameters) {
|
||||
$parameters = array();
|
||||
} elseif (!is_array($parameters)) {
|
||||
$parameters = array($parameters);
|
||||
}
|
||||
|
||||
$this->paramNb = 0;
|
||||
$this->params = & $parameters;
|
||||
$query = preg_replace_callback('/\?/', array($this, 'replaceParam'), $query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function replaceParam($match)
|
||||
{
|
||||
$param = & $this->params[$this->paramNb];
|
||||
$this->paramNb++;
|
||||
|
||||
if ($param === null) {
|
||||
return 'NULL';
|
||||
} else {
|
||||
return "'" . addslashes($param) . "'";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error number
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param string $errno
|
||||
* @return bool
|
||||
*/
|
||||
public function isErrNo($e, $errno)
|
||||
{
|
||||
return mysqli_errno($this->connection) == $errno;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of affected rows in last query
|
||||
*
|
||||
* @param mixed $queryResult Result from query()
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount($queryResult)
|
||||
{
|
||||
return mysqli_affected_rows($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Transaction
|
||||
* @return string TransactionID
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
if (!$this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->connection->autocommit(false)) {
|
||||
$this->activeTransaction = uniqid();
|
||||
return $this->activeTransaction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
* @param $xid
|
||||
* @throws DbException
|
||||
* @internal param TransactionID $string from beginTransaction
|
||||
*/
|
||||
public function commit($xid)
|
||||
{
|
||||
if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->activeTransaction = false;
|
||||
|
||||
if (!$this->connection->commit()) {
|
||||
throw new DbException("Commit failed");
|
||||
}
|
||||
|
||||
$this->connection->autocommit(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
* @param $xid
|
||||
* @throws DbException
|
||||
* @internal param TransactionID $string from beginTransaction
|
||||
*/
|
||||
public function rollBack($xid)
|
||||
{
|
||||
if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->activeTransaction = false;
|
||||
|
||||
if (!$this->connection->rollback()) {
|
||||
throw new DbException("Rollback failed");
|
||||
}
|
||||
|
||||
$this->connection->autocommit(true);
|
||||
}
|
||||
}
|
333
msd2/tracking/piwik/core/Tracker/Db/Pdo/Mysql.php
Normal file
333
msd2/tracking/piwik/core/Tracker/Db/Pdo/Mysql.php
Normal file
@ -0,0 +1,333 @@
|
||||
<?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\Db\Pdo;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
use Piwik\Tracker\Db;
|
||||
use Piwik\Tracker\Db\DbException;
|
||||
|
||||
/**
|
||||
* PDO MySQL wrapper
|
||||
*
|
||||
*/
|
||||
class Mysql extends Db
|
||||
{
|
||||
/**
|
||||
* @var PDO
|
||||
*/
|
||||
protected $connection = null;
|
||||
protected $dsn;
|
||||
protected $username;
|
||||
protected $password;
|
||||
protected $charset;
|
||||
|
||||
protected $mysqlOptions = array();
|
||||
|
||||
|
||||
protected $activeTransaction = false;
|
||||
|
||||
/**
|
||||
* Builds the DB object
|
||||
*
|
||||
* @param array $dbInfo
|
||||
* @param string $driverName
|
||||
*/
|
||||
public function __construct($dbInfo, $driverName = 'mysql')
|
||||
{
|
||||
if (isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') {
|
||||
$this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket'];
|
||||
} elseif (!empty($dbInfo['port']) && $dbInfo['port'][0] == '/') {
|
||||
$this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['port'];
|
||||
} else {
|
||||
$this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';host=' . $dbInfo['host'] . ';port=' . $dbInfo['port'];
|
||||
}
|
||||
|
||||
$this->username = $dbInfo['username'];
|
||||
$this->password = $dbInfo['password'];
|
||||
|
||||
if (isset($dbInfo['charset'])) {
|
||||
$this->charset = $dbInfo['charset'];
|
||||
$this->dsn .= ';charset=' . $this->charset;
|
||||
}
|
||||
|
||||
|
||||
if (isset($dbInfo['enable_ssl']) && $dbInfo['enable_ssl']) {
|
||||
|
||||
if (!empty($dbInfo['ssl_key'])) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_KEY] = $dbInfo['ssl_key'];
|
||||
}
|
||||
if (!empty($dbInfo['ssl_cert'])) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CERT] = $dbInfo['ssl_cert'];
|
||||
}
|
||||
if (!empty($dbInfo['ssl_ca'])) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CA] = $dbInfo['ssl_ca'];
|
||||
}
|
||||
if (!empty($dbInfo['ssl_ca_path'])) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $dbInfo['ssl_ca_path'];
|
||||
}
|
||||
if (!empty($dbInfo['ssl_cipher'])) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $dbInfo['ssl_cipher'];
|
||||
}
|
||||
if (!empty($dbInfo['ssl_no_verify']) && defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the DB
|
||||
*
|
||||
* @throws Exception if there was an error connecting the DB
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
// Make sure MySQL returns all matched rows on update queries including
|
||||
// rows that actually didn't have to be updated because the values didn't
|
||||
// change. This matches common behaviour among other database systems.
|
||||
// See #6296 why this is important in tracker
|
||||
$this->mysqlOptions[PDO::MYSQL_ATTR_FOUND_ROWS] = true;
|
||||
$this->mysqlOptions[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
|
||||
|
||||
|
||||
|
||||
$this->connection = @new PDO($this->dsn, $this->username, $this->password, $this->mysqlOptions);
|
||||
|
||||
// we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked
|
||||
// the matomo.php would stay waiting for the database... bad!
|
||||
// we delete the password from this object "just in case" it could be printed
|
||||
$this->password = '';
|
||||
|
||||
/*
|
||||
* Lazy initialization via MYSQL_ATTR_INIT_COMMAND depends
|
||||
* on mysqlnd support, PHP version, and OS.
|
||||
* see ZF-7428 and http://bugs.php.net/bug.php?id=47224
|
||||
*/
|
||||
if (!empty($this->charset)) {
|
||||
$sql = "SET NAMES '" . $this->charset . "'";
|
||||
$this->connection->exec($sql);
|
||||
}
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile('connect', $timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the server
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all the rows of a query result, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return array|bool
|
||||
* @see query()
|
||||
* @throws Exception|DbException if an exception occurred
|
||||
*/
|
||||
public function fetchAll($query, $parameters = array())
|
||||
{
|
||||
try {
|
||||
$sth = $this->query($query, $parameters);
|
||||
if ($sth === false) {
|
||||
return false;
|
||||
}
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the first column of all SQL result rows as an array.
|
||||
*
|
||||
* @param string $sql An SQL SELECT statement.
|
||||
* @param mixed $bind Data to bind into SELECT placeholders.
|
||||
* @throws \Piwik\Tracker\Db\DbException
|
||||
* @return string
|
||||
*/
|
||||
public function fetchCol($sql, $bind = array())
|
||||
{
|
||||
try {
|
||||
$sth = $this->query($sql, $bind);
|
||||
if ($sth === false) {
|
||||
return false;
|
||||
}
|
||||
$result = $sth->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
return $result;
|
||||
} catch (PDOException $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first row of a query result, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array $parameters Parameters to bind
|
||||
* @return bool|mixed
|
||||
* @see query()
|
||||
* @throws Exception|DbException if an exception occurred
|
||||
*/
|
||||
public function fetch($query, $parameters = array())
|
||||
{
|
||||
try {
|
||||
$sth = $this->query($query, $parameters);
|
||||
if ($sth === false) {
|
||||
return false;
|
||||
}
|
||||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
throw new DbException("Error query: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query, using optional bound parameters.
|
||||
*
|
||||
* @param string $query Query
|
||||
* @param array|string $parameters Parameters to bind array('idsite'=> 1)
|
||||
* @return PDOStatement|bool PDOStatement or false if failed
|
||||
* @throws DbException if an exception occurred
|
||||
*/
|
||||
public function query($query, $parameters = array())
|
||||
{
|
||||
if (is_null($this->connection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
if (!is_array($parameters)) {
|
||||
$parameters = array($parameters);
|
||||
}
|
||||
$sth = $this->connection->prepare($query);
|
||||
$sth->execute($parameters);
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile($query, $timer);
|
||||
}
|
||||
return $sth;
|
||||
} catch (PDOException $e) {
|
||||
$message = $e->getMessage() . " In query: $query Parameters: " . var_export($parameters, true);
|
||||
throw new DbException("Error query: " . $message, (int) $e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last inserted ID in the DB
|
||||
* Wrapper of PDO::lastInsertId()
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return $this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error number
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param string $errno
|
||||
* @return bool
|
||||
*/
|
||||
public function isErrNo($e, $errno)
|
||||
{
|
||||
if (preg_match('/([0-9]{4})/', $e->getMessage(), $match)) {
|
||||
return $match[1] == $errno;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of affected rows in last query
|
||||
*
|
||||
* @param mixed $queryResult Result from query()
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount($queryResult)
|
||||
{
|
||||
return $queryResult->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Transaction
|
||||
* @return string TransactionID
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
if (!$this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->connection->beginTransaction()) {
|
||||
$this->activeTransaction = uniqid();
|
||||
return $this->activeTransaction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
* @param $xid
|
||||
* @throws DbException
|
||||
* @internal param TransactionID $string from beginTransaction
|
||||
*/
|
||||
public function commit($xid)
|
||||
{
|
||||
if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->activeTransaction = false;
|
||||
|
||||
if (!$this->connection->commit()) {
|
||||
throw new DbException("Commit failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
* @param $xid
|
||||
* @throws DbException
|
||||
* @internal param TransactionID $string from beginTransaction
|
||||
*/
|
||||
public function rollBack($xid)
|
||||
{
|
||||
if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->activeTransaction = false;
|
||||
|
||||
if (!$this->connection->rollBack()) {
|
||||
throw new DbException("Rollback failed");
|
||||
}
|
||||
}
|
||||
}
|
116
msd2/tracking/piwik/core/Tracker/Db/Pdo/Pgsql.php
Normal file
116
msd2/tracking/piwik/core/Tracker/Db/Pdo/Pgsql.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?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\Db\Pdo;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* PDO PostgreSQL wrapper
|
||||
*
|
||||
*/
|
||||
class Pgsql extends Mysql
|
||||
{
|
||||
/**
|
||||
* Builds the DB object
|
||||
*
|
||||
* @param array $dbInfo
|
||||
* @param string $driverName
|
||||
*/
|
||||
public function __construct($dbInfo, $driverName = 'pgsql')
|
||||
{
|
||||
parent::__construct($dbInfo, $driverName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the DB
|
||||
*
|
||||
* @throws Exception if there was an error connecting the DB
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if (self::$profiling) {
|
||||
$timer = $this->initProfiler();
|
||||
}
|
||||
|
||||
$this->connection = new PDO($this->dsn, $this->username, $this->password, $config = array());
|
||||
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
// we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked
|
||||
// the matomo.php would stay waiting for the database... bad!
|
||||
// we delete the password from this object "just in case" it could be printed
|
||||
$this->password = '';
|
||||
|
||||
if (!empty($this->charset)) {
|
||||
$sql = "SET NAMES '" . $this->charset . "'";
|
||||
$this->connection->exec($sql);
|
||||
}
|
||||
|
||||
if (self::$profiling && isset($timer)) {
|
||||
$this->recordQueryProfile('connect', $timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error number
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param string $errno
|
||||
* @return bool
|
||||
*/
|
||||
public function isErrNo($e, $errno)
|
||||
{
|
||||
// map MySQL driver-specific error codes to PostgreSQL SQLSTATE
|
||||
$map = array(
|
||||
// MySQL: Unknown database '%s'
|
||||
// PostgreSQL: database "%s" does not exist
|
||||
'1049' => '08006',
|
||||
|
||||
// MySQL: Table '%s' already exists
|
||||
// PostgreSQL: relation "%s" already exists
|
||||
'1050' => '42P07',
|
||||
|
||||
// MySQL: Unknown column '%s' in '%s'
|
||||
// PostgreSQL: column "%s" does not exist
|
||||
'1054' => '42703',
|
||||
|
||||
// MySQL: Duplicate column name '%s'
|
||||
// PostgreSQL: column "%s" of relation "%s" already exists
|
||||
'1060' => '42701',
|
||||
|
||||
// MySQL: Duplicate entry '%s' for key '%s'
|
||||
// PostgreSQL: duplicate key violates unique constraint
|
||||
'1062' => '23505',
|
||||
|
||||
// MySQL: Can't DROP '%s'; check that column/key exists
|
||||
// PostgreSQL: index "%s" does not exist
|
||||
'1091' => '42704',
|
||||
|
||||
// MySQL: Table '%s.%s' doesn't exist
|
||||
// PostgreSQL: relation "%s" does not exist
|
||||
'1146' => '42P01',
|
||||
);
|
||||
|
||||
if (preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) {
|
||||
return $match[1] == $map[$errno];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of affected rows in last query
|
||||
*
|
||||
* @param mixed $queryResult Result from query()
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount($queryResult)
|
||||
{
|
||||
return $queryResult->rowCount();
|
||||
}
|
||||
}
|
197
msd2/tracking/piwik/core/Tracker/Failures.php
Normal file
197
msd2/tracking/piwik/core/Tracker/Failures.php
Normal file
@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/**
|
||||
* Matomo - free/libre analytics platform
|
||||
*
|
||||
* @link https://matomo.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\Tracker;
|
||||
|
||||
use Piwik\Common;
|
||||
use Piwik\Date;
|
||||
use Piwik\Exception\InvalidRequestParameterException;
|
||||
use Piwik\Exception\UnexpectedWebsiteFoundException;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Site;
|
||||
use Piwik\Db as PiwikDb;
|
||||
|
||||
class Failures
|
||||
{
|
||||
const CLEANUP_OLD_FAILURES_DAYS = 2;
|
||||
const FAILURE_ID_INVALID_SITE = 1;
|
||||
const FAILURE_ID_NOT_AUTHENTICATED = 2;
|
||||
|
||||
private $table = 'tracking_failure';
|
||||
private $tablePrefixed;
|
||||
private $now;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->tablePrefixed = Common::prefixTable($this->table);
|
||||
}
|
||||
|
||||
public function setNow(Date $now)
|
||||
{
|
||||
$this->now = $now;
|
||||
}
|
||||
|
||||
private function getNow()
|
||||
{
|
||||
if (isset($this->now)) {
|
||||
return $this->now;
|
||||
}
|
||||
return Date::now();
|
||||
}
|
||||
|
||||
public function logFailure($idFailure, Request $request)
|
||||
{
|
||||
$isVisitExcluded = $request->getMetadata('CoreHome', 'isVisitExcluded');
|
||||
|
||||
if ($isVisitExcluded === null) {
|
||||
try {
|
||||
$visitExcluded = new VisitExcluded($request);
|
||||
$isVisitExcluded = $visitExcluded->isExcluded();
|
||||
} catch (InvalidRequestParameterException $e) {
|
||||
// we ignore this error and assume visit is not excluded... happens eg when using `cip` and request was
|
||||
// not authenticated...
|
||||
$isVisitExcluded = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isVisitExcluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$idSite = (int) $request->getIdSiteUnverified();
|
||||
$idFailure = (int) $idFailure;
|
||||
|
||||
if ($idSite > 9999999 || $idSite < 0 || $this->hasLoggedFailure($idSite, $idFailure)) {
|
||||
return; // we prevent creating huge amount of entries in the cache
|
||||
}
|
||||
|
||||
$params = $this->getParamsWithTokenAnonymized($request);
|
||||
$sql = sprintf('INSERT INTO %s (`idsite`, `idfailure`, `date_first_occurred`, `request_url`) VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE idsite=idsite;', $this->tablePrefixed);
|
||||
|
||||
PiwikDb::get()->query($sql, array($idSite, $idFailure, $this->getNow()->getDatetime(), http_build_query($params)));
|
||||
}
|
||||
|
||||
private function hasLoggedFailure($idSite, $idFailure)
|
||||
{
|
||||
$sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? and idfailure = ?', $this->tablePrefixed);
|
||||
$row = PiwikDb::fetchRow($sql, array($idSite, $idFailure));
|
||||
|
||||
return !empty($row);
|
||||
}
|
||||
|
||||
private function getParamsWithTokenAnonymized(Request $request)
|
||||
{
|
||||
// eg if there is a typo in the token auth we want to replace it as well to not accidentally leak a token
|
||||
// eg imagine a super user tries to issue an API request for a site and sending the wrong parameter for a token...
|
||||
// an admin may have view access for this and can see the super users token
|
||||
$token = $request->getTokenAuth();
|
||||
$params = $request->getRawParams();
|
||||
foreach (array('token_auth', 'token', 'tokenauth', 'token__auth') as $key) {
|
||||
if (isset($params[$key])) {
|
||||
$params[$key] = '__TOKEN_AUTH__';
|
||||
}
|
||||
}
|
||||
foreach ($params as $key => $value) {
|
||||
if (!empty($token) && $value === $token) {
|
||||
$params[$key] = '__TOKEN_AUTH__'; // user accidentally posted the token in a wrong field
|
||||
} elseif (!empty($value) && is_string($value)
|
||||
&& Common::mb_strlen($value) >= 29 && Common::mb_strlen($value) <= 36
|
||||
&& ctype_xdigit($value)) {
|
||||
$params[$key] = '__TOKEN_AUTH__'; // user maybe posted a token in a different field... it looks like it might be a token
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function removeFailuresOlderThanDays($days)
|
||||
{
|
||||
$minutesAgo = $this->getNow()->subDay($days)->getDatetime();
|
||||
|
||||
PiwikDb::query(sprintf('DELETE FROM %s WHERE date_first_occurred < ?', $this->tablePrefixed), array($minutesAgo));
|
||||
}
|
||||
|
||||
public function getAllFailures()
|
||||
{
|
||||
$failures = PiwikDb::fetchAll(sprintf('SELECT * FROM %s', $this->tablePrefixed));
|
||||
return $this->enrichFailures($failures);
|
||||
}
|
||||
|
||||
public function getFailuresForSites($idSites)
|
||||
{
|
||||
if (empty($idSites)) {
|
||||
return array();
|
||||
}
|
||||
$idSites = array_map('intval', $idSites);
|
||||
$idSites = implode(',', $idSites);
|
||||
$failures = PiwikDb::fetchAll(sprintf('SELECT * FROM %s WHERE idsite IN (%s)', $this->tablePrefixed, $idSites));
|
||||
return $this->enrichFailures($failures);
|
||||
}
|
||||
|
||||
public function deleteTrackingFailure($idSite, $idFailure)
|
||||
{
|
||||
PiwikDb::query(sprintf('DELETE FROM %s WHERE idsite = ? and idfailure = ?', $this->tablePrefixed), array($idSite, $idFailure));
|
||||
}
|
||||
|
||||
public function deleteTrackingFailures($idSites)
|
||||
{
|
||||
if (!empty($idSites)) {
|
||||
$idSites = array_map('intval', $idSites);
|
||||
$idSites = implode(',', $idSites);
|
||||
PiwikDb::query(sprintf('DELETE FROM %s WHERE idsite IN(%s)', $this->tablePrefixed, $idSites));
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAllTrackingFailures()
|
||||
{
|
||||
PiwikDb::query(sprintf('DELETE FROM %s', $this->tablePrefixed));
|
||||
}
|
||||
|
||||
private function enrichFailures($failures)
|
||||
{
|
||||
foreach ($failures as &$failure) {
|
||||
try {
|
||||
$failure['site_name'] = Site::getNameFor($failure['idsite']);
|
||||
} catch (UnexpectedWebsiteFoundException $e) {
|
||||
$failure['site_name'] = Piwik::translate('General_Unknown');
|
||||
}
|
||||
$failure['pretty_date_first_occurred'] = Date::factory($failure['date_first_occurred'])->getLocalized(Date::DATETIME_FORMAT_SHORT);
|
||||
parse_str($failure['request_url'], $params);
|
||||
if (empty($params['url'])) {
|
||||
$params['url'] = ' ';// workaround it using the default provider in request constructor
|
||||
}
|
||||
$request = new Request($params);
|
||||
$failure['url'] = trim($request->getParam('url'));
|
||||
$failure['problem'] = '';
|
||||
$failure['solution'] = '';
|
||||
$failure['solution_url'] = '';
|
||||
|
||||
switch ($failure['idfailure']) {
|
||||
case self::FAILURE_ID_INVALID_SITE:
|
||||
$failure['problem'] = Piwik::translate('CoreAdminHome_TrackingFailureInvalidSiteProblem');
|
||||
$failure['solution'] = Piwik::translate('CoreAdminHome_TrackingFailureInvalidSiteSolution');
|
||||
$failure['solution_url'] = 'https://matomo.org/faq/how-to/faq_30838/';
|
||||
break;
|
||||
case self::FAILURE_ID_NOT_AUTHENTICATED:
|
||||
$failure['problem'] = Piwik::translate('CoreAdminHome_TrackingFailureAuthenticationProblem');
|
||||
$failure['solution'] = Piwik::translate('CoreAdminHome_TrackingFailureAuthenticationSolution');
|
||||
$failure['solution_url'] = 'https://matomo.org/faq/how-to/faq_30835/';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* internal use only
|
||||
*/
|
||||
Piwik::postEvent('Tracking.makeFailuresHumanReadable', array(&$failures));
|
||||
|
||||
return $failures;
|
||||
}
|
||||
}
|
914
msd2/tracking/piwik/core/Tracker/GoalManager.php
Normal file
914
msd2/tracking/piwik/core/Tracker/GoalManager.php
Normal file
@ -0,0 +1,914 @@
|
||||
<?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 Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Container\StaticContainer;
|
||||
use Piwik\Date;
|
||||
use Piwik\Exception\InvalidRequestParameterException;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugin\Dimension\ConversionDimension;
|
||||
use Piwik\Plugin\Dimension\VisitDimension;
|
||||
use Piwik\Plugins\CustomVariables\CustomVariables;
|
||||
use Piwik\Plugins\Events\Actions\ActionEvent;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\Tracker\Visit\VisitProperties;
|
||||
|
||||
/**
|
||||
*/
|
||||
class GoalManager
|
||||
{
|
||||
// log_visit.visit_goal_buyer
|
||||
const TYPE_BUYER_OPEN_CART = 2;
|
||||
const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3;
|
||||
|
||||
// log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts
|
||||
const ITEM_IDORDER_ABANDONED_CART = 0;
|
||||
|
||||
// log_conversion.idgoal special values
|
||||
const IDGOAL_CART = -1;
|
||||
const IDGOAL_ORDER = 0;
|
||||
|
||||
const REVENUE_PRECISION = 2;
|
||||
|
||||
const MAXIMUM_PRODUCT_CATEGORIES = 5;
|
||||
|
||||
// In the GET items parameter, each item has the following array of information
|
||||
const INDEX_ITEM_SKU = 0;
|
||||
const INDEX_ITEM_NAME = 1;
|
||||
const INDEX_ITEM_CATEGORY = 2;
|
||||
const INDEX_ITEM_PRICE = 3;
|
||||
const INDEX_ITEM_QUANTITY = 4;
|
||||
|
||||
// Used in the array of items, internally to this class
|
||||
const INTERNAL_ITEM_SKU = 0;
|
||||
const INTERNAL_ITEM_NAME = 1;
|
||||
const INTERNAL_ITEM_CATEGORY = 2;
|
||||
const INTERNAL_ITEM_CATEGORY2 = 3;
|
||||
const INTERNAL_ITEM_CATEGORY3 = 4;
|
||||
const INTERNAL_ITEM_CATEGORY4 = 5;
|
||||
const INTERNAL_ITEM_CATEGORY5 = 6;
|
||||
const INTERNAL_ITEM_PRICE = 7;
|
||||
const INTERNAL_ITEM_QUANTITY = 8;
|
||||
|
||||
/**
|
||||
* TODO: should remove this, but it is used by getGoalColumn which is used by dimensions. should replace w/ value object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $currentGoal = array();
|
||||
|
||||
public function detectIsThereExistingCartInVisit($visitInformation)
|
||||
{
|
||||
if (empty($visitInformation['visit_goal_buyer'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$goalBuyer = $visitInformation['visit_goal_buyer'];
|
||||
$types = array(GoalManager::TYPE_BUYER_OPEN_CART, GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART);
|
||||
|
||||
// Was there a Cart for this visit prior to the order?
|
||||
return in_array($goalBuyer, $types);
|
||||
}
|
||||
|
||||
public static function getGoalDefinitions($idSite)
|
||||
{
|
||||
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
|
||||
|
||||
if (isset($websiteAttributes['goals'])) {
|
||||
return $websiteAttributes['goals'];
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public static function getGoalDefinition($idSite, $idGoal)
|
||||
{
|
||||
$goals = self::getGoalDefinitions($idSite);
|
||||
|
||||
foreach ($goals as $goal) {
|
||||
if ($goal['idgoal'] == $idGoal) {
|
||||
return $goal;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception('Goal not found');
|
||||
}
|
||||
|
||||
public static function getGoalIds($idSite)
|
||||
{
|
||||
$goals = self::getGoalDefinitions($idSite);
|
||||
$goalIds = array();
|
||||
|
||||
foreach ($goals as $goal) {
|
||||
$goalIds[] = $goal['idgoal'];
|
||||
}
|
||||
|
||||
return $goalIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at the URL or Page Title and sees if it matches any existing Goal definition
|
||||
*
|
||||
* @param int $idSite
|
||||
* @param Action $action
|
||||
* @throws Exception
|
||||
* @return array[] Goals matched
|
||||
*/
|
||||
public function detectGoalsMatchingUrl($idSite, $action)
|
||||
{
|
||||
if (!Common::isGoalPluginEnabled()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$goals = $this->getGoalDefinitions($idSite);
|
||||
|
||||
$convertedGoals = array();
|
||||
foreach ($goals as $goal) {
|
||||
$convertedUrl = $this->detectGoalMatch($goal, $action);
|
||||
if (!is_null($convertedUrl)) {
|
||||
$convertedGoals[] = array('url' => $convertedUrl) + $goal;
|
||||
}
|
||||
}
|
||||
return $convertedGoals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if an Action matches a given goal. If it does, the URL that triggered the goal
|
||||
* is returned. Otherwise null is returned.
|
||||
*
|
||||
* @param array $goal
|
||||
* @param Action $action
|
||||
* @return if a goal is matched, a string of the Action URL is returned, or if no goal was matched it returns null
|
||||
*/
|
||||
public function detectGoalMatch($goal, Action $action)
|
||||
{
|
||||
$actionType = $action->getActionType();
|
||||
|
||||
$attribute = $goal['match_attribute'];
|
||||
|
||||
// if the attribute to match is not the type of the current action
|
||||
if ((($attribute == 'url' || $attribute == 'title') && $actionType != Action::TYPE_PAGE_URL)
|
||||
|| ($attribute == 'file' && $actionType != Action::TYPE_DOWNLOAD)
|
||||
|| ($attribute == 'external_website' && $actionType != Action::TYPE_OUTLINK)
|
||||
|| ($attribute == 'manually')
|
||||
|| self::isEventMatchingGoal($goal) && $actionType != Action::TYPE_EVENT
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
switch ($attribute) {
|
||||
case 'title':
|
||||
// Matching on Page Title
|
||||
$actionToMatch = $action->getActionName();
|
||||
break;
|
||||
case 'event_action':
|
||||
$actionToMatch = $action->getEventAction();
|
||||
break;
|
||||
case 'event_name':
|
||||
$actionToMatch = $action->getEventName();
|
||||
break;
|
||||
case 'event_category':
|
||||
$actionToMatch = $action->getEventCategory();
|
||||
break;
|
||||
// url, external_website, file, manually...
|
||||
default:
|
||||
$actionToMatch = $action->getActionUrlRaw();
|
||||
break;
|
||||
}
|
||||
|
||||
$pattern_type = $goal['pattern_type'];
|
||||
|
||||
$match = $this->isUrlMatchingGoal($goal, $pattern_type, $actionToMatch);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $action->getActionUrl();
|
||||
}
|
||||
|
||||
public function detectGoalId($idSite, Request $request)
|
||||
{
|
||||
if (!Common::isGoalPluginEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$idGoal = $request->getParam('idgoal');
|
||||
|
||||
$goals = $this->getGoalDefinitions($idSite);
|
||||
|
||||
if (!isset($goals[$idGoal])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$goal = $goals[$idGoal];
|
||||
|
||||
$url = $request->getParam('url');
|
||||
$goal['url'] = PageUrl::excludeQueryParametersFromUrl($url, $idSite);
|
||||
return $goal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records one or several goals matched in this request.
|
||||
*
|
||||
* @param Visitor $visitor
|
||||
* @param array $visitorInformation
|
||||
* @param array $visitCustomVariables
|
||||
* @param Action $action
|
||||
*/
|
||||
public function recordGoals(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
$visitorInformation = $visitProperties->getProperties();
|
||||
$visitCustomVariables = $request->getMetadata('CustomVariables', 'visitCustomVariables') ?: array();
|
||||
|
||||
/** @var Action $action */
|
||||
$action = $request->getMetadata('Actions', 'action');
|
||||
|
||||
$goal = $this->getGoalFromVisitor($visitProperties, $request, $action);
|
||||
|
||||
// Copy Custom Variables from Visit row to the Goal conversion
|
||||
// Otherwise, set the Custom Variables found in the cookie sent with this request
|
||||
$goal += $visitCustomVariables;
|
||||
$maxCustomVariables = CustomVariables::getNumUsableCustomVariables();
|
||||
|
||||
for ($i = 1; $i <= $maxCustomVariables; $i++) {
|
||||
if (isset($visitorInformation['custom_var_k' . $i])
|
||||
&& strlen($visitorInformation['custom_var_k' . $i])
|
||||
) {
|
||||
$goal['custom_var_k' . $i] = $visitorInformation['custom_var_k' . $i];
|
||||
}
|
||||
if (isset($visitorInformation['custom_var_v' . $i])
|
||||
&& strlen($visitorInformation['custom_var_v' . $i])
|
||||
) {
|
||||
$goal['custom_var_v' . $i] = $visitorInformation['custom_var_v' . $i];
|
||||
}
|
||||
}
|
||||
|
||||
// some goals are converted, so must be ecommerce Order or Cart Update
|
||||
$isRequestEcommerce = $request->getMetadata('Ecommerce', 'isRequestEcommerce');
|
||||
if ($isRequestEcommerce) {
|
||||
$this->recordEcommerceGoal($visitProperties, $request, $goal, $action);
|
||||
} else {
|
||||
$this->recordStandardGoals($visitProperties, $request, $goal, $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rounded decimal revenue, or if revenue is integer, then returns as is.
|
||||
*
|
||||
* @param int|float $revenue
|
||||
* @return int|float
|
||||
*/
|
||||
protected function getRevenue($revenue)
|
||||
{
|
||||
if (round($revenue) != $revenue) {
|
||||
$revenue = round($revenue, self::REVENUE_PRECISION);
|
||||
}
|
||||
|
||||
$revenue = Common::forceDotAsSeparatorForDecimalPoint($revenue);
|
||||
|
||||
return $revenue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an Ecommerce conversion in the DB. Deals with Items found in the request.
|
||||
* Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc).
|
||||
*
|
||||
* @param array $conversion
|
||||
* @param Visitor $visitor
|
||||
* @param Action $action
|
||||
* @param array $visitInformation
|
||||
*/
|
||||
protected function recordEcommerceGoal(VisitProperties $visitProperties, Request $request, $conversion, $action)
|
||||
{
|
||||
$isThereExistingCartInVisit = $request->getMetadata('Goals', 'isThereExistingCartInVisit');
|
||||
if ($isThereExistingCartInVisit) {
|
||||
Common::printDebug("There is an existing cart for this visit");
|
||||
}
|
||||
|
||||
$visitor = Visitor::makeFromVisitProperties($visitProperties, $request);
|
||||
|
||||
$isGoalAnOrder = $request->getMetadata('Ecommerce', 'isGoalAnOrder');
|
||||
if ($isGoalAnOrder) {
|
||||
$debugMessage = 'The conversion is an Ecommerce order';
|
||||
|
||||
$orderId = $request->getParam('ec_id');
|
||||
|
||||
$conversion['idorder'] = $orderId;
|
||||
$conversion['idgoal'] = self::IDGOAL_ORDER;
|
||||
$conversion['buster'] = Common::hashStringToInt($orderId);
|
||||
|
||||
$conversionDimensions = ConversionDimension::getAllDimensions();
|
||||
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceOrderConversion', $visitor, $action, $conversion);
|
||||
} // If Cart update, select current items in the previous Cart
|
||||
else {
|
||||
$debugMessage = 'The conversion is an Ecommerce Cart Update';
|
||||
|
||||
$conversion['buster'] = 0;
|
||||
$conversion['idgoal'] = self::IDGOAL_CART;
|
||||
|
||||
$conversionDimensions = ConversionDimension::getAllDimensions();
|
||||
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceCartUpdateConversion', $visitor, $action, $conversion);
|
||||
}
|
||||
|
||||
Common::printDebug($debugMessage . ':' . var_export($conversion, true));
|
||||
|
||||
// INSERT or Sync items in the Cart / Order for this visit & order
|
||||
$items = $this->getEcommerceItemsFromRequest($request);
|
||||
|
||||
if (false === $items) {
|
||||
return;
|
||||
}
|
||||
|
||||
$itemsCount = 0;
|
||||
foreach ($items as $item) {
|
||||
$itemsCount += $item[GoalManager::INTERNAL_ITEM_QUANTITY];
|
||||
}
|
||||
|
||||
$conversion['items'] = $itemsCount;
|
||||
|
||||
if ($isThereExistingCartInVisit) {
|
||||
$recorded = $this->getModel()->updateConversion(
|
||||
$visitProperties->getProperty('idvisit'), self::IDGOAL_CART, $conversion);
|
||||
} else {
|
||||
$recorded = $this->insertNewConversion($conversion, $visitProperties->getProperties(), $request, $action);
|
||||
}
|
||||
|
||||
if ($recorded) {
|
||||
$this->recordEcommerceItems($conversion, $items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Items read from the request string
|
||||
* @return array|bool
|
||||
*/
|
||||
private function getEcommerceItemsFromRequest(Request $request)
|
||||
{
|
||||
$items = $request->getParam('ec_items');
|
||||
|
||||
if (empty($items)) {
|
||||
Common::printDebug("There are no Ecommerce items in the request");
|
||||
// we still record an Ecommerce order without any item in it
|
||||
return array();
|
||||
}
|
||||
|
||||
if (!is_array($items)) {
|
||||
Common::printDebug("Error while json_decode the Ecommerce items = " . var_export($items, true));
|
||||
return false;
|
||||
}
|
||||
|
||||
$items = Common::unsanitizeInputValues($items);
|
||||
|
||||
$cleanedItems = $this->getCleanedEcommerceItems($items);
|
||||
return $cleanedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Ecommerce items from the request and records them in the DB
|
||||
*
|
||||
* @param array $goal
|
||||
* @param array $items
|
||||
* @throws Exception
|
||||
* @return int Number of items in the cart
|
||||
*/
|
||||
protected function recordEcommerceItems($goal, $items)
|
||||
{
|
||||
$itemInCartBySku = array();
|
||||
foreach ($items as $item) {
|
||||
$itemInCartBySku[$item[0]] = $item;
|
||||
}
|
||||
|
||||
$itemsInDb = $this->getModel()->getAllItemsCurrentlyInTheCart($goal, self::ITEM_IDORDER_ABANDONED_CART);
|
||||
|
||||
// Look at which items need to be deleted, which need to be added or updated, based on the SKU
|
||||
$skuFoundInDb = $itemsToUpdate = array();
|
||||
|
||||
foreach ($itemsInDb as $itemInDb) {
|
||||
$skuFoundInDb[] = $itemInDb['idaction_sku'];
|
||||
|
||||
// Ensure price comparisons will have the same assumption
|
||||
$itemInDb['price'] = $this->getRevenue($itemInDb['price']);
|
||||
$itemInDbOriginal = $itemInDb;
|
||||
$itemInDb = array_values($itemInDb);
|
||||
|
||||
// Cast all as string, because what comes out of the fetchAll() are strings
|
||||
$itemInDb = $this->getItemRowCast($itemInDb);
|
||||
|
||||
//Item in the cart in the DB, but not anymore in the cart
|
||||
if (!isset($itemInCartBySku[$itemInDb[0]])) {
|
||||
$itemToUpdate = array_merge($itemInDb,
|
||||
array('deleted' => 1,
|
||||
'idorder_original_value' => $itemInDbOriginal['idorder_original_value']
|
||||
)
|
||||
);
|
||||
|
||||
$itemsToUpdate[] = $itemToUpdate;
|
||||
Common::printDebug("Item found in the previous Cart, but no in the current cart/order");
|
||||
Common::printDebug($itemToUpdate);
|
||||
continue;
|
||||
}
|
||||
|
||||
$newItem = $itemInCartBySku[$itemInDb[0]];
|
||||
$newItem = $this->getItemRowCast($newItem);
|
||||
|
||||
if (count($itemInDb) != count($newItem)) {
|
||||
Common::printDebug("ERROR: Different format in items from cart and DB");
|
||||
throw new Exception(" Item in DB and Item in cart have a different format, this is not expected... " . var_export($itemInDb, true) . var_export($newItem, true));
|
||||
}
|
||||
Common::printDebug("Item has changed since the last cart. Previous item stored in cart in database:");
|
||||
Common::printDebug($itemInDb);
|
||||
Common::printDebug("New item to UPDATE the previous row:");
|
||||
$newItem['idorder_original_value'] = $itemInDbOriginal['idorder_original_value'];
|
||||
Common::printDebug($newItem);
|
||||
$itemsToUpdate[] = $newItem;
|
||||
}
|
||||
|
||||
// Items to UPDATE
|
||||
$this->updateEcommerceItems($goal, $itemsToUpdate);
|
||||
|
||||
// Items to INSERT
|
||||
$itemsToInsert = array();
|
||||
foreach ($items as $item) {
|
||||
if (!in_array($item[0], $skuFoundInDb)) {
|
||||
$itemsToInsert[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$this->insertEcommerceItems($goal, $itemsToInsert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads items from the request, then looks up the names from the lookup table
|
||||
* and returns a clean array of items ready for the database.
|
||||
*
|
||||
* @param array $items
|
||||
* @return array $cleanedItems
|
||||
*/
|
||||
private function getCleanedEcommerceItems($items)
|
||||
{
|
||||
// Clean up the items array
|
||||
$cleanedItems = array();
|
||||
foreach ($items as $item) {
|
||||
$name = $category = $category2 = $category3 = $category4 = $category5 = false;
|
||||
$price = 0;
|
||||
$quantity = 1;
|
||||
|
||||
// items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity )
|
||||
if (empty($item[self::INDEX_ITEM_SKU])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sku = $item[self::INDEX_ITEM_SKU];
|
||||
if (!empty($item[self::INDEX_ITEM_NAME])) {
|
||||
$name = $item[self::INDEX_ITEM_NAME];
|
||||
}
|
||||
|
||||
if (!empty($item[self::INDEX_ITEM_CATEGORY])) {
|
||||
$category = $item[self::INDEX_ITEM_CATEGORY];
|
||||
}
|
||||
|
||||
if (isset($item[self::INDEX_ITEM_PRICE])
|
||||
&& is_numeric($item[self::INDEX_ITEM_PRICE])
|
||||
) {
|
||||
$price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]);
|
||||
}
|
||||
if (!empty($item[self::INDEX_ITEM_QUANTITY])
|
||||
&& is_numeric($item[self::INDEX_ITEM_QUANTITY])
|
||||
) {
|
||||
$quantity = (int)$item[self::INDEX_ITEM_QUANTITY];
|
||||
}
|
||||
|
||||
// self::INDEX_ITEM_* are in order
|
||||
$cleanedItems[] = array(
|
||||
self::INTERNAL_ITEM_SKU => $sku,
|
||||
self::INTERNAL_ITEM_NAME => $name,
|
||||
self::INTERNAL_ITEM_CATEGORY => $category,
|
||||
self::INTERNAL_ITEM_CATEGORY2 => $category2,
|
||||
self::INTERNAL_ITEM_CATEGORY3 => $category3,
|
||||
self::INTERNAL_ITEM_CATEGORY4 => $category4,
|
||||
self::INTERNAL_ITEM_CATEGORY5 => $category5,
|
||||
self::INTERNAL_ITEM_PRICE => $price,
|
||||
self::INTERNAL_ITEM_QUANTITY => $quantity
|
||||
);
|
||||
}
|
||||
|
||||
// Lookup Item SKUs, Names & Categories Ids
|
||||
$actionsToLookupAllItems = array();
|
||||
|
||||
// Each item has 7 potential "ids" to lookup in the lookup table
|
||||
$columnsInEachRow = 1 + 1 + self::MAXIMUM_PRODUCT_CATEGORIES;
|
||||
|
||||
foreach ($cleanedItems as $item) {
|
||||
$actionsToLookup = array();
|
||||
list($sku, $name, $category, $price, $quantity) = $item;
|
||||
$actionsToLookup[] = array(trim($sku), Action::TYPE_ECOMMERCE_ITEM_SKU);
|
||||
$actionsToLookup[] = array(trim($name), Action::TYPE_ECOMMERCE_ITEM_NAME);
|
||||
|
||||
// Only one category
|
||||
if (!is_array($category)) {
|
||||
$actionsToLookup[] = array(trim($category), Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
|
||||
} // Multiple categories
|
||||
else {
|
||||
$countCategories = 0;
|
||||
foreach ($category as $productCategory) {
|
||||
$productCategory = trim($productCategory);
|
||||
if (empty($productCategory)) {
|
||||
continue;
|
||||
}
|
||||
$countCategories++;
|
||||
if ($countCategories > self::MAXIMUM_PRODUCT_CATEGORIES) {
|
||||
break;
|
||||
}
|
||||
$actionsToLookup[] = array($productCategory, Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
|
||||
}
|
||||
}
|
||||
// Ensure that each row has the same number of columns, fill in the blanks
|
||||
for ($i = count($actionsToLookup); $i < $columnsInEachRow; $i++) {
|
||||
$actionsToLookup[] = array(false, Action::TYPE_ECOMMERCE_ITEM_CATEGORY);
|
||||
}
|
||||
$actionsToLookupAllItems = array_merge($actionsToLookupAllItems, $actionsToLookup);
|
||||
}
|
||||
|
||||
$actionsLookedUp = TableLogAction::loadIdsAction($actionsToLookupAllItems);
|
||||
|
||||
// Replace SKU, name & category by their ID action
|
||||
foreach ($cleanedItems as $index => &$item) {
|
||||
// SKU
|
||||
$item[0] = $actionsLookedUp[$index * $columnsInEachRow + 0];
|
||||
// Name
|
||||
$item[1] = $actionsLookedUp[$index * $columnsInEachRow + 1];
|
||||
// Categories
|
||||
$item[2] = $actionsLookedUp[$index * $columnsInEachRow + 2];
|
||||
$item[3] = $actionsLookedUp[$index * $columnsInEachRow + 3];
|
||||
$item[4] = $actionsLookedUp[$index * $columnsInEachRow + 4];
|
||||
$item[5] = $actionsLookedUp[$index * $columnsInEachRow + 5];
|
||||
$item[6] = $actionsLookedUp[$index * $columnsInEachRow + 6];
|
||||
}
|
||||
|
||||
return $cleanedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cart items in the DB
|
||||
* that have been modified since the last cart update
|
||||
*
|
||||
* @param array $goal
|
||||
* @param array $itemsToUpdate
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function updateEcommerceItems($goal, $itemsToUpdate)
|
||||
{
|
||||
if (empty($itemsToUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Common::printDebug("Goal data used to update ecommerce items:");
|
||||
Common::printDebug($goal);
|
||||
|
||||
foreach ($itemsToUpdate as $item) {
|
||||
$newRow = $this->getItemRowEnriched($goal, $item);
|
||||
Common::printDebug($newRow);
|
||||
|
||||
$this->getModel()->updateEcommerceItem($item['idorder_original_value'], $newRow);
|
||||
}
|
||||
}
|
||||
|
||||
private function getModel()
|
||||
{
|
||||
return new Model();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts in the cart in the DB the new items
|
||||
* that were not previously in the cart
|
||||
*
|
||||
* @param array $goal
|
||||
* @param array $itemsToInsert
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function insertEcommerceItems($goal, $itemsToInsert)
|
||||
{
|
||||
if (empty($itemsToInsert)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Common::printDebug("Ecommerce items that are added to the cart/order");
|
||||
Common::printDebug($itemsToInsert);
|
||||
|
||||
$items = array();
|
||||
|
||||
foreach ($itemsToInsert as $item) {
|
||||
$items[] = $this->getItemRowEnriched($goal, $item);
|
||||
}
|
||||
|
||||
$this->getModel()->createEcommerceItems($items);
|
||||
}
|
||||
|
||||
protected function getItemRowEnriched($goal, $item)
|
||||
{
|
||||
$newRow = array(
|
||||
'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU],
|
||||
'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME],
|
||||
'idaction_category' => (int)$item[self::INTERNAL_ITEM_CATEGORY],
|
||||
'idaction_category2' => (int)$item[self::INTERNAL_ITEM_CATEGORY2],
|
||||
'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3],
|
||||
'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4],
|
||||
'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5],
|
||||
'price' => Common::forceDotAsSeparatorForDecimalPoint($item[self::INTERNAL_ITEM_PRICE]),
|
||||
'quantity' => $item[self::INTERNAL_ITEM_QUANTITY],
|
||||
'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted
|
||||
'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts
|
||||
'idsite' => $goal['idsite'],
|
||||
'idvisitor' => $goal['idvisitor'],
|
||||
'server_time' => $goal['server_time'],
|
||||
'idvisit' => $goal['idvisit']
|
||||
);
|
||||
return $newRow;
|
||||
}
|
||||
|
||||
public function getGoalColumn($column)
|
||||
{
|
||||
if (array_key_exists($column, $this->currentGoal)) {
|
||||
return $this->currentGoal[$column];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a standard non-Ecommerce goal in the DB (URL/Title matching),
|
||||
* linking the conversion to the action that triggered it
|
||||
* @param $goal
|
||||
* @param Visitor $visitor
|
||||
* @param Action $action
|
||||
* @param $visitorInformation
|
||||
*/
|
||||
protected function recordStandardGoals(VisitProperties $visitProperties, Request $request, $goal, $action)
|
||||
{
|
||||
$visitor = Visitor::makeFromVisitProperties($visitProperties, $request);
|
||||
|
||||
$convertedGoals = $request->getMetadata('Goals', 'goalsConverted') ?: array();
|
||||
foreach ($convertedGoals as $convertedGoal) {
|
||||
$this->currentGoal = $convertedGoal;
|
||||
Common::printDebug("- Goal " . $convertedGoal['idgoal'] . " matched. Recording...");
|
||||
$conversion = $goal;
|
||||
$conversion['idgoal'] = $convertedGoal['idgoal'];
|
||||
$conversion['url'] = $convertedGoal['url'];
|
||||
|
||||
if (!is_null($action)) {
|
||||
$conversion['idaction_url'] = $action->getIdActionUrl();
|
||||
$conversion['idlink_va'] = $action->getIdLinkVisitAction();
|
||||
}
|
||||
|
||||
// If multiple Goal conversions per visit, set a cache buster
|
||||
if ($convertedGoal['allow_multiple'] == 0) {
|
||||
$conversion['buster'] = 0;
|
||||
} else {
|
||||
$lastActionTime = $visitProperties->getProperty('visit_last_action_time');
|
||||
if (empty($lastActionTime)) {
|
||||
$conversion['buster'] = $this->makeRandomMySqlUnsignedInt(10);
|
||||
} else {
|
||||
$conversion['buster'] = $this->makeRandomMySqlUnsignedInt(2) . Common::mb_substr($visitProperties->getProperty('visit_last_action_time'), 2);
|
||||
}
|
||||
}
|
||||
|
||||
$conversionDimensions = ConversionDimension::getAllDimensions();
|
||||
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onGoalConversion', $visitor, $action, $conversion);
|
||||
|
||||
$this->insertNewConversion($conversion, $visitProperties->getProperties(), $request, $action, $convertedGoal);
|
||||
}
|
||||
}
|
||||
|
||||
private function makeRandomMySqlUnsignedInt($length)
|
||||
{
|
||||
// mysql int unsgined max value is 4294967295 so we want to allow max 39999...
|
||||
$randomInt = Common::getRandomString(1, '123');
|
||||
$randomInt .= Common::getRandomString($length - 1, '0123456789');
|
||||
return $randomInt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB
|
||||
*
|
||||
* @param array $conversion
|
||||
* @param array $visitInformation
|
||||
* @param Request $request
|
||||
* @param Action|null $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function insertNewConversion($conversion, $visitInformation, Request $request, $action, $convertedGoal = null)
|
||||
{
|
||||
/**
|
||||
* Triggered before persisting a new [conversion entity](/guides/persistence-and-the-mysql-backend#conversions).
|
||||
*
|
||||
* This event can be used to modify conversion information or to add new information to be persisted.
|
||||
*
|
||||
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
|
||||
*
|
||||
* @param array $conversion The conversion entity. Read [this](/guides/persistence-and-the-mysql-backend#conversions)
|
||||
* to see what it contains.
|
||||
* @param array $visitInformation The visit entity that we are tracking a conversion for. See what
|
||||
* information it contains [here](/guides/persistence-and-the-mysql-backend#visits).
|
||||
* @param \Piwik\Tracker\Request $request An object describing the tracking request being processed.
|
||||
* @param Action|null $action An action object like ActionPageView or ActionDownload, or null if no action is
|
||||
* supposed to be processed.
|
||||
* @deprecated
|
||||
* @ignore
|
||||
*/
|
||||
Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $request, $action));
|
||||
|
||||
if (!empty($convertedGoal)
|
||||
&& $this->isEventMatchingGoal($convertedGoal)
|
||||
&& !empty($convertedGoal['event_value_as_revenue'])
|
||||
) {
|
||||
$eventValue = ActionEvent::getEventValue($request);
|
||||
if ($eventValue != '') {
|
||||
$conversion['revenue'] = $eventValue;
|
||||
}
|
||||
}
|
||||
|
||||
$newGoalDebug = $conversion;
|
||||
$newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']);
|
||||
Common::printDebug($newGoalDebug);
|
||||
|
||||
$idorder = $request->getParam('ec_id');
|
||||
|
||||
$wasInserted = $this->getModel()->createConversion($conversion);
|
||||
if (!$wasInserted
|
||||
&& !empty($idorder)
|
||||
) {
|
||||
$idSite = $request->getIdSite();
|
||||
throw new InvalidRequestParameterException("Invalid non-unique idsite/idorder combination ($idSite, $idorder), conversion was not inserted.");
|
||||
}
|
||||
|
||||
return $wasInserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the item array so that array comparisons work nicely
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function getItemRowCast($row)
|
||||
{
|
||||
return array(
|
||||
(string)(int)$row[self::INTERNAL_ITEM_SKU],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_NAME],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_CATEGORY],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_CATEGORY2],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_CATEGORY3],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_CATEGORY4],
|
||||
(string)(int)$row[self::INTERNAL_ITEM_CATEGORY5],
|
||||
(string)$row[self::INTERNAL_ITEM_PRICE],
|
||||
(string)$row[self::INTERNAL_ITEM_QUANTITY],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $goal
|
||||
* @param $pattern_type
|
||||
* @param $url
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function isUrlMatchingGoal($goal, $pattern_type, $url)
|
||||
{
|
||||
$url = Common::unsanitizeInputValue($url);
|
||||
$goal['pattern'] = Common::unsanitizeInputValue($goal['pattern']);
|
||||
|
||||
$match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url);
|
||||
|
||||
if (!$match) {
|
||||
// Users may set Goal matching URL as URL encoded
|
||||
$goal['pattern'] = urldecode($goal['pattern']);
|
||||
|
||||
$match = $this->isGoalPatternMatchingUrl($goal, $pattern_type, $url);
|
||||
}
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ConversionDimension[] $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(Request $request, $dimensions, $hook, $visitor, $action, $valuesToUpdate)
|
||||
{
|
||||
foreach ($dimensions as $dimension) {
|
||||
$value = $dimension->$hook($request, $visitor, $action, $this);
|
||||
|
||||
if (false !== $value) {
|
||||
if (is_float($value)) {
|
||||
$value = Common::forceDotAsSeparatorForDecimalPoint($value);
|
||||
}
|
||||
|
||||
$fieldName = $dimension->getColumnName();
|
||||
$visitor->setVisitorColumn($fieldName, $value);
|
||||
|
||||
$valuesToUpdate[$fieldName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesToUpdate;
|
||||
}
|
||||
|
||||
private function getGoalFromVisitor(VisitProperties $visitProperties, Request $request, $action)
|
||||
{
|
||||
$goal = array(
|
||||
'idvisit' => $visitProperties->getProperty('idvisit'),
|
||||
'idvisitor' => $visitProperties->getProperty('idvisitor'),
|
||||
'server_time' => Date::getDatetimeFromTimestamp($visitProperties->getProperty('visit_last_action_time')),
|
||||
);
|
||||
|
||||
$visitDimensions = VisitDimension::getAllDimensions();
|
||||
|
||||
$visit = Visitor::makeFromVisitProperties($visitProperties, $request);
|
||||
foreach ($visitDimensions as $dimension) {
|
||||
$value = $dimension->onAnyGoalConversion($request, $visit, $action);
|
||||
if (false !== $value) {
|
||||
$goal[$dimension->getColumnName()] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $goal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $goal
|
||||
* @param $pattern_type
|
||||
* @param $url
|
||||
* @return bool
|
||||
*/
|
||||
protected function isGoalPatternMatchingUrl($goal, $pattern_type, $url)
|
||||
{
|
||||
switch ($pattern_type) {
|
||||
case 'regex':
|
||||
$pattern = self::formatRegex($goal['pattern']);
|
||||
if (!$goal['case_sensitive']) {
|
||||
$pattern .= 'i';
|
||||
}
|
||||
$match = (@preg_match($pattern, $url) == 1);
|
||||
break;
|
||||
case 'contains':
|
||||
if ($goal['case_sensitive']) {
|
||||
$matched = strpos($url, $goal['pattern']);
|
||||
} else {
|
||||
$matched = stripos($url, $goal['pattern']);
|
||||
}
|
||||
$match = ($matched !== false);
|
||||
break;
|
||||
case 'exact':
|
||||
if ($goal['case_sensitive']) {
|
||||
$matched = strcmp($goal['pattern'], $url);
|
||||
} else {
|
||||
$matched = strcasecmp($goal['pattern'], $url);
|
||||
}
|
||||
$match = ($matched == 0);
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
StaticContainer::get('Psr\Log\LoggerInterface')->warning(Piwik::translate('General_ExceptionInvalidGoalPattern', array($pattern_type)));
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
$match = false;
|
||||
break;
|
||||
}
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a goal regex pattern to a usable regex
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return string
|
||||
*/
|
||||
public static function formatRegex($pattern)
|
||||
{
|
||||
if (strpos($pattern, '/') !== false
|
||||
&& strpos($pattern, '\\/') === false
|
||||
) {
|
||||
$pattern = str_replace('/', '\\/', $pattern);
|
||||
}
|
||||
return '/' . $pattern . '/';
|
||||
}
|
||||
|
||||
public static function isEventMatchingGoal($goal)
|
||||
{
|
||||
return in_array($goal['match_attribute'], array('event_action', 'event_name', 'event_category'));
|
||||
}
|
||||
}
|
117
msd2/tracking/piwik/core/Tracker/Handler.php
Normal file
117
msd2/tracking/piwik/core/Tracker/Handler.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?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\Common;
|
||||
use Piwik\Exception\InvalidRequestParameterException;
|
||||
use Piwik\Exception\UnexpectedWebsiteFoundException;
|
||||
use Piwik\Tracker;
|
||||
use Exception;
|
||||
use Piwik\Url;
|
||||
|
||||
class Handler
|
||||
{
|
||||
/**
|
||||
* @var Response
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* @var ScheduledTasksRunner
|
||||
*/
|
||||
private $tasksRunner;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setResponse(new Response());
|
||||
}
|
||||
|
||||
public function setResponse($response)
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function init(Tracker $tracker, RequestSet $requestSet)
|
||||
{
|
||||
$this->response->init($tracker);
|
||||
}
|
||||
|
||||
public function process(Tracker $tracker, RequestSet $requestSet)
|
||||
{
|
||||
foreach ($requestSet->getRequests() as $request) {
|
||||
$tracker->trackRequest($request);
|
||||
}
|
||||
}
|
||||
|
||||
public function onStartTrackRequests(Tracker $tracker, RequestSet $requestSet)
|
||||
{
|
||||
}
|
||||
|
||||
public function onAllRequestsTracked(Tracker $tracker, RequestSet $requestSet)
|
||||
{
|
||||
$tasks = $this->getScheduledTasksRunner();
|
||||
if ($tasks->shouldRun($tracker)) {
|
||||
$tasks->runScheduledTasks();
|
||||
}
|
||||
}
|
||||
|
||||
private function getScheduledTasksRunner()
|
||||
{
|
||||
if (is_null($this->tasksRunner)) {
|
||||
$this->tasksRunner = new ScheduledTasksRunner();
|
||||
}
|
||||
|
||||
return $this->tasksRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setScheduledTasksRunner(ScheduledTasksRunner $runner)
|
||||
{
|
||||
$this->tasksRunner = $runner;
|
||||
}
|
||||
|
||||
public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e)
|
||||
{
|
||||
Common::printDebug("Exception: " . $e->getMessage());
|
||||
|
||||
$statusCode = 500;
|
||||
if ($e instanceof UnexpectedWebsiteFoundException) {
|
||||
$statusCode = 400;
|
||||
} elseif ($e instanceof InvalidRequestParameterException) {
|
||||
$statusCode = 400;
|
||||
}
|
||||
|
||||
$this->response->outputException($tracker, $e, $statusCode);
|
||||
$this->redirectIfNeeded($requestSet);
|
||||
}
|
||||
|
||||
public function finish(Tracker $tracker, RequestSet $requestSet)
|
||||
{
|
||||
$this->response->outputResponse($tracker);
|
||||
$this->redirectIfNeeded($requestSet);
|
||||
return $this->response->getOutput();
|
||||
}
|
||||
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
protected function redirectIfNeeded(RequestSet $requestSet)
|
||||
{
|
||||
$redirectUrl = $requestSet->shouldPerformRedirectToUrl();
|
||||
|
||||
if (!empty($redirectUrl)) {
|
||||
Url::redirectToUrl($redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
42
msd2/tracking/piwik/core/Tracker/Handler/Factory.php
Normal file
42
msd2/tracking/piwik/core/Tracker/Handler/Factory.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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\Handler;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Tracker\Handler;
|
||||
|
||||
class Factory
|
||||
{
|
||||
public static function make()
|
||||
{
|
||||
$handler = null;
|
||||
|
||||
/**
|
||||
* Triggered before a new **handler tracking object** is created. Subscribers to this
|
||||
* event can force the use of a custom handler tracking object that extends from
|
||||
* {@link Piwik\Tracker\Handler} and customize any tracking behavior.
|
||||
*
|
||||
* @param \Piwik\Tracker\Handler &$handler Initialized to null, but can be set to
|
||||
* a new handler object. If it isn't modified
|
||||
* Piwik uses the default class.
|
||||
* @ignore This event is not public yet as the Handler API is not really stable yet
|
||||
*/
|
||||
Piwik::postEvent('Tracker.newHandler', array(&$handler));
|
||||
|
||||
if (is_null($handler)) {
|
||||
$handler = new Handler();
|
||||
} elseif (!($handler instanceof Handler)) {
|
||||
throw new Exception("The Handler object set in the plugin must be an instance of Piwik\\Tracker\\Handler");
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
}
|
80
msd2/tracking/piwik/core/Tracker/IgnoreCookie.php
Normal file
80
msd2/tracking/piwik/core/Tracker/IgnoreCookie.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?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\Config;
|
||||
use Piwik\Cookie;
|
||||
|
||||
/**
|
||||
* Tracking cookies.
|
||||
*
|
||||
*/
|
||||
class IgnoreCookie
|
||||
{
|
||||
/**
|
||||
* Get tracking cookie
|
||||
*
|
||||
* @return Cookie
|
||||
*/
|
||||
private static function getTrackingCookie()
|
||||
{
|
||||
$cookie_name = @Config::getInstance()->Tracker['cookie_name'];
|
||||
$cookie_path = @Config::getInstance()->Tracker['cookie_path'];
|
||||
|
||||
return new Cookie($cookie_name, null, $cookie_path);
|
||||
}
|
||||
|
||||
public static function deleteThirdPartyCookieUIDIfExists()
|
||||
{
|
||||
$trackingCookie = self::getTrackingCookie();
|
||||
if ($trackingCookie->isCookieFound()) {
|
||||
$trackingCookie->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ignore (visit) cookie
|
||||
*
|
||||
* @return Cookie
|
||||
*/
|
||||
public static function getIgnoreCookie()
|
||||
{
|
||||
$cookie_name = @Config::getInstance()->Tracker['ignore_visits_cookie_name'];
|
||||
$cookie_path = @Config::getInstance()->Tracker['cookie_path'];
|
||||
|
||||
return new Cookie($cookie_name, null, $cookie_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ignore (visit) cookie or deletes it if already present
|
||||
*/
|
||||
public static function setIgnoreCookie()
|
||||
{
|
||||
$ignoreCookie = self::getIgnoreCookie();
|
||||
if ($ignoreCookie->isCookieFound()) {
|
||||
$ignoreCookie->delete();
|
||||
} else {
|
||||
$ignoreCookie->set('ignore', '*');
|
||||
$ignoreCookie->save();
|
||||
}
|
||||
|
||||
self::deleteThirdPartyCookieUIDIfExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if ignore (visit) cookie is present
|
||||
*
|
||||
* @return bool True if ignore cookie found; false otherwise
|
||||
*/
|
||||
public static function isIgnoreCookieFound()
|
||||
{
|
||||
$cookie = self::getIgnoreCookie();
|
||||
return $cookie->isCookieFound() && $cookie->get('ignore') === '*';
|
||||
}
|
||||
}
|
111
msd2/tracking/piwik/core/Tracker/LogTable.php
Normal file
111
msd2/tracking/piwik/core/Tracker/LogTable.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Base class for LogTables. You need to create a log table eg if you want to be able to create a segment for a custom
|
||||
* log table.
|
||||
*/
|
||||
abstract class LogTable {
|
||||
|
||||
/**
|
||||
* Get the unprefixed database table name. For example 'log_visit' or 'log_action'.
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getName();
|
||||
|
||||
/**
|
||||
* Get the name of the column that represents the primary key. For example "idvisit" or "idlink_va". If the table
|
||||
* does not have a unique ID for each row, you may choose a column that comes closest to it, for example "idvisit".
|
||||
* @return string
|
||||
*/
|
||||
public function getIdColumn()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the column that can be used to join a visit with another table. This is the name of the column
|
||||
* that represents the "idvisit".
|
||||
* @return string
|
||||
*/
|
||||
public function getColumnToJoinOnIdVisit()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the column that can be used to join an action with another table. This is the name of the column
|
||||
* that represents the "idaction".
|
||||
*
|
||||
* This could be more generic eg by specifiying "$this->joinableOn = array('action' => 'idaction') and this
|
||||
* would allow to also add more complex structures in the future but not needed for now I'd say. Let's go with
|
||||
* simpler, more clean and expressive solution for now until needed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getColumnToJoinOnIdAction()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* If a table can neither be joined via idVisit nor idAction, it should be given a way to join with other tables
|
||||
* so the log table can be joined via idvisit through a different table joins.
|
||||
*
|
||||
* For this to work it requires the same column to be present in two tables. If for example you have a table
|
||||
* `log_foo_bar (idlogfoobar, idlogfoo)` and a table `log_foo(idlogfoo, idsite, idvisit)`, then you can in the
|
||||
* log table instance for `log_foo_bar` return `array('log_foo' => 'idlogfoo')`. This tells the core that a join
|
||||
* with that other log table is possible using the specified column.
|
||||
* @return array
|
||||
*/
|
||||
public function getWaysToJoinToOtherLogTables()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines whether this table should be joined via a subselect. Return true if a complex join is needed. (eg when
|
||||
* having visits and needing actions, or when having visits and needing conversions, or vice versa).
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldJoinWithSubSelect()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of a log table that allows to join on a visit. Eg if there is a table "action", and it is not
|
||||
* joinable with "visit" table, it can return "log_link_visit_action" to be able to join the action table on visit
|
||||
* via this link table.
|
||||
*
|
||||
* In theory there could be case where it may be needed to join via two tables, so it could be needed at some
|
||||
* point to return an array of tables here. not sure if we should handle this case just yet. Alternatively,
|
||||
* once needed eg in LogQueryBuilder, we should maybe better call instead ->getLinkTableToBeAbleToJoinOnVisit()
|
||||
* again on the returned table until we have found a table that can be joined with visit.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLinkTableToBeAbleToJoinOnVisit()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of the columns that represents the primary key. For example "idvisit" or "idlink_va". If the table
|
||||
* defines the primary key based on multiple columns, you must specify them all
|
||||
* (eg array('idvisit', 'idgoal', 'buster')).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPrimaryKey()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
}
|
467
msd2/tracking/piwik/core/Tracker/Model.php
Normal file
467
msd2/tracking/piwik/core/Tracker/Model.php
Normal file
@ -0,0 +1,467 @@
|
||||
<?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 Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Tracker;
|
||||
|
||||
class Model
|
||||
{
|
||||
|
||||
public function createAction($visitAction)
|
||||
{
|
||||
$fields = implode(", ", array_keys($visitAction));
|
||||
$values = Common::getSqlStringFieldsArray($visitAction);
|
||||
$table = Common::prefixTable('log_link_visit_action');
|
||||
|
||||
$sql = "INSERT INTO $table ($fields) VALUES ($values)";
|
||||
$bind = array_values($visitAction);
|
||||
|
||||
$db = $this->getDb();
|
||||
$db->query($sql, $bind);
|
||||
|
||||
$id = $db->lastInsertId();
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function createConversion($conversion)
|
||||
{
|
||||
$fields = implode(", ", array_keys($conversion));
|
||||
$bindFields = Common::getSqlStringFieldsArray($conversion);
|
||||
$table = Common::prefixTable('log_conversion');
|
||||
|
||||
$sql = "INSERT IGNORE INTO $table ($fields) VALUES ($bindFields) ";
|
||||
$bind = array_values($conversion);
|
||||
|
||||
$db = $this->getDb();
|
||||
$result = $db->query($sql, $bind);
|
||||
|
||||
// If a record was inserted, we return true
|
||||
return $db->rowCount($result) > 0;
|
||||
}
|
||||
|
||||
public function updateConversion($idVisit, $idGoal, $newConversion)
|
||||
{
|
||||
$updateWhere = array(
|
||||
'idvisit' => $idVisit,
|
||||
'idgoal' => $idGoal,
|
||||
'buster' => 0,
|
||||
);
|
||||
|
||||
$updateParts = $sqlBind = $updateWhereParts = array();
|
||||
|
||||
foreach ($newConversion as $name => $value) {
|
||||
$updateParts[] = $name . " = ?";
|
||||
$sqlBind[] = $value;
|
||||
}
|
||||
|
||||
foreach ($updateWhere as $name => $value) {
|
||||
$updateWhereParts[] = $name . " = ?";
|
||||
$sqlBind[] = $value;
|
||||
}
|
||||
|
||||
$parts = implode($updateParts, ', ');
|
||||
$table = Common::prefixTable('log_conversion');
|
||||
|
||||
$sql = "UPDATE $table SET $parts WHERE " . implode($updateWhereParts, ' AND ');
|
||||
|
||||
try {
|
||||
$this->getDb()->query($sql, $sqlBind);
|
||||
} catch (Exception $e) {
|
||||
Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the Ecommerce items from the request and records them in the DB
|
||||
*
|
||||
* @param array $goal
|
||||
* @param int $defaultIdOrder
|
||||
* @throws Exception
|
||||
* @return array
|
||||
*/
|
||||
public function getAllItemsCurrentlyInTheCart($goal, $defaultIdOrder)
|
||||
{
|
||||
$sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value
|
||||
FROM " . Common::prefixTable('log_conversion_item') . "
|
||||
WHERE idvisit = ? AND (idorder = ? OR idorder = ?)";
|
||||
|
||||
$bind = array(
|
||||
$goal['idvisit'],
|
||||
isset($goal['idorder']) ? $goal['idorder'] : $defaultIdOrder,
|
||||
$defaultIdOrder
|
||||
);
|
||||
|
||||
$itemsInDb = $this->getDb()->fetchAll($sql, $bind);
|
||||
|
||||
Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true));
|
||||
Common::printDebug($itemsInDb);
|
||||
|
||||
return $itemsInDb;
|
||||
}
|
||||
|
||||
public function createEcommerceItems($ecommerceItems)
|
||||
{
|
||||
$sql = "INSERT INTO " . Common::prefixTable('log_conversion_item');
|
||||
$i = 0;
|
||||
$bind = array();
|
||||
|
||||
foreach ($ecommerceItems as $item) {
|
||||
if ($i === 0) {
|
||||
$fields = implode(', ', array_keys($item));
|
||||
$sql .= ' (' . $fields . ') VALUES ';
|
||||
} elseif ($i > 0) {
|
||||
$sql .= ',';
|
||||
}
|
||||
|
||||
$newRow = array_values($item);
|
||||
$sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) ";
|
||||
$bind = array_merge($bind, $newRow);
|
||||
$i++;
|
||||
}
|
||||
|
||||
Common::printDebug($sql);
|
||||
Common::printDebug($bind);
|
||||
|
||||
try {
|
||||
$this->getDb()->query($sql, $bind);
|
||||
} catch (Exception $e) {
|
||||
if ($e->getCode() == 23000 ||
|
||||
false !== strpos($e->getMessage(), 'Duplicate entry') ||
|
||||
false !== strpos($e->getMessage(), 'Integrity constraint violation')) {
|
||||
Common::printDebug('Did not create ecommerce item as item was already created');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new action into the log_action table. If there is an existing action that was inserted
|
||||
* due to another request pre-empting this one, the newly inserted action is deleted.
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $type
|
||||
* @param int $urlPrefix
|
||||
* @return int The ID of the action (can be for an existing action or new action).
|
||||
*/
|
||||
public function createNewIdAction($name, $type, $urlPrefix)
|
||||
{
|
||||
$newActionId = $this->insertNewAction($name, $type, $urlPrefix);
|
||||
|
||||
$realFirstActionId = $this->getIdActionMatchingNameAndType($name, $type);
|
||||
|
||||
// if the inserted action ID is not the same as the queried action ID, then that means we inserted
|
||||
// a duplicate, so remove it now
|
||||
if ($realFirstActionId != $newActionId) {
|
||||
$this->deleteDuplicateAction($newActionId);
|
||||
}
|
||||
|
||||
return $realFirstActionId;
|
||||
}
|
||||
|
||||
private function insertNewAction($name, $type, $urlPrefix)
|
||||
{
|
||||
$table = Common::prefixTable('log_action');
|
||||
$sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)";
|
||||
|
||||
$db = $this->getDb();
|
||||
$db->query($sql, array($name, $name, $type, $urlPrefix));
|
||||
|
||||
$actionId = $db->lastInsertId();
|
||||
|
||||
return $actionId;
|
||||
}
|
||||
|
||||
private function getSqlSelectActionId()
|
||||
{
|
||||
// it is possible for multiple actions to exist in the DB (due to rare concurrency issues), so the ORDER BY and
|
||||
// LIMIT are important
|
||||
$sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action')
|
||||
. " WHERE " . $this->getSqlConditionToMatchSingleAction() . " "
|
||||
. "ORDER BY idaction ASC LIMIT 1";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getIdActionMatchingNameAndType($name, $type)
|
||||
{
|
||||
$sql = $this->getSqlSelectActionId();
|
||||
$bind = array($name, $name, $type);
|
||||
|
||||
$idAction = $this->getDb()->fetchOne($sql, $bind);
|
||||
|
||||
return $idAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IDs for multiple actions based on name + type values.
|
||||
*
|
||||
* @param array $actionsNameAndType Array like `array( array('name' => '...', 'type' => 1), ... )`
|
||||
* @return array|false Array of DB rows w/ columns: **idaction**, **type**, **name**.
|
||||
*/
|
||||
public function getIdsAction($actionsNameAndType)
|
||||
{
|
||||
$sql = "SELECT MIN(idaction) as idaction, type, name FROM " . Common::prefixTable('log_action')
|
||||
. " WHERE";
|
||||
$bind = array();
|
||||
|
||||
$i = 0;
|
||||
foreach ($actionsNameAndType as $actionNameType) {
|
||||
$name = $actionNameType['name'];
|
||||
|
||||
if (empty($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($i > 0) {
|
||||
$sql .= " OR";
|
||||
}
|
||||
|
||||
$sql .= " " . $this->getSqlConditionToMatchSingleAction() . " ";
|
||||
|
||||
$bind[] = $name;
|
||||
$bind[] = $name;
|
||||
$bind[] = $actionNameType['type'];
|
||||
$i++;
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY type, hash, name";
|
||||
|
||||
// Case URL & Title are empty
|
||||
if (empty($bind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actionIds = $this->getDb()->fetchAll($sql, $bind);
|
||||
|
||||
return $actionIds;
|
||||
}
|
||||
|
||||
public function updateEcommerceItem($originalIdOrder, $newItem)
|
||||
{
|
||||
$updateParts = $sqlBind = array();
|
||||
foreach ($newItem as $name => $value) {
|
||||
$updateParts[] = $name . " = ?";
|
||||
$sqlBind[] = $value;
|
||||
}
|
||||
|
||||
$parts = implode($updateParts, ', ');
|
||||
$table = Common::prefixTable('log_conversion_item');
|
||||
|
||||
$sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?";
|
||||
|
||||
$sqlBind[] = $newItem['idvisit'];
|
||||
$sqlBind[] = $originalIdOrder;
|
||||
$sqlBind[] = $newItem['idaction_sku'];
|
||||
|
||||
$this->getDb()->query($sql, $sqlBind);
|
||||
}
|
||||
|
||||
public function createVisit($visit)
|
||||
{
|
||||
$fields = array_keys($visit);
|
||||
$fields = implode(", ", $fields);
|
||||
$values = Common::getSqlStringFieldsArray($visit);
|
||||
$table = Common::prefixTable('log_visit');
|
||||
|
||||
$sql = "INSERT INTO $table ($fields) VALUES ($values)";
|
||||
$bind = array_values($visit);
|
||||
|
||||
$db = $this->getDb();
|
||||
$db->query($sql, $bind);
|
||||
|
||||
return $db->lastInsertId();
|
||||
}
|
||||
|
||||
public function updateVisit($idSite, $idVisit, $valuesToUpdate)
|
||||
{
|
||||
list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate);
|
||||
|
||||
$parts = implode($updateParts, ', ');
|
||||
$table = Common::prefixTable('log_visit');
|
||||
|
||||
$sqlQuery = "UPDATE $table SET $parts WHERE idsite = ? AND idvisit = ?";
|
||||
|
||||
$sqlBind[] = $idSite;
|
||||
$sqlBind[] = $idVisit;
|
||||
|
||||
$db = $this->getDb();
|
||||
$result = $db->query($sqlQuery, $sqlBind);
|
||||
$wasInserted = $db->rowCount($result) != 0;
|
||||
|
||||
if (!$wasInserted) {
|
||||
Common::printDebug("Visitor with this idvisit wasn't found in the DB.");
|
||||
Common::printDebug("$sqlQuery --- ");
|
||||
Common::printDebug($sqlBind);
|
||||
}
|
||||
|
||||
return $wasInserted;
|
||||
}
|
||||
|
||||
public function updateAction($idLinkVa, $valuesToUpdate)
|
||||
{
|
||||
if (empty($idLinkVa)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate);
|
||||
|
||||
$parts = implode($updateParts, ', ');
|
||||
$table = Common::prefixTable('log_link_visit_action');
|
||||
|
||||
$sqlQuery = "UPDATE $table SET $parts WHERE idlink_va = ?";
|
||||
|
||||
$sqlBind[] = $idLinkVa;
|
||||
|
||||
$db = $this->getDb();
|
||||
$result = $db->query($sqlQuery, $sqlBind);
|
||||
$wasInserted = $db->rowCount($result) != 0;
|
||||
|
||||
if (!$wasInserted) {
|
||||
Common::printDebug("Action with this idLinkVa wasn't found in the DB.");
|
||||
Common::printDebug("$sqlQuery --- ");
|
||||
Common::printDebug($sqlBind);
|
||||
}
|
||||
|
||||
return $wasInserted;
|
||||
}
|
||||
|
||||
public function findVisitor($idSite, $configId, $idVisitor, $fieldsToRead, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead)
|
||||
{
|
||||
$selectCustomVariables = '';
|
||||
|
||||
$selectFields = implode(', ', $fieldsToRead);
|
||||
|
||||
$select = "SELECT $selectFields $selectCustomVariables ";
|
||||
$from = "FROM " . Common::prefixTable('log_visit');
|
||||
|
||||
// Two use cases:
|
||||
// 1) there is no visitor ID so we try to match only on config_id (heuristics)
|
||||
// Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed,
|
||||
// importing server access logs with import_logs.py, etc.
|
||||
// In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign
|
||||
// this page view to the wrong visitor, but this is better than creating artificial visits.
|
||||
// 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API),
|
||||
// and in these cases, we force to look up this visitor id
|
||||
$configIdWhere = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?";
|
||||
$configIdbindSql = array(
|
||||
$timeLookBack,
|
||||
$timeLookAhead,
|
||||
$idSite
|
||||
);
|
||||
|
||||
$visitorIdWhere = 'idsite = ? AND visit_last_action_time <= ?';
|
||||
$visitorIdbindSql = [$idSite, $timeLookAhead];
|
||||
|
||||
if ($shouldMatchOneFieldOnly && $isVisitorIdToLookup) {
|
||||
$visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
|
||||
} elseif ($shouldMatchOneFieldOnly) {
|
||||
$visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
|
||||
} else {
|
||||
$visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $visitorIdWhere, $visitorIdbindSql);
|
||||
|
||||
if (empty($visitRow)) {
|
||||
$configIdWhere .= ' AND user_id IS NULL ';
|
||||
$visitRow = $this->findVisitorByConfigId($configId, $select, $from, $configIdWhere, $configIdbindSql);
|
||||
}
|
||||
}
|
||||
|
||||
return $visitRow;
|
||||
}
|
||||
|
||||
private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql)
|
||||
{
|
||||
// will use INDEX index_idsite_idvisitor (idsite, idvisitor)
|
||||
$where .= ' AND idvisitor = ?';
|
||||
$bindSql[] = $idVisitor;
|
||||
|
||||
return $this->fetchVisitor($select, $from, $where, $bindSql);
|
||||
}
|
||||
|
||||
private function findVisitorByConfigId($configId, $select, $from, $where, $bindSql)
|
||||
{
|
||||
// will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time)
|
||||
$where .= ' AND config_id = ?';
|
||||
$bindSql[] = $configId;
|
||||
|
||||
return $this->fetchVisitor($select, $from, $where, $bindSql);
|
||||
}
|
||||
|
||||
private function fetchVisitor($select, $from, $where, $bindSql)
|
||||
{
|
||||
$sql = "$select $from WHERE " . $where . "
|
||||
ORDER BY visit_last_action_time DESC
|
||||
LIMIT 1";
|
||||
|
||||
$visitRow = $this->getDb()->fetch($sql, $bindSql);
|
||||
|
||||
return $visitRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the site doesn't have raw data.
|
||||
*
|
||||
* @param int $siteId
|
||||
* @return bool
|
||||
*/
|
||||
public function isSiteEmpty($siteId)
|
||||
{
|
||||
$sql = sprintf('SELECT idsite FROM %s WHERE idsite = ? limit 1', Common::prefixTable('log_visit'));
|
||||
|
||||
$result = \Piwik\Db::fetchOne($sql, array($siteId));
|
||||
|
||||
return $result == null;
|
||||
}
|
||||
|
||||
private function fieldsToQuery($valuesToUpdate)
|
||||
{
|
||||
$updateParts = array();
|
||||
$sqlBind = array();
|
||||
|
||||
foreach ($valuesToUpdate as $name => $value) {
|
||||
// Case where bind parameters don't work
|
||||
if ($value === $name . ' + 1') {
|
||||
//$name = 'visit_total_events'
|
||||
//$value = 'visit_total_events + 1';
|
||||
$updateParts[] = " $name = $value ";
|
||||
} else {
|
||||
$updateParts[] = $name . " = ?";
|
||||
$sqlBind[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return array($updateParts, $sqlBind);
|
||||
}
|
||||
|
||||
private function deleteDuplicateAction($newActionId)
|
||||
{
|
||||
$sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?";
|
||||
|
||||
$db = $this->getDb();
|
||||
$db->query($sql, array($newActionId));
|
||||
}
|
||||
|
||||
private function getDb()
|
||||
{
|
||||
return Tracker::getDatabase();
|
||||
}
|
||||
|
||||
private function getSqlConditionToMatchSingleAction()
|
||||
{
|
||||
return "( hash = CRC32(?) AND name = ? AND type = ? )";
|
||||
}
|
||||
}
|
385
msd2/tracking/piwik/core/Tracker/PageUrl.php
Normal file
385
msd2/tracking/piwik/core/Tracker/PageUrl.php
Normal file
@ -0,0 +1,385 @@
|
||||
<?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\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\UrlHelper;
|
||||
|
||||
class PageUrl
|
||||
{
|
||||
|
||||
/**
|
||||
* Map URL prefixes to integers.
|
||||
* @see self::normalizeUrl(), self::reconstructNormalizedUrl()
|
||||
*/
|
||||
public static $urlPrefixMap = array(
|
||||
'http://www.' => 1,
|
||||
'http://' => 0,
|
||||
'https://www.' => 3,
|
||||
'https://' => 2
|
||||
);
|
||||
|
||||
/**
|
||||
* Given the Input URL, will exclude all query parameters set for this site
|
||||
*
|
||||
* @static
|
||||
* @param $originalUrl
|
||||
* @param $idSite
|
||||
* @return bool|string Returned URL is HTML entities decoded
|
||||
*/
|
||||
public static function excludeQueryParametersFromUrl($originalUrl, $idSite)
|
||||
{
|
||||
$originalUrl = self::cleanupUrl($originalUrl);
|
||||
|
||||
$parsedUrl = @parse_url($originalUrl);
|
||||
$parsedUrl = self::cleanupHostAndHashTag($parsedUrl, $idSite);
|
||||
$parametersToExclude = self::getQueryParametersToExclude($idSite);
|
||||
|
||||
if (empty($parsedUrl['query'])) {
|
||||
if (empty($parsedUrl['fragment'])) {
|
||||
return UrlHelper::getParseUrlReverse($parsedUrl);
|
||||
}
|
||||
|
||||
// Exclude from the hash tag as well
|
||||
$queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['fragment']);
|
||||
$parsedUrl['fragment'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude);
|
||||
$url = UrlHelper::getParseUrlReverse($parsedUrl);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
$queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['query']);
|
||||
$parsedUrl['query'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude);
|
||||
$url = UrlHelper::getParseUrlReverse($parsedUrl);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of parameters names that must be excluded from the Query String in all tracked URLs
|
||||
* @static
|
||||
* @param $idSite
|
||||
* @return array
|
||||
*/
|
||||
public static function getQueryParametersToExclude($idSite)
|
||||
{
|
||||
$campaignTrackingParameters = Common::getCampaignParameters();
|
||||
|
||||
$campaignTrackingParameters = array_merge(
|
||||
$campaignTrackingParameters[0], // campaign name parameters
|
||||
$campaignTrackingParameters[1] // campaign keyword parameters
|
||||
);
|
||||
|
||||
$website = Cache::getCacheWebsiteAttributes($idSite);
|
||||
$excludedParameters = self::getExcludedParametersFromWebsite($website);
|
||||
|
||||
$parametersToExclude = array_merge($excludedParameters,
|
||||
self::getUrlParameterNamesToExcludeFromUrl(),
|
||||
$campaignTrackingParameters);
|
||||
|
||||
/**
|
||||
* Triggered before setting the action url in Piwik\Tracker\Action so plugins can register
|
||||
* parameters to be excluded from the tracking URL (e.g. campaign parameters).
|
||||
*
|
||||
* @param array &$parametersToExclude An array of parameters to exclude from the tracking url.
|
||||
*/
|
||||
Piwik::postEvent('Tracker.PageUrl.getQueryParametersToExclude', array(&$parametersToExclude));
|
||||
|
||||
if (!empty($parametersToExclude)) {
|
||||
Common::printDebug('Excluding parameters "' . implode(',', $parametersToExclude) . '" from URL');
|
||||
}
|
||||
|
||||
$parametersToExclude = array_map('strtolower', $parametersToExclude);
|
||||
return $parametersToExclude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of URL query parameters that should be removed from the tracked URL query string.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function getUrlParameterNamesToExcludeFromUrl()
|
||||
{
|
||||
$paramsToExclude = Config::getInstance()->Tracker['url_query_parameter_to_exclude_from_url'];
|
||||
$paramsToExclude = explode(",", $paramsToExclude);
|
||||
$paramsToExclude = array_map('trim', $paramsToExclude);
|
||||
return $paramsToExclude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if URL fragments should be removed for a specific site,
|
||||
* false if otherwise.
|
||||
*
|
||||
* This function uses the Tracker cache and not the MySQL database.
|
||||
*
|
||||
* @param $idSite int The ID of the site to check for.
|
||||
* @return bool
|
||||
*/
|
||||
public static function shouldRemoveURLFragmentFor($idSite)
|
||||
{
|
||||
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
|
||||
return empty($websiteAttributes['keep_url_fragment']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans and/or removes the URL fragment of a URL.
|
||||
*
|
||||
* @param $urlFragment string The URL fragment to process.
|
||||
* @param $idSite int|bool If not false, this function will check if URL fragments
|
||||
* should be removed for the site w/ this ID and if so,
|
||||
* the returned processed fragment will be empty.
|
||||
*
|
||||
* @return string The processed URL fragment.
|
||||
*/
|
||||
public static function processUrlFragment($urlFragment, $idSite = false)
|
||||
{
|
||||
// if we should discard the url fragment for this site, return an empty string as
|
||||
// the processed url fragment
|
||||
if ($idSite !== false
|
||||
&& PageUrl::shouldRemoveURLFragmentFor($idSite)
|
||||
) {
|
||||
return '';
|
||||
} else {
|
||||
// Remove trailing Hash tag in ?query#hash#
|
||||
if (substr($urlFragment, -1) == '#') {
|
||||
$urlFragment = substr($urlFragment, 0, strlen($urlFragment) - 1);
|
||||
}
|
||||
return $urlFragment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will cleanup the hostname (some browser do not strolower the hostname),
|
||||
* and deal ith the hash tag on incoming URLs based on website setting.
|
||||
*
|
||||
* @param $parsedUrl
|
||||
* @param $idSite int|bool The site ID of the current visit. This parameter is
|
||||
* only used by the tracker to see if we should remove
|
||||
* the URL fragment for this site.
|
||||
* @return array
|
||||
*/
|
||||
protected static function cleanupHostAndHashTag($parsedUrl, $idSite = false)
|
||||
{
|
||||
if (empty($parsedUrl)) {
|
||||
return $parsedUrl;
|
||||
}
|
||||
|
||||
if (!empty($parsedUrl['host'])) {
|
||||
$parsedUrl['host'] = Common::mb_strtolower($parsedUrl['host']);
|
||||
}
|
||||
|
||||
if (!empty($parsedUrl['fragment'])) {
|
||||
$parsedUrl['fragment'] = PageUrl::processUrlFragment($parsedUrl['fragment'], $idSite);
|
||||
}
|
||||
|
||||
return $parsedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Matrix URL format
|
||||
* from http://example.org/thing;paramA=1;paramB=6542
|
||||
* to http://example.org/thing?paramA=1¶mB=6542
|
||||
*
|
||||
* @param string $originalUrl
|
||||
* @return string
|
||||
*/
|
||||
public static function convertMatrixUrl($originalUrl)
|
||||
{
|
||||
$posFirstSemiColon = strpos($originalUrl, ";");
|
||||
|
||||
if (false === $posFirstSemiColon) {
|
||||
return $originalUrl;
|
||||
}
|
||||
|
||||
$posQuestionMark = strpos($originalUrl, "?");
|
||||
$replace = (false === $posQuestionMark);
|
||||
|
||||
if ($posQuestionMark > $posFirstSemiColon) {
|
||||
$originalUrl = substr_replace($originalUrl, ";", $posQuestionMark, 1);
|
||||
$replace = true;
|
||||
}
|
||||
|
||||
if ($replace) {
|
||||
$originalUrl = substr_replace($originalUrl, "?", strpos($originalUrl, ";"), 1);
|
||||
$originalUrl = str_replace(";", "&", $originalUrl);
|
||||
}
|
||||
|
||||
return $originalUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up string contents (filter, truncate, ...)
|
||||
*
|
||||
* @param string $string Dirty string
|
||||
* @return string
|
||||
*/
|
||||
public static function cleanupString($string)
|
||||
{
|
||||
$string = trim($string);
|
||||
$string = str_replace(array("\n", "\r", "\0"), '', $string);
|
||||
|
||||
$limit = Config::getInstance()->Tracker['page_maximum_length'];
|
||||
$clean = substr($string, 0, $limit);
|
||||
return $clean;
|
||||
}
|
||||
|
||||
protected static function reencodeParameterValue($value, $encoding)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$decoded = urldecode($value);
|
||||
if (function_exists('mb_check_encoding')
|
||||
&& @mb_check_encoding($decoded, $encoding)) {
|
||||
$value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding));
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected static function reencodeParametersArray($queryParameters, $encoding)
|
||||
{
|
||||
foreach ($queryParameters as &$value) {
|
||||
if (is_array($value)) {
|
||||
$value = self::reencodeParametersArray($value, $encoding);
|
||||
} else {
|
||||
$value = PageUrl::reencodeParameterValue($value, $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
return $queryParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if query parameters are of a non-UTF-8 encoding and converts the values
|
||||
* from the specified encoding to UTF-8.
|
||||
* This method is used to workaround browser/webapp bugs (see #3450). When
|
||||
* browsers fail to encode query parameters in UTF-8, the tracker will send the
|
||||
* charset of the page viewed and we can sometimes work around invalid data
|
||||
* being stored.
|
||||
*
|
||||
* @param array $queryParameters Name/value mapping of query parameters.
|
||||
* @param bool|string $encoding of the HTML page the URL is for. Used to workaround
|
||||
* browser bugs & mis-coded webapps. See #3450.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function reencodeParameters(&$queryParameters, $encoding = false)
|
||||
{
|
||||
if (function_exists('mb_check_encoding')) {
|
||||
// if query params are encoded w/ non-utf8 characters (due to browser bug or whatever),
|
||||
// encode to UTF-8.
|
||||
if (strtolower($encoding) != 'utf-8'
|
||||
&& $encoding != false
|
||||
) {
|
||||
Common::printDebug("Encoding page URL query parameters to $encoding.");
|
||||
|
||||
$queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding);
|
||||
}
|
||||
} else {
|
||||
Common::printDebug("Page charset supplied in tracking request, but mbstring extension is not available.");
|
||||
}
|
||||
|
||||
return $queryParameters;
|
||||
}
|
||||
|
||||
public static function cleanupUrl($url)
|
||||
{
|
||||
$url = Common::unsanitizeInputValue($url);
|
||||
$url = PageUrl::cleanupString($url);
|
||||
$url = PageUrl::convertMatrixUrl($url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL from the prefix ID and the rest.
|
||||
*
|
||||
* @param string $url
|
||||
* @param integer $prefixId
|
||||
* @return string
|
||||
*/
|
||||
public static function reconstructNormalizedUrl($url, $prefixId)
|
||||
{
|
||||
$map = array_flip(self::$urlPrefixMap);
|
||||
|
||||
if ($prefixId !== null && isset($map[$prefixId])) {
|
||||
$fullUrl = $map[$prefixId] . $url;
|
||||
} else {
|
||||
$fullUrl = $url;
|
||||
}
|
||||
|
||||
// Clean up host & hash tags, for URLs
|
||||
$parsedUrl = @parse_url($fullUrl);
|
||||
$parsedUrl = PageUrl::cleanupHostAndHashTag($parsedUrl);
|
||||
$url = UrlHelper::getParseUrlReverse($parsedUrl);
|
||||
|
||||
if (!empty($url)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return $fullUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the prefix from a URL.
|
||||
* Return the prefix ID and the rest.
|
||||
*
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeUrl($url)
|
||||
{
|
||||
foreach (self::$urlPrefixMap as $prefix => $id) {
|
||||
if (strtolower(substr($url, 0, strlen($prefix))) == $prefix) {
|
||||
return array(
|
||||
'url' => substr($url, strlen($prefix)),
|
||||
'prefixId' => $id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array('url' => $url, 'prefixId' => null);
|
||||
}
|
||||
|
||||
public static function getUrlIfLookValid($url)
|
||||
{
|
||||
$url = PageUrl::cleanupString($url);
|
||||
|
||||
if (!UrlHelper::isLookLikeUrl($url)) {
|
||||
Common::printDebug("WARNING: URL looks invalid and is discarded");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private static function getExcludedParametersFromWebsite($website)
|
||||
{
|
||||
if (isset($website['excluded_parameters'])) {
|
||||
return $website['excluded_parameters'];
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public static function urldecodeValidUtf8($value)
|
||||
{
|
||||
$value = urldecode($value);
|
||||
if (function_exists('mb_check_encoding')
|
||||
&& !@mb_check_encoding($value, 'utf-8')
|
||||
) {
|
||||
return urlencode($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
935
msd2/tracking/piwik/core/Tracker/Request.php
Normal file
935
msd2/tracking/piwik/core/Tracker/Request.php
Normal file
@ -0,0 +1,935 @@
|
||||
<?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 Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Container\StaticContainer;
|
||||
use Piwik\Cookie;
|
||||
use Piwik\Exception\InvalidRequestParameterException;
|
||||
use Piwik\Exception\UnexpectedWebsiteFoundException;
|
||||
use Piwik\IP;
|
||||
use Piwik\Network\IPUtils;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugins\CustomVariables\CustomVariables;
|
||||
use Piwik\Plugins\UsersManager\UsersManager;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\Cache as PiwikCache;
|
||||
|
||||
/**
|
||||
* The Request object holding the http parameters for this tracking request. Use getParam() to fetch a named parameter.
|
||||
*
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
private $cdtCache;
|
||||
private $idSiteCache;
|
||||
private $paramsCache = array();
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params;
|
||||
protected $rawParams;
|
||||
|
||||
protected $isAuthenticated = null;
|
||||
private $isEmptyRequest = false;
|
||||
|
||||
protected $tokenAuth;
|
||||
|
||||
/**
|
||||
* Stores plugin specific tracking request metadata. RequestProcessors can store
|
||||
* whatever they want in this array, and other RequestProcessors can modify these
|
||||
* values to change tracker behavior.
|
||||
*
|
||||
* @var string[][]
|
||||
*/
|
||||
private $requestMetadata = array();
|
||||
|
||||
const UNKNOWN_RESOLUTION = 'unknown';
|
||||
|
||||
private $customTimestampDoesNotRequireTokenauthWhenNewerThan;
|
||||
|
||||
/**
|
||||
* @param $params
|
||||
* @param bool|string $tokenAuth
|
||||
*/
|
||||
public function __construct($params, $tokenAuth = false)
|
||||
{
|
||||
if (!is_array($params)) {
|
||||
$params = array();
|
||||
}
|
||||
$this->params = $params;
|
||||
$this->rawParams = $params;
|
||||
$this->tokenAuth = $tokenAuth;
|
||||
$this->timestamp = time();
|
||||
$this->isEmptyRequest = empty($params);
|
||||
$this->customTimestampDoesNotRequireTokenauthWhenNewerThan = (int) TrackerConfig::getConfigValue('tracking_requests_require_authentication_when_custom_timestamp_newer_than');
|
||||
|
||||
// When the 'url' and referrer url parameter are not given, we might be in the 'Simple Image Tracker' mode.
|
||||
// The URL can default to the Referrer, which will be in this case
|
||||
// the URL of the page containing the Simple Image beacon
|
||||
if (empty($this->params['urlref'])
|
||||
&& empty($this->params['url'])
|
||||
&& array_key_exists('HTTP_REFERER', $_SERVER)
|
||||
) {
|
||||
$url = $_SERVER['HTTP_REFERER'];
|
||||
if (!empty($url)) {
|
||||
$this->params['url'] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
// check for 4byte utf8 characters in all tracking params and replace them with <20>
|
||||
// @TODO Remove as soon as our database tables use utf8mb4 instead of utf8
|
||||
$this->params = $this->replaceUnsupportedUtf8Chars($this->params);
|
||||
}
|
||||
|
||||
protected function replaceUnsupportedUtf8Chars($value, $key=false)
|
||||
{
|
||||
if (is_string($value) && preg_match('/[\x{10000}-\x{10FFFF}]/u', $value)) {
|
||||
Common::printDebug("Unsupport character detected in $key. Replacing with \xEF\xBF\xBD");
|
||||
return preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
array_walk_recursive ($value, function(&$value, $key){
|
||||
$value = $this->replaceUnsupportedUtf8Chars($value, $key);
|
||||
});
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the params that were originally passed to the instance. These params do not contain any params that were added
|
||||
* within this object.
|
||||
* @return array
|
||||
*/
|
||||
public function getRawParams()
|
||||
{
|
||||
return $this->rawParams;
|
||||
}
|
||||
|
||||
public function getTokenAuth()
|
||||
{
|
||||
return $this->tokenAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthenticated()
|
||||
{
|
||||
if (is_null($this->isAuthenticated)) {
|
||||
$this->authenticateTrackingApi($this->tokenAuth);
|
||||
}
|
||||
|
||||
return $this->isAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows to set custom IP + server time + visitor ID, when using Tracking API.
|
||||
* These two attributes can be only set by the Super User (passing token_auth).
|
||||
*/
|
||||
protected function authenticateTrackingApi($tokenAuth)
|
||||
{
|
||||
$shouldAuthenticate = TrackerConfig::getConfigValue('tracking_requests_require_authentication');
|
||||
|
||||
if ($shouldAuthenticate) {
|
||||
try {
|
||||
$idSite = $this->getIdSite();
|
||||
} catch (Exception $e) {
|
||||
Common::printDebug("failed to authenticate: invalid idSite");
|
||||
$this->isAuthenticated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($tokenAuth)) {
|
||||
$tokenAuth = Common::getRequestVar('token_auth', false, 'string', $this->params);
|
||||
}
|
||||
|
||||
$cache = PiwikCache::getTransientCache();
|
||||
$cacheKey = 'tracker_request_authentication_' . $idSite . '_' . $tokenAuth;
|
||||
|
||||
if ($cache->contains($cacheKey)) {
|
||||
Common::printDebug("token_auth is authenticated in cache!");
|
||||
$this->isAuthenticated = $cache->fetch($cacheKey);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->isAuthenticated = self::authenticateSuperUserOrAdminOrWrite($tokenAuth, $idSite);
|
||||
$cache->save($cacheKey, $this->isAuthenticated);
|
||||
} catch (Exception $e) {
|
||||
Common::printDebug("could not authenticate, caught exception: " . $e->getMessage());
|
||||
|
||||
$this->isAuthenticated = false;
|
||||
}
|
||||
|
||||
if ($this->isAuthenticated) {
|
||||
Common::printDebug("token_auth is authenticated!");
|
||||
} else {
|
||||
StaticContainer::get('Piwik\Tracker\Failures')->logFailure(Failures::FAILURE_ID_NOT_AUTHENTICATED, $this);
|
||||
}
|
||||
} else {
|
||||
$this->isAuthenticated = true;
|
||||
Common::printDebug("token_auth authentication not required");
|
||||
}
|
||||
}
|
||||
|
||||
public static function authenticateSuperUserOrAdminOrWrite($tokenAuth, $idSite)
|
||||
{
|
||||
if (empty($tokenAuth)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Piwik::postEvent('Request.initAuthenticationObject');
|
||||
|
||||
/** @var \Piwik\Auth $auth */
|
||||
$auth = StaticContainer::get('Piwik\Auth');
|
||||
$auth->setTokenAuth($tokenAuth);
|
||||
$auth->setLogin(null);
|
||||
$auth->setPassword(null);
|
||||
$auth->setPasswordHash(null);
|
||||
$access = $auth->authenticate();
|
||||
|
||||
if (!empty($access) && $access->hasSuperUserAccess()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now checking the list of admin token_auth cached in the Tracker config file
|
||||
if (!empty($idSite) && $idSite > 0) {
|
||||
$website = Cache::getCacheWebsiteAttributes($idSite);
|
||||
$hashedToken = UsersManager::hashTrackingToken((string) $tokenAuth, $idSite);
|
||||
|
||||
if (array_key_exists('tracking_token_auth', $website)
|
||||
&& in_array($hashedToken, $website['tracking_token_auth'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Common::printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin / Write was NOT authenticated");
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @internal
|
||||
*/
|
||||
Piwik::postEvent('Tracker.Request.authenticate.failed');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int
|
||||
*/
|
||||
public function getDaysSinceFirstVisit()
|
||||
{
|
||||
$cookieFirstVisitTimestamp = $this->getParam('_idts');
|
||||
|
||||
if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) {
|
||||
$cookieFirstVisitTimestamp = $this->getCurrentTimestamp();
|
||||
}
|
||||
|
||||
$daysSinceFirstVisit = floor(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400);
|
||||
|
||||
if ($daysSinceFirstVisit < 0) {
|
||||
$daysSinceFirstVisit = 0;
|
||||
}
|
||||
|
||||
return $daysSinceFirstVisit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|float|int
|
||||
*/
|
||||
public function getDaysSinceLastOrder()
|
||||
{
|
||||
$daysSinceLastOrder = false;
|
||||
$lastOrderTimestamp = $this->getParam('_ects');
|
||||
|
||||
if ($this->isTimestampValid($lastOrderTimestamp)) {
|
||||
$daysSinceLastOrder = round(($this->getCurrentTimestamp() - $lastOrderTimestamp) / 86400, $precision = 0);
|
||||
if ($daysSinceLastOrder < 0) {
|
||||
$daysSinceLastOrder = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $daysSinceLastOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int
|
||||
*/
|
||||
public function getDaysSinceLastVisit()
|
||||
{
|
||||
$daysSinceLastVisit = 0;
|
||||
$lastVisitTimestamp = $this->getParam('_viewts');
|
||||
|
||||
if ($this->isTimestampValid($lastVisitTimestamp)) {
|
||||
$daysSinceLastVisit = round(($this->getCurrentTimestamp() - $lastVisitTimestamp) / 86400, $precision = 0);
|
||||
if ($daysSinceLastVisit < 0) {
|
||||
$daysSinceLastVisit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $daysSinceLastVisit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getVisitCount()
|
||||
{
|
||||
$visitCount = $this->getParam('_idvc');
|
||||
if ($visitCount < 1) {
|
||||
$visitCount = 1;
|
||||
}
|
||||
return $visitCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language the visitor is viewing.
|
||||
*
|
||||
* @return string browser language code, eg. "en-gb,en;q=0.5"
|
||||
*/
|
||||
public function getBrowserLanguage()
|
||||
{
|
||||
return Common::getRequestVar('lang', Common::getBrowserLanguage(), 'string', $this->params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalTime()
|
||||
{
|
||||
$localTimes = array(
|
||||
'h' => (string)Common::getRequestVar('h', $this->getCurrentDate("H"), 'int', $this->params),
|
||||
'i' => (string)Common::getRequestVar('m', $this->getCurrentDate("i"), 'int', $this->params),
|
||||
's' => (string)Common::getRequestVar('s', $this->getCurrentDate("s"), 'int', $this->params)
|
||||
);
|
||||
if($localTimes['h'] < 0 || $localTimes['h'] > 23) {
|
||||
$localTimes['h'] = 0;
|
||||
}
|
||||
if($localTimes['i'] < 0 || $localTimes['i'] > 59) {
|
||||
$localTimes['i'] = 0;
|
||||
}
|
||||
if($localTimes['s'] < 0 || $localTimes['s'] > 59) {
|
||||
$localTimes['s'] = 0;
|
||||
}
|
||||
foreach ($localTimes as $k => $time) {
|
||||
if (strlen($time) == 1) {
|
||||
$localTimes[$k] = '0' . $time;
|
||||
}
|
||||
}
|
||||
$localTime = $localTimes['h'] . ':' . $localTimes['i'] . ':' . $localTimes['s'];
|
||||
return $localTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current date in the "Y-m-d" PHP format
|
||||
*
|
||||
* @param string $format
|
||||
* @return string
|
||||
*/
|
||||
protected function getCurrentDate($format = "Y-m-d")
|
||||
{
|
||||
return date($format, $this->getCurrentTimestamp());
|
||||
}
|
||||
|
||||
public function getGoalRevenue($defaultGoalRevenue)
|
||||
{
|
||||
return Common::getRequestVar('revenue', $defaultGoalRevenue, 'float', $this->params);
|
||||
}
|
||||
|
||||
public function getParam($name)
|
||||
{
|
||||
static $supportedParams = array(
|
||||
// Name => array( defaultValue, type )
|
||||
'_refts' => array(0, 'int'),
|
||||
'_ref' => array('', 'string'),
|
||||
'_rcn' => array('', 'string'),
|
||||
'_rck' => array('', 'string'),
|
||||
'_idts' => array(0, 'int'),
|
||||
'_viewts' => array(0, 'int'),
|
||||
'_ects' => array(0, 'int'),
|
||||
'_idvc' => array(1, 'int'),
|
||||
'url' => array('', 'string'),
|
||||
'urlref' => array('', 'string'),
|
||||
'res' => array(self::UNKNOWN_RESOLUTION, 'string'),
|
||||
'idgoal' => array(-1, 'int'),
|
||||
'ping' => array(0, 'int'),
|
||||
|
||||
// other
|
||||
'bots' => array(0, 'int'),
|
||||
'dp' => array(0, 'int'),
|
||||
'rec' => array(0, 'int'),
|
||||
'new_visit' => array(0, 'int'),
|
||||
|
||||
// Ecommerce
|
||||
'ec_id' => array('', 'string'),
|
||||
'ec_st' => array(false, 'float'),
|
||||
'ec_tx' => array(false, 'float'),
|
||||
'ec_sh' => array(false, 'float'),
|
||||
'ec_dt' => array(false, 'float'),
|
||||
'ec_items' => array('', 'json'),
|
||||
|
||||
// Events
|
||||
'e_c' => array('', 'string'),
|
||||
'e_a' => array('', 'string'),
|
||||
'e_n' => array('', 'string'),
|
||||
'e_v' => array(false, 'float'),
|
||||
|
||||
// some visitor attributes can be overwritten
|
||||
'cip' => array('', 'string'),
|
||||
'cdt' => array('', 'string'),
|
||||
'cid' => array('', 'string'),
|
||||
'uid' => array('', 'string'),
|
||||
|
||||
// Actions / pages
|
||||
'cs' => array('', 'string'),
|
||||
'download' => array('', 'string'),
|
||||
'link' => array('', 'string'),
|
||||
'action_name' => array('', 'string'),
|
||||
'search' => array('', 'string'),
|
||||
'search_cat' => array('', 'string'),
|
||||
'pv_id' => array('', 'string'),
|
||||
'search_count' => array(-1, 'int'),
|
||||
'gt_ms' => array(-1, 'int'),
|
||||
|
||||
// Content
|
||||
'c_p' => array('', 'string'),
|
||||
'c_n' => array('', 'string'),
|
||||
'c_t' => array('', 'string'),
|
||||
'c_i' => array('', 'string'),
|
||||
);
|
||||
|
||||
if (isset($this->paramsCache[$name])) {
|
||||
return $this->paramsCache[$name];
|
||||
}
|
||||
|
||||
if (!isset($supportedParams[$name])) {
|
||||
throw new Exception("Requested parameter $name is not a known Tracking API Parameter.");
|
||||
}
|
||||
|
||||
$paramDefaultValue = $supportedParams[$name][0];
|
||||
$paramType = $supportedParams[$name][1];
|
||||
|
||||
if ($this->hasParam($name)) {
|
||||
$this->paramsCache[$name] = $this->replaceUnsupportedUtf8Chars(Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params), $name);
|
||||
} else {
|
||||
$this->paramsCache[$name] = $paramDefaultValue;
|
||||
}
|
||||
|
||||
return $this->paramsCache[$name];
|
||||
}
|
||||
|
||||
public function setParam($name, $value)
|
||||
{
|
||||
$this->params[$name] = $value;
|
||||
unset($this->paramsCache[$name]);
|
||||
|
||||
if ($name === 'cdt') {
|
||||
$this->cdtCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function hasParam($name)
|
||||
{
|
||||
return isset($this->params[$name]);
|
||||
}
|
||||
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
public function getCurrentTimestamp()
|
||||
{
|
||||
if (!isset($this->cdtCache)) {
|
||||
$this->cdtCache = $this->getCustomTimestamp();
|
||||
}
|
||||
|
||||
if (!empty($this->cdtCache)) {
|
||||
return $this->cdtCache;
|
||||
}
|
||||
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function setCurrentTimestamp($timestamp)
|
||||
{
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
protected function getCustomTimestamp()
|
||||
{
|
||||
if (!$this->hasParam('cdt')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cdt = $this->getParam('cdt');
|
||||
|
||||
if (empty($cdt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_numeric($cdt)) {
|
||||
$cdt = strtotime($cdt);
|
||||
}
|
||||
|
||||
if (!$this->isTimestampValid($cdt, $this->timestamp)) {
|
||||
Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If timestamp in the past, token_auth is required
|
||||
$timeFromNow = $this->timestamp - $cdt;
|
||||
$isTimestampRecent = $timeFromNow < $this->customTimestampDoesNotRequireTokenauthWhenNewerThan;
|
||||
|
||||
if (!$isTimestampRecent) {
|
||||
if (!$this->isAuthenticated()) {
|
||||
$message = sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow);
|
||||
Common::printDebug($message);
|
||||
Common::printDebug("WARN: Tracker API 'cdt' was used with invalid token_auth");
|
||||
throw new InvalidRequestParameterException($message);
|
||||
}
|
||||
}
|
||||
|
||||
return $cdt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the timestamp is valid ie. timestamp is sometime in the last 10 years and is not in the future.
|
||||
*
|
||||
* @param $time int Timestamp to test
|
||||
* @param $now int Current timestamp
|
||||
* @return bool
|
||||
*/
|
||||
protected function isTimestampValid($time, $now = null)
|
||||
{
|
||||
if (empty($now)) {
|
||||
$now = $this->getCurrentTimestamp();
|
||||
}
|
||||
|
||||
return $time <= $now
|
||||
&& $time > $now - 20 * 365 * 86400;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @ignore
|
||||
*/
|
||||
public function getIdSiteUnverified()
|
||||
{
|
||||
$idSite = Common::getRequestVar('idsite', 0, 'int', $this->params);
|
||||
|
||||
/**
|
||||
* Triggered when obtaining the ID of the site we are tracking a visit for.
|
||||
*
|
||||
* This event can be used to change the site ID so data is tracked for a different
|
||||
* website.
|
||||
*
|
||||
* @param int &$idSite Initialized to the value of the **idsite** query parameter. If a
|
||||
* subscriber sets this variable, the value it uses must be greater
|
||||
* than 0.
|
||||
* @param array $params The entire array of request parameters in the current tracking
|
||||
* request.
|
||||
*/
|
||||
Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params));
|
||||
return $idSite;
|
||||
}
|
||||
|
||||
public function getIdSite()
|
||||
{
|
||||
if (isset($this->idSiteCache)) {
|
||||
return $this->idSiteCache;
|
||||
}
|
||||
|
||||
$idSite = $this->getIdSiteUnverified();
|
||||
|
||||
if ($idSite <= 0) {
|
||||
throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
|
||||
}
|
||||
|
||||
// check site actually exists, should throw UnexpectedWebsiteFoundException directly
|
||||
$site = Cache::getCacheWebsiteAttributes($idSite);
|
||||
|
||||
if (empty($site)) {
|
||||
// fallback just in case exception wasn't thrown...
|
||||
throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
|
||||
}
|
||||
|
||||
$this->idSiteCache = $idSite;
|
||||
|
||||
return $idSite;
|
||||
}
|
||||
|
||||
public function getUserAgent()
|
||||
{
|
||||
$default = false;
|
||||
|
||||
if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
|
||||
$default = $_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
return Common::getRequestVar('ua', $default, 'string', $this->params);
|
||||
}
|
||||
|
||||
public function getCustomVariablesInVisitScope()
|
||||
{
|
||||
return $this->getCustomVariables('visit');
|
||||
}
|
||||
|
||||
public function getCustomVariablesInPageScope()
|
||||
{
|
||||
return $this->getCustomVariables('page');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Piwik 2.10.0. Use Request::getCustomVariablesInPageScope() or Request::getCustomVariablesInVisitScope() instead.
|
||||
* When we "remove" this method we will only set visibility to "private" and pass $parameter = _cvar|cvar as an argument instead of $scope
|
||||
*/
|
||||
public function getCustomVariables($scope)
|
||||
{
|
||||
if ($scope == 'visit') {
|
||||
$parameter = '_cvar';
|
||||
} else {
|
||||
$parameter = 'cvar';
|
||||
}
|
||||
|
||||
$cvar = Common::getRequestVar($parameter, '', 'json', $this->params);
|
||||
$customVar = Common::unsanitizeInputValues($cvar);
|
||||
|
||||
if (!is_array($customVar)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$customVariables = array();
|
||||
$maxCustomVars = CustomVariables::getNumUsableCustomVariables();
|
||||
|
||||
foreach ($customVar as $id => $keyValue) {
|
||||
$id = (int)$id;
|
||||
|
||||
if ($id < 1
|
||||
|| $id > $maxCustomVars
|
||||
|| count($keyValue) != 2
|
||||
|| (!is_string($keyValue[0]) && !is_numeric($keyValue[0]))
|
||||
) {
|
||||
Common::printDebug("Invalid custom variables detected (id=$id)");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strlen($keyValue[1]) == 0) {
|
||||
$keyValue[1] = "";
|
||||
}
|
||||
// We keep in the URL when Custom Variable have empty names
|
||||
// and values, as it means they can be deleted server side
|
||||
|
||||
$customVariables['custom_var_k' . $id] = self::truncateCustomVariable($keyValue[0]);
|
||||
$customVariables['custom_var_v' . $id] = self::truncateCustomVariable($keyValue[1]);
|
||||
}
|
||||
|
||||
return $customVariables;
|
||||
}
|
||||
|
||||
public static function truncateCustomVariable($input)
|
||||
{
|
||||
return substr(trim($input), 0, CustomVariables::getMaxLengthCustomVariables());
|
||||
}
|
||||
|
||||
protected function shouldUseThirdPartyCookie()
|
||||
{
|
||||
return (bool)Config::getInstance()->Tracker['use_third_party_id_cookie'];
|
||||
}
|
||||
|
||||
public function getThirdPartyCookieVisitorId()
|
||||
{
|
||||
$cookie = $this->makeThirdPartyCookieUID();
|
||||
$idVisitor = $cookie->get(0);
|
||||
if ($idVisitor !== false
|
||||
&& strlen($idVisitor) == Tracker::LENGTH_HEX_ID_STRING
|
||||
) {
|
||||
return $idVisitor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cookie information.
|
||||
*/
|
||||
public function setThirdPartyCookie($idVisitor)
|
||||
{
|
||||
if (!$this->shouldUseThirdPartyCookie()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cookie = $this->makeThirdPartyCookieUID();
|
||||
$idVisitor = bin2hex($idVisitor);
|
||||
$cookie->set(0, $idVisitor);
|
||||
$cookie->save();
|
||||
|
||||
Common::printDebug(sprintf("We set the visitor ID to %s in the 3rd party cookie...", $idVisitor));
|
||||
}
|
||||
|
||||
protected function makeThirdPartyCookieUID()
|
||||
{
|
||||
$cookie = new Cookie(
|
||||
$this->getCookieName(),
|
||||
$this->getCookieExpire(),
|
||||
$this->getCookiePath());
|
||||
|
||||
$domain = $this->getCookieDomain();
|
||||
if (!empty($domain)) {
|
||||
$cookie->setDomain($domain);
|
||||
}
|
||||
|
||||
Common::printDebug($cookie);
|
||||
|
||||
return $cookie;
|
||||
}
|
||||
|
||||
protected function getCookieName()
|
||||
{
|
||||
return TrackerConfig::getConfigValue('cookie_name');
|
||||
}
|
||||
|
||||
protected function getCookieExpire()
|
||||
{
|
||||
return $this->getCurrentTimestamp() + TrackerConfig::getConfigValue('cookie_expire');
|
||||
}
|
||||
|
||||
protected function getCookiePath()
|
||||
{
|
||||
return TrackerConfig::getConfigValue('cookie_path');
|
||||
}
|
||||
|
||||
protected function getCookieDomain()
|
||||
{
|
||||
return TrackerConfig::getConfigValue('cookie_domain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID from the request in this order:
|
||||
* return from a given User ID,
|
||||
* or from a Tracking API forced Visitor ID,
|
||||
* or from a Visitor ID from 3rd party (optional) cookies,
|
||||
* or from a given Visitor Id from 1st party?
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getVisitorId()
|
||||
{
|
||||
$found = false;
|
||||
|
||||
// If User ID is set it takes precedence
|
||||
$userId = $this->getForcedUserId();
|
||||
if ($userId) {
|
||||
$userIdHashed = $this->getUserIdHashed($userId);
|
||||
$idVisitor = $this->truncateIdAsVisitorId($userIdHashed);
|
||||
Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = $idVisitor)");
|
||||
$found = true;
|
||||
}
|
||||
|
||||
// Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request?
|
||||
if (!$found) {
|
||||
$idVisitor = $this->getForcedVisitorId();
|
||||
if (!empty($idVisitor)) {
|
||||
if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
|
||||
throw new InvalidRequestParameterException("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
|
||||
}
|
||||
Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
// - If set to use 3rd party cookies for Visit ID, read the cookie
|
||||
if (!$found) {
|
||||
$useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
|
||||
if ($useThirdPartyCookie) {
|
||||
$idVisitor = $this->getThirdPartyCookieVisitorId();
|
||||
if(!empty($idVisitor)) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a third party cookie was not found, we default to the first party cookie
|
||||
if (!$found) {
|
||||
$idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
|
||||
$found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
return $this->getVisitorIdAsBinary($idVisitor);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When creating a third party cookie, we want to ensure that the original value set in this 3rd party cookie
|
||||
* sticks and is not overwritten later.
|
||||
*/
|
||||
public function getVisitorIdForThirdPartyCookie()
|
||||
{
|
||||
$found = false;
|
||||
|
||||
// For 3rd party cookies, priority is on re-using the existing 3rd party cookie value
|
||||
if (!$found) {
|
||||
$useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
|
||||
if ($useThirdPartyCookie) {
|
||||
$idVisitor = $this->getThirdPartyCookieVisitorId();
|
||||
if(!empty($idVisitor)) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a third party cookie was not found, we default to the first party cookie
|
||||
if (!$found) {
|
||||
$idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
|
||||
$found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
return $this->getVisitorIdAsBinary($idVisitor);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function getIp()
|
||||
{
|
||||
return IPUtils::stringToBinaryIP($this->getIpString());
|
||||
}
|
||||
|
||||
public function getForcedUserId()
|
||||
{
|
||||
$userId = $this->getParam('uid');
|
||||
if (strlen($userId) > 0) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getForcedVisitorId()
|
||||
{
|
||||
return $this->getParam('cid');
|
||||
}
|
||||
|
||||
public function getPlugins()
|
||||
{
|
||||
static $pluginsInOrder = array('fla', 'java', 'dir', 'qt', 'realp', 'pdf', 'wma', 'gears', 'ag', 'cookie');
|
||||
$plugins = array();
|
||||
foreach ($pluginsInOrder as $param) {
|
||||
$plugins[] = Common::getRequestVar($param, 0, 'int', $this->params);
|
||||
}
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
public function isEmptyRequest()
|
||||
{
|
||||
return $this->isEmptyRequest;
|
||||
}
|
||||
|
||||
const GENERATION_TIME_MS_MAXIMUM = 3600000; // 1 hour
|
||||
|
||||
public function getPageGenerationTime()
|
||||
{
|
||||
$generationTime = $this->getParam('gt_ms');
|
||||
if ($generationTime > 0
|
||||
&& $generationTime < self::GENERATION_TIME_MS_MAXIMUM
|
||||
) {
|
||||
return (int)$generationTime;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $idVisitor
|
||||
* @return string
|
||||
*/
|
||||
private function truncateIdAsVisitorId($idVisitor)
|
||||
{
|
||||
return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches implementation of PiwikTracker::getUserIdHashed
|
||||
*
|
||||
* @param $userId
|
||||
* @return string
|
||||
*/
|
||||
public function getUserIdHashed($userId)
|
||||
{
|
||||
return substr(sha1($userId), 0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getIpString()
|
||||
{
|
||||
$cip = $this->getParam('cip');
|
||||
|
||||
if (empty($cip)) {
|
||||
return IP::getIpFromHeader();
|
||||
}
|
||||
|
||||
if (!$this->isAuthenticated()) {
|
||||
Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth");
|
||||
return IP::getIpFromHeader();
|
||||
}
|
||||
|
||||
return $cip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a request metadata value.
|
||||
*
|
||||
* @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setMetadata($pluginName, $key, $value)
|
||||
{
|
||||
$this->requestMetadata[$pluginName][$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a request metadata value. Returns `null` if none exists.
|
||||
*
|
||||
* @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($pluginName, $key)
|
||||
{
|
||||
return isset($this->requestMetadata[$pluginName][$key]) ? $this->requestMetadata[$pluginName][$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $idVisitor
|
||||
* @return bool|string
|
||||
*/
|
||||
private function getVisitorIdAsBinary($idVisitor)
|
||||
{
|
||||
$truncated = $this->truncateIdAsVisitorId($idVisitor);
|
||||
$binVisitorId = @Common::hex2bin($truncated);
|
||||
if (!empty($binVisitorId)) {
|
||||
return $binVisitorId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
174
msd2/tracking/piwik/core/Tracker/RequestProcessor.php
Normal file
174
msd2/tracking/piwik/core/Tracker/RequestProcessor.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?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\Tracker\Visit\VisitProperties;
|
||||
|
||||
/**
|
||||
* Base class for all tracker RequestProcessors. RequestProcessors handle and respond to tracking
|
||||
* requests.
|
||||
*
|
||||
* ## Concept: Request Metadata
|
||||
*
|
||||
* RequestProcessors take a Tracker\Request object and based on its information, set request metadata.
|
||||
*
|
||||
* Request metadata is information about the current tracking request, for example, whether
|
||||
* the request is for an existing visit or new visit, or whether the current visitor is a known
|
||||
* visitor, etc. It is used to control tracking behavior.
|
||||
*
|
||||
* Request metadata is shared between RequestProcessors, so RequestProcessors can tweak each others
|
||||
* behavior, and thus, the behavior of the Tracker. Request metadata can be set and get using the
|
||||
* {@link Request::setMetadata()} and {@link Request::getMetadata()}
|
||||
* methods.
|
||||
*
|
||||
* Each RequestProcessor lists the request metadata it computes and exposes in its class
|
||||
* documentation.
|
||||
*
|
||||
* ## The Tracking Process
|
||||
*
|
||||
* When Piwik handles a single tracking request, it gathers all available RequestProcessors and
|
||||
* invokes their methods in sequence.
|
||||
*
|
||||
* The first method called is {@link self::manipulateRequest()}. By default this is a no-op, but
|
||||
* RequestProcessors can use it to manipulate tracker requests before they are processed.
|
||||
*
|
||||
* The second method called is {@link self::processRequestParams()}. RequestProcessors should use
|
||||
* this method to compute request metadata and set visit properties using the tracking request.
|
||||
* An example includes the ActionRequestProcessor, which uses this method to determine the action
|
||||
* being tracked.
|
||||
*
|
||||
* The third method called is {@link self::afterRequestProcessed()}. RequestProcessors should
|
||||
* use this method to either compute request metadata/visit properties using other plugins'
|
||||
* request metadata, OR override other plugins' request metadata to tweak tracker behavior.
|
||||
* An example of the former can be seen in the GoalsRequestProcessor which uses the action
|
||||
* detected by the ActionsRequestProcessor to see if there are any action-matching goal
|
||||
* conversions. An example of the latter can be seen in the PingRequestProcessor, which on
|
||||
* ping requests, aborts conversion recording and new visit recording.
|
||||
*
|
||||
* After these methods are called, either {@link self::onNewVisit()} or {@link self::onExistingVisit()}
|
||||
* is called. Generally, plugins should favor defining Dimension classes instead of using these methods,
|
||||
* however sometimes it is not possible (as is the case with the CustomVariables plugin).
|
||||
*
|
||||
* Finally, the {@link self::recordLogs()} method is called. In this method, RequestProcessors
|
||||
* should use the request metadata that was set (and maybe overridden) to insert whatever log data
|
||||
* they want.
|
||||
*
|
||||
* ## Extending The Piwik Tracker
|
||||
*
|
||||
* Plugins that want to change the tracking process in order to track new data or change how
|
||||
* existing data is tracked can create RequestProcessors to accomplish.
|
||||
*
|
||||
* _Note: If you only want to add tracked data to visits, actions or conversions, you should create
|
||||
* a {@link Dimension} class._
|
||||
*
|
||||
* To create a new RequestProcessor, create a new class that derives from this one, and implement the
|
||||
* methods you need. Then put this class inside the `Tracker` directory of your plugin.
|
||||
*
|
||||
* Final note: RequestProcessors are shared between tracking requests, and so, should ideally be
|
||||
* stateless. They are stored in DI, so they can contain references to other objects in DI, but
|
||||
* they shouldn't contain data that might change between tracking requests.
|
||||
*/
|
||||
abstract class RequestProcessor
|
||||
{
|
||||
/**
|
||||
* This is the first method called when processing a tracker request.
|
||||
*
|
||||
* Derived classes can use this method to manipulate a tracker request before the request
|
||||
* is handled. Plugins could change the URL, add custom variables, etc.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function manipulateRequest(Request $request)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second method called when processing a tracker request.
|
||||
*
|
||||
* Derived classes should use this method to set request metadata based on the tracking
|
||||
* request alone. They should not try to access request metadata from other plugins,
|
||||
* since they may not be set yet.
|
||||
*
|
||||
* When this method is called, `$visitProperties->visitorInfo` will be empty.
|
||||
*
|
||||
* @param VisitProperties $visitProperties
|
||||
* @param Request $request
|
||||
* @return bool If `true` the tracking request will be aborted.
|
||||
*/
|
||||
public function processRequestParams(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the third method called when processing a tracker request.
|
||||
*
|
||||
* Derived classes should use this method to set request metadata that needs request metadata
|
||||
* from other plugins, or to override request metadata from other plugins to change
|
||||
* tracking behavior.
|
||||
*
|
||||
* When this method is called, you can assume all available request metadata from all plugins
|
||||
* will be initialized (but not at their final value). Also, `$visitProperties->visitorInfo`
|
||||
* will contain the values of the visitor's last known visit (if any).
|
||||
*
|
||||
* @param VisitProperties $visitProperties
|
||||
* @param Request $request
|
||||
* @return bool If `true` the tracking request will be aborted.
|
||||
*/
|
||||
public function afterRequestProcessed(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before recording a new visit. You can set/change visit information here
|
||||
* to change what gets inserted into `log_visit`.
|
||||
*
|
||||
* Only implement this method if you cannot use a Dimension for the same thing.
|
||||
*
|
||||
* @param VisitProperties $visitProperties
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onNewVisit(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before updating an existing visit. You can set/change visit information
|
||||
* here to change what gets recorded in `log_visit`.
|
||||
*
|
||||
* Only implement this method if you cannot use a Dimension for the same thing.
|
||||
*
|
||||
* @param array &$valuesToUpdate
|
||||
* @param VisitProperties $visitProperties
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onExistingVisit(&$valuesToUpdate, VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called last. Derived classes should use this method to insert log data. They
|
||||
* should also only read request metadata, and not set it.
|
||||
*
|
||||
* When this method is called, you can assume all request metadata have their final values. Also,
|
||||
* `$visitProperties->visitorInfo` will contain the properties of the visitor's current visit (in
|
||||
* other words, the values in the array were persisted to the DB before this method was called).
|
||||
*
|
||||
* @param VisitProperties $visitProperties
|
||||
* @param Request $request
|
||||
*/
|
||||
public function recordLogs(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
}
|
257
msd2/tracking/piwik/core/Tracker/RequestSet.php
Normal file
257
msd2/tracking/piwik/core/Tracker/RequestSet.php
Normal file
@ -0,0 +1,257 @@
|
||||
<?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\Common;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugins\SitesManager\SiteUrls;
|
||||
use Piwik\Url;
|
||||
|
||||
class RequestSet
|
||||
{
|
||||
/**
|
||||
* The set of visits to track.
|
||||
*
|
||||
* @var Request[]
|
||||
*/
|
||||
private $requests = null;
|
||||
|
||||
/**
|
||||
* The token auth supplied with a bulk visits POST.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $tokenAuth = null;
|
||||
|
||||
private $env = array();
|
||||
|
||||
public function setRequests($requests)
|
||||
{
|
||||
$this->requests = array();
|
||||
|
||||
foreach ($requests as $request) {
|
||||
if (empty($request) && !is_array($request)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$request instanceof Request) {
|
||||
$request = new Request($request, $this->getTokenAuth());
|
||||
}
|
||||
|
||||
$this->requests[] = $request;
|
||||
}
|
||||
}
|
||||
|
||||
public function setTokenAuth($tokenAuth)
|
||||
{
|
||||
$this->tokenAuth = $tokenAuth;
|
||||
}
|
||||
|
||||
public function getNumberOfRequests()
|
||||
{
|
||||
if (is_array($this->requests)) {
|
||||
return count($this->requests);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getRequests()
|
||||
{
|
||||
if (!$this->areRequestsInitialized()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $this->requests;
|
||||
}
|
||||
|
||||
public function getTokenAuth()
|
||||
{
|
||||
if (!is_null($this->tokenAuth)) {
|
||||
return $this->tokenAuth;
|
||||
}
|
||||
|
||||
return Common::getRequestVar('token_auth', false);
|
||||
}
|
||||
|
||||
private function areRequestsInitialized()
|
||||
{
|
||||
return !is_null($this->requests);
|
||||
}
|
||||
|
||||
public function initRequestsAndTokenAuth()
|
||||
{
|
||||
if ($this->areRequestsInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when detecting tracking requests. A plugin can use this event to set
|
||||
* requests that should be tracked by calling the {@link RequestSet::setRequests()} method.
|
||||
* For example the BulkTracking plugin uses this event to detect tracking requests and auth token based on
|
||||
* a sent JSON instead of default $_GET+$_POST. It would allow you for example to track requests based on
|
||||
* XML or you could import tracking requests stored in a file.
|
||||
*
|
||||
* @param \Piwik\Tracker\RequestSet &$requestSet Call {@link setRequests()} to initialize requests and
|
||||
* {@link setTokenAuth()} to set a detected auth token.
|
||||
*
|
||||
* @ignore This event is not public yet as the RequestSet API is not really stable yet
|
||||
*/
|
||||
Piwik::postEvent('Tracker.initRequestSet', array($this));
|
||||
|
||||
if (!$this->areRequestsInitialized()) {
|
||||
$this->requests = array();
|
||||
|
||||
if (!empty($_GET) || !empty($_POST)) {
|
||||
$this->setRequests(array($_GET + $_POST));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasRequests()
|
||||
{
|
||||
return !empty($this->requests);
|
||||
}
|
||||
|
||||
protected function getRedirectUrl()
|
||||
{
|
||||
return Common::getRequestVar('redirecturl', false, 'string');
|
||||
}
|
||||
|
||||
protected function hasRedirectUrl()
|
||||
{
|
||||
$redirectUrl = $this->getRedirectUrl();
|
||||
|
||||
return !empty($redirectUrl);
|
||||
}
|
||||
|
||||
protected function getAllSiteIdsWithinRequest()
|
||||
{
|
||||
if (empty($this->requests)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$siteIds = array();
|
||||
foreach ($this->requests as $request) {
|
||||
$siteIds[] = (int) $request->getIdSite();
|
||||
}
|
||||
|
||||
return array_values(array_unique($siteIds));
|
||||
}
|
||||
|
||||
// TODO maybe move to response? or somewhere else? not sure where!
|
||||
public function shouldPerformRedirectToUrl()
|
||||
{
|
||||
if (!$this->hasRedirectUrl()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->hasRequests()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$redirectUrl = $this->getRedirectUrl();
|
||||
$host = Url::getHostFromUrl($redirectUrl);
|
||||
|
||||
if (empty($host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$urls = new SiteUrls();
|
||||
$siteUrls = $urls->getAllCachedSiteUrls();
|
||||
$siteIds = $this->getAllSiteIdsWithinRequest();
|
||||
|
||||
foreach ($siteIds as $siteId) {
|
||||
if (empty($siteUrls[$siteId])) {
|
||||
$siteUrls[$siteId] = array();
|
||||
}
|
||||
|
||||
if (Url::isHostInUrls($host, $siteUrls[$siteId])) {
|
||||
return $redirectUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getState()
|
||||
{
|
||||
$requests = array(
|
||||
'requests' => array(),
|
||||
'env' => $this->getEnvironment(),
|
||||
'tokenAuth' => $this->getTokenAuth(),
|
||||
'time' => time()
|
||||
);
|
||||
|
||||
foreach ($this->getRequests() as $request) {
|
||||
$requests['requests'][] = $request->getRawParams();
|
||||
}
|
||||
|
||||
return $requests;
|
||||
}
|
||||
|
||||
public function restoreState($state)
|
||||
{
|
||||
$backupEnv = $this->getCurrentEnvironment();
|
||||
|
||||
$this->setEnvironment($state['env']);
|
||||
$this->setTokenAuth($state['tokenAuth']);
|
||||
|
||||
$this->restoreEnvironment();
|
||||
$this->setRequests($state['requests']);
|
||||
|
||||
foreach ($this->getRequests() as $request) {
|
||||
$request->setCurrentTimestamp($state['time']);
|
||||
}
|
||||
|
||||
$this->resetEnvironment($backupEnv);
|
||||
}
|
||||
|
||||
public function rememberEnvironment()
|
||||
{
|
||||
$this->setEnvironment($this->getEnvironment());
|
||||
}
|
||||
|
||||
public function setEnvironment($env)
|
||||
{
|
||||
$this->env = $env;
|
||||
}
|
||||
|
||||
protected function getEnvironment()
|
||||
{
|
||||
if (!empty($this->env)) {
|
||||
return $this->env;
|
||||
}
|
||||
|
||||
return $this->getCurrentEnvironment();
|
||||
}
|
||||
|
||||
public function restoreEnvironment()
|
||||
{
|
||||
if (empty($this->env)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->resetEnvironment($this->env);
|
||||
}
|
||||
|
||||
private function resetEnvironment($env)
|
||||
{
|
||||
$_SERVER = $env['server'];
|
||||
$_COOKIE = isset($env['cookie']) ? $env['cookie'] : array();
|
||||
}
|
||||
|
||||
private function getCurrentEnvironment()
|
||||
{
|
||||
return array(
|
||||
'server' => $_SERVER,
|
||||
'cookie' => $_COOKIE
|
||||
);
|
||||
}
|
||||
}
|
187
msd2/tracking/piwik/core/Tracker/Response.php
Normal file
187
msd2/tracking/piwik/core/Tracker/Response.php
Normal file
@ -0,0 +1,187 @@
|
||||
<?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 Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Profiler;
|
||||
use Piwik\Timer;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\Tracker\Db as TrackerDb;
|
||||
|
||||
class Response
|
||||
{
|
||||
private $timer;
|
||||
|
||||
private $content;
|
||||
|
||||
public function init(Tracker $tracker)
|
||||
{
|
||||
ob_start(); // we use ob_start only because of Common::printDebug, we should actually not really use ob_start
|
||||
|
||||
if ($tracker->isDebugModeEnabled()) {
|
||||
$this->timer = new Timer();
|
||||
|
||||
TrackerDb::enableProfiling();
|
||||
}
|
||||
}
|
||||
|
||||
public function getOutput()
|
||||
{
|
||||
$this->outputAccessControlHeaders();
|
||||
|
||||
if (is_null($this->content) && ob_get_level() > 0) {
|
||||
$this->content = ob_get_clean();
|
||||
}
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Echos an error message & other information, then exits.
|
||||
*
|
||||
* @param Tracker $tracker
|
||||
* @param Exception $e
|
||||
* @param int $statusCode eg 500
|
||||
*/
|
||||
public function outputException(Tracker $tracker, Exception $e, $statusCode)
|
||||
{
|
||||
Common::sendResponseCode($statusCode);
|
||||
$this->logExceptionToErrorLog($e);
|
||||
|
||||
if ($tracker->isDebugModeEnabled()) {
|
||||
Common::sendHeader('Content-Type: text/html; charset=utf-8');
|
||||
$trailer = '<span style="color: #888888">Backtrace:<br /><pre>' . $e->getTraceAsString() . '</pre></span>';
|
||||
$headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl');
|
||||
$footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl');
|
||||
$headerPage = str_replace('{$HTML_TITLE}', 'Matomo › Error', $headerPage);
|
||||
|
||||
echo $headerPage . '<p>' . $this->getMessageFromException($e) . '</p>' . $trailer . $footerPage;
|
||||
} else {
|
||||
$this->outputApiResponse($tracker);
|
||||
}
|
||||
}
|
||||
|
||||
public function outputResponse(Tracker $tracker)
|
||||
{
|
||||
if (!$tracker->shouldRecordStatistics()) {
|
||||
Common::sendResponseCode(503);
|
||||
$this->outputApiResponse($tracker);
|
||||
Common::printDebug("Logging disabled, display transparent logo");
|
||||
} elseif (!$tracker->hasLoggedRequests()) {
|
||||
if (!$this->isHttpGetRequest() || !empty($_GET) || !empty($_POST)) {
|
||||
Common::sendResponseCode(400);
|
||||
}
|
||||
Common::printDebug("Empty request => Matomo page");
|
||||
echo "This resource is part of Matomo. Keep full control of your data with the leading free and open source <a href='https://matomo.org' target='_blank' rel='noopener noreferrer nofollow'>web analytics & conversion optimisation platform</a>.";
|
||||
} else {
|
||||
$this->outputApiResponse($tracker);
|
||||
Common::printDebug("Nothing to notice => default behaviour");
|
||||
}
|
||||
|
||||
Common::printDebug("End of the page.");
|
||||
|
||||
if ($tracker->isDebugModeEnabled()
|
||||
&& $tracker->isDatabaseConnected()
|
||||
&& TrackerDb::isProfilingEnabled()) {
|
||||
$db = Tracker::getDatabase();
|
||||
$db->recordProfiling();
|
||||
Profiler::displayDbTrackerProfile($db);
|
||||
}
|
||||
|
||||
if ($tracker->isDebugModeEnabled()) {
|
||||
Common::printDebug($_COOKIE);
|
||||
Common::printDebug((string)$this->timer);
|
||||
}
|
||||
}
|
||||
|
||||
private function outputAccessControlHeaders()
|
||||
{
|
||||
if (!$this->isHttpGetRequest()) {
|
||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*';
|
||||
Common::sendHeader('Access-Control-Allow-Origin: ' . $origin);
|
||||
Common::sendHeader('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
}
|
||||
|
||||
private function isHttpGetRequest()
|
||||
{
|
||||
$requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
|
||||
|
||||
return strtoupper($requestMethod) === 'GET';
|
||||
}
|
||||
|
||||
private function getOutputBuffer()
|
||||
{
|
||||
return ob_get_contents();
|
||||
}
|
||||
|
||||
protected function hasAlreadyPrintedOutput()
|
||||
{
|
||||
return strlen($this->getOutputBuffer()) > 0;
|
||||
}
|
||||
|
||||
private function outputApiResponse(Tracker $tracker)
|
||||
{
|
||||
if ($tracker->isDebugModeEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasAlreadyPrintedOutput()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $_GET + $_POST;
|
||||
|
||||
if ($this->isHttpGetRequest()) {
|
||||
Common::sendHeader('Cache-Control: no-store');
|
||||
}
|
||||
|
||||
if (array_key_exists('send_image', $request) && $request['send_image'] === '0') {
|
||||
Common::sendResponseCode(204);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->outputTransparentGif();
|
||||
}
|
||||
|
||||
private function outputTransparentGif()
|
||||
{
|
||||
$transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
||||
Common::sendHeader('Content-Type: image/gif');
|
||||
|
||||
echo base64_decode($transGifBase64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error message to output when a tracking request fails.
|
||||
*
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getMessageFromException($e)
|
||||
{
|
||||
// Note: duplicated from FormDatabaseSetup.isAccessDenied
|
||||
// Avoid leaking the username/db name when access denied
|
||||
if ($e->getCode() == 1044 || $e->getCode() == 42000) {
|
||||
return "Error while connecting to the Matomo database - please check your credentials in config/config.ini.php file";
|
||||
}
|
||||
|
||||
if (Common::isPhpCliMode()) {
|
||||
return $e->getMessage() . "\n" . $e->getTraceAsString();
|
||||
}
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
protected function logExceptionToErrorLog($e)
|
||||
{
|
||||
error_log(sprintf("Error in Matomo (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e))));
|
||||
}
|
||||
}
|
86
msd2/tracking/piwik/core/Tracker/ScheduledTasksRunner.php
Normal file
86
msd2/tracking/piwik/core/Tracker/ScheduledTasksRunner.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?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\CliMulti;
|
||||
use Piwik\Common;
|
||||
use Piwik\Option;
|
||||
use Piwik\Tracker;
|
||||
|
||||
class ScheduledTasksRunner
|
||||
{
|
||||
|
||||
public function shouldRun(Tracker $tracker)
|
||||
{
|
||||
if (Common::isPhpCliMode()) {
|
||||
// don't run scheduled tasks in CLI mode from Tracker, this is the case
|
||||
// where we bulk load logs & don't want to lose time with tasks
|
||||
return false;
|
||||
}
|
||||
|
||||
return $tracker->shouldRecordStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracker requests will automatically trigger the Scheduled tasks.
|
||||
* This is useful for users who don't setup the cron,
|
||||
* but still want daily/weekly/monthly PDF reports emailed automatically.
|
||||
*
|
||||
* This is similar to calling the API CoreAdminHome.runScheduledTasks
|
||||
*/
|
||||
public function runScheduledTasks()
|
||||
{
|
||||
$now = time();
|
||||
|
||||
// Currently, there are no hourly tasks. When there are some,
|
||||
// this could be too aggressive minimum interval (some hours would be skipped in case of low traffic)
|
||||
$minimumInterval = TrackerConfig::getConfigValue('scheduled_tasks_min_interval');
|
||||
|
||||
// If the user disabled browser archiving, he has already setup a cron
|
||||
// To avoid parallel requests triggering the Scheduled Tasks,
|
||||
// Get last time tasks started executing
|
||||
$cache = Cache::getCacheGeneral();
|
||||
|
||||
if ($minimumInterval <= 0
|
||||
|| empty($cache['isBrowserTriggerEnabled'])
|
||||
) {
|
||||
Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
$nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval;
|
||||
|
||||
if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS)
|
||||
|| $cache['lastTrackerCronRun'] === false
|
||||
|| $nextRunTime < $now
|
||||
) {
|
||||
$cache['lastTrackerCronRun'] = $now;
|
||||
Cache::setCacheGeneral($cache);
|
||||
|
||||
Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']);
|
||||
Common::printDebug('-> Scheduled Tasks: Starting...');
|
||||
|
||||
$invokeScheduledTasksUrl = "?module=API&format=csv&convertToUnicode=0&method=CoreAdminHome.runScheduledTasks&trigger=archivephp";
|
||||
|
||||
$cliMulti = new CliMulti();
|
||||
$cliMulti->runAsSuperUser();
|
||||
$responses = $cliMulti->request(array($invokeScheduledTasksUrl));
|
||||
$resultTasks = reset($responses);
|
||||
|
||||
Common::printDebug($resultTasks);
|
||||
|
||||
Common::printDebug('Finished Scheduled Tasks.');
|
||||
} else {
|
||||
Common::printDebug("-> Scheduled tasks not triggered.");
|
||||
}
|
||||
|
||||
Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
|
||||
}
|
||||
}
|
126
msd2/tracking/piwik/core/Tracker/Settings.php
Normal file
126
msd2/tracking/piwik/core/Tracker/Settings.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?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\Config;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\DeviceDetectorFactory;
|
||||
use Piwik\SettingsPiwik;
|
||||
|
||||
class Settings // TODO: merge w/ visitor recognizer or make it it's own service. the class name is required for BC.
|
||||
{
|
||||
const OS_BOT = 'BOT';
|
||||
|
||||
/**
|
||||
* If `true`, the config ID for a visitor will be the same no matter what site is being tracked.
|
||||
* If `false, the config ID will be different.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isSameFingerprintsAcrossWebsites;
|
||||
|
||||
public function __construct($isSameFingerprintsAcrossWebsites)
|
||||
{
|
||||
$this->isSameFingerprintsAcrossWebsites = $isSameFingerprintsAcrossWebsites;
|
||||
}
|
||||
|
||||
public function getConfigId(Request $request, $ipAddress)
|
||||
{
|
||||
list($plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF,
|
||||
$plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie) = $request->getPlugins();
|
||||
|
||||
$userAgent = $request->getUserAgent();
|
||||
|
||||
$deviceDetector = DeviceDetectorFactory::getInstance($userAgent);
|
||||
$aBrowserInfo = $deviceDetector->getClient();
|
||||
|
||||
if ($aBrowserInfo['type'] != 'browser') {
|
||||
// for now only track browsers
|
||||
unset($aBrowserInfo);
|
||||
}
|
||||
|
||||
$browserName = !empty($aBrowserInfo['short_name']) ? $aBrowserInfo['short_name'] : 'UNK';
|
||||
$browserVersion = !empty($aBrowserInfo['version']) ? $aBrowserInfo['version'] : '';
|
||||
|
||||
if ($deviceDetector->isBot()) {
|
||||
$os = self::OS_BOT;
|
||||
} else {
|
||||
$os = $deviceDetector->getOS();
|
||||
$os = empty($os['short_name']) ? 'UNK' : $os['short_name'];
|
||||
}
|
||||
|
||||
$browserLang = substr($request->getBrowserLanguage(), 0, 20); // limit the length of this string to match db
|
||||
|
||||
return $this->getConfigHash(
|
||||
$request,
|
||||
$os,
|
||||
$browserName,
|
||||
$browserVersion,
|
||||
$plugin_Flash,
|
||||
$plugin_Java,
|
||||
$plugin_Director,
|
||||
$plugin_Quicktime,
|
||||
$plugin_RealPlayer,
|
||||
$plugin_PDF,
|
||||
$plugin_WindowsMedia,
|
||||
$plugin_Gears,
|
||||
$plugin_Silverlight,
|
||||
$plugin_Cookie,
|
||||
$ipAddress,
|
||||
$browserLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 64-bit hash that attemps to identify a user.
|
||||
* Maintaining some privacy by default, eg. prevents the merging of several Piwik serve together for matching across instances..
|
||||
*
|
||||
* @param $os
|
||||
* @param $browserName
|
||||
* @param $browserVersion
|
||||
* @param $plugin_Flash
|
||||
* @param $plugin_Java
|
||||
* @param $plugin_Director
|
||||
* @param $plugin_Quicktime
|
||||
* @param $plugin_RealPlayer
|
||||
* @param $plugin_PDF
|
||||
* @param $plugin_WindowsMedia
|
||||
* @param $plugin_Gears
|
||||
* @param $plugin_Silverlight
|
||||
* @param $plugin_Cookie
|
||||
* @param $ip
|
||||
* @param $browserLang
|
||||
* @return string
|
||||
*/
|
||||
protected function getConfigHash(Request $request, $os, $browserName, $browserVersion, $plugin_Flash, $plugin_Java,
|
||||
$plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF,
|
||||
$plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip,
|
||||
$browserLang)
|
||||
{
|
||||
// prevent the config hash from being the same, across different Piwik instances
|
||||
// (limits ability of different Piwik instances to cross-match users)
|
||||
$salt = SettingsPiwik::getSalt();
|
||||
|
||||
$configString =
|
||||
$os
|
||||
. $browserName . $browserVersion
|
||||
. $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF
|
||||
. $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie
|
||||
. $ip
|
||||
. $browserLang
|
||||
. $salt;
|
||||
|
||||
if (!$this->isSameFingerprintsAcrossWebsites) {
|
||||
$configString .= $request->getIdSite();
|
||||
}
|
||||
|
||||
$hash = md5($configString, $raw_output = true);
|
||||
|
||||
return substr($hash, 0, Tracker::LENGTH_BINARY_ID);
|
||||
}
|
||||
}
|
284
msd2/tracking/piwik/core/Tracker/TableLogAction.php
Normal file
284
msd2/tracking/piwik/core/Tracker/TableLogAction.php
Normal file
@ -0,0 +1,284 @@
|
||||
<?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\Common;
|
||||
use Piwik\Config;
|
||||
use Piwik\Container\StaticContainer;
|
||||
use Piwik\Segment\SegmentExpression;
|
||||
|
||||
/**
|
||||
* This class is used to query Action IDs from the log_action table.
|
||||
*
|
||||
* A pageview, outlink, download or site search are made of several "Action IDs"
|
||||
* For example pageview is idaction_url and idaction_name.
|
||||
*
|
||||
*/
|
||||
class TableLogAction
|
||||
{
|
||||
/**
|
||||
* This function will find the idaction from the lookup table log_action,
|
||||
* given an Action name, type, and an optional URL Prefix.
|
||||
*
|
||||
* This is used to record Page URLs, Page Titles, Ecommerce items SKUs, item names, item categories
|
||||
*
|
||||
* If the action name does not exist in the lookup table, it will INSERT it
|
||||
* @param array $actionsNameAndType Array of one or many (name,type)
|
||||
* @return array Returns the an array (Field name => idaction)
|
||||
*/
|
||||
public static function loadIdsAction($actionsNameAndType)
|
||||
{
|
||||
// Add url prefix if not set
|
||||
foreach ($actionsNameAndType as &$action) {
|
||||
if (2 == count($action)) {
|
||||
$action[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$actionIds = self::queryIdsAction($actionsNameAndType);
|
||||
|
||||
list($queriedIds, $fieldNamesToInsert) = self::processIdsToInsert($actionsNameAndType, $actionIds);
|
||||
|
||||
$insertedIds = self::insertNewIdsAction($actionsNameAndType, $fieldNamesToInsert);
|
||||
$queriedIds = $queriedIds + $insertedIds;
|
||||
|
||||
return $queriedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $matchType
|
||||
* @param $actionType
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function getSelectQueryWhereNameContains($matchType, $actionType)
|
||||
{
|
||||
// now, we handle the cases =@ (contains) and !@ (does not contain)
|
||||
// build the expression based on the match type
|
||||
$sql = 'SELECT idaction FROM ' . Common::prefixTable('log_action') . ' WHERE %s AND type = ' . $actionType . ' )';
|
||||
|
||||
switch ($matchType) {
|
||||
case SegmentExpression::MATCH_CONTAINS:
|
||||
// use concat to make sure, no %s occurs because some plugins use %s in their sql
|
||||
$where = '( name LIKE CONCAT(\'%\', ?, \'%\') ';
|
||||
break;
|
||||
case SegmentExpression::MATCH_DOES_NOT_CONTAIN:
|
||||
$where = '( name NOT LIKE CONCAT(\'%\', ?, \'%\') ';
|
||||
break;
|
||||
case SegmentExpression::MATCH_STARTS_WITH:
|
||||
// use concat to make sure, no %s occurs because some plugins use %s in their sql
|
||||
$where = '( name LIKE CONCAT(?, \'%\') ';
|
||||
break;
|
||||
case SegmentExpression::MATCH_ENDS_WITH:
|
||||
// use concat to make sure, no %s occurs because some plugins use %s in their sql
|
||||
$where = '( name LIKE CONCAT(\'%\', ?) ';
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("This match type $matchType is not available for action-segments.");
|
||||
break;
|
||||
}
|
||||
|
||||
$sql = sprintf($sql, $where);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private static function insertNewIdsAction($actionsNameAndType, $fieldNamesToInsert)
|
||||
{
|
||||
// Then, we insert all new actions in the lookup table
|
||||
$inserted = array();
|
||||
|
||||
foreach ($fieldNamesToInsert as $fieldName) {
|
||||
list($name, $type, $urlPrefix) = $actionsNameAndType[$fieldName];
|
||||
|
||||
$actionId = self::getModel()->createNewIdAction($name, $type, $urlPrefix);
|
||||
|
||||
Common::printDebug("Recorded a new action (" . Action::getTypeAsString($type) . ") in the lookup table: " . $name . " (idaction = " . $actionId . ")");
|
||||
|
||||
$inserted[$fieldName] = $actionId;
|
||||
}
|
||||
|
||||
return $inserted;
|
||||
}
|
||||
|
||||
private static function getModel()
|
||||
{
|
||||
return new Model();
|
||||
}
|
||||
|
||||
private static function queryIdsAction($actionsNameAndType)
|
||||
{
|
||||
$toQuery = array();
|
||||
foreach ($actionsNameAndType as &$actionNameType) {
|
||||
list($name, $type, $urlPrefix) = $actionNameType;
|
||||
$toQuery[] = array('name' => $name, 'type' => $type);
|
||||
}
|
||||
|
||||
$actionIds = self::getModel()->getIdsAction($toQuery);
|
||||
|
||||
return $actionIds;
|
||||
}
|
||||
|
||||
private static function processIdsToInsert($actionsNameAndType, $actionIds)
|
||||
{
|
||||
// For the Actions found in the lookup table, add the idaction in the array,
|
||||
// If not found in lookup table, queue for INSERT
|
||||
$fieldNamesToInsert = $fieldNameToActionId = array();
|
||||
|
||||
foreach ($actionsNameAndType as $fieldName => &$actionNameType) {
|
||||
@list($name, $type, $urlPrefix) = $actionNameType;
|
||||
if (empty($name)) {
|
||||
$fieldNameToActionId[$fieldName] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
foreach ($actionIds as $row) {
|
||||
if ($name == $row['name']
|
||||
&& $type == $row['type']
|
||||
) {
|
||||
$found = true;
|
||||
|
||||
$fieldNameToActionId[$fieldName] = $row['idaction'];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
$fieldNamesToInsert[] = $fieldName;
|
||||
}
|
||||
}
|
||||
|
||||
return array($fieldNameToActionId, $fieldNamesToInsert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert segment expression to an action ID or an SQL expression.
|
||||
*
|
||||
* This method is used as a sqlFilter-callback for the segments of this plugin.
|
||||
* Usually, these callbacks only return a value that should be compared to the
|
||||
* column in the database. In this case, that doesn't work since multiple IDs
|
||||
* can match an expression (e.g. "pageUrl=@foo").
|
||||
* @param string $valueToMatch
|
||||
* @param string $sqlField
|
||||
* @param string $matchType
|
||||
* @param string $segmentName
|
||||
* @throws \Exception
|
||||
* @return array|int|string
|
||||
*/
|
||||
public static function getIdActionFromSegment($valueToMatch, $sqlField, $matchType, $segmentName)
|
||||
{
|
||||
if ($segmentName === 'actionType') {
|
||||
$actionType = (int) $valueToMatch;
|
||||
$valueToMatch = array();
|
||||
$sql = 'SELECT idaction FROM ' . Common::prefixTable('log_action') . ' WHERE type = ' . $actionType . ' )';
|
||||
} else {
|
||||
$actionType = self::guessActionTypeFromSegment($segmentName);
|
||||
if ($actionType == Action::TYPE_PAGE_URL || $segmentName == 'eventUrl') {
|
||||
// for urls trim protocol and www because it is not recorded in the db
|
||||
$valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch);
|
||||
}
|
||||
|
||||
$valueToMatch = self::normaliseActionString($actionType, $valueToMatch);
|
||||
if ($matchType == SegmentExpression::MATCH_EQUAL
|
||||
|| $matchType == SegmentExpression::MATCH_NOT_EQUAL
|
||||
) {
|
||||
$idAction = self::getModel()->getIdActionMatchingNameAndType($valueToMatch, $actionType);
|
||||
// Action is not found (eg. &segment=pageTitle==Větrnásssssss)
|
||||
if (empty($idAction)) {
|
||||
$idAction = null;
|
||||
}
|
||||
return $idAction;
|
||||
}
|
||||
|
||||
// "name contains $string" match can match several idaction so we cannot return yet an idaction
|
||||
// special case
|
||||
$sql = self::getSelectQueryWhereNameContains($matchType, $actionType);
|
||||
}
|
||||
|
||||
|
||||
$cache = StaticContainer::get('Piwik\Tracker\TableLogAction\Cache');
|
||||
return $cache->getIdActionFromSegment($valueToMatch, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $segmentName
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function guessActionTypeFromSegment($segmentName)
|
||||
{
|
||||
$exactMatch = array(
|
||||
'outlinkUrl' => Action::TYPE_OUTLINK,
|
||||
'downloadUrl' => Action::TYPE_DOWNLOAD,
|
||||
'eventUrl' => Action::TYPE_EVENT,
|
||||
'eventAction' => Action::TYPE_EVENT_ACTION,
|
||||
'eventCategory' => Action::TYPE_EVENT_CATEGORY,
|
||||
'eventName' => Action::TYPE_EVENT_NAME,
|
||||
'contentPiece' => Action::TYPE_CONTENT_PIECE,
|
||||
'contentTarget' => Action::TYPE_CONTENT_TARGET,
|
||||
'contentName' => Action::TYPE_CONTENT_NAME,
|
||||
'contentInteraction' => Action::TYPE_CONTENT_INTERACTION,
|
||||
);
|
||||
|
||||
if (!empty($exactMatch[$segmentName])) {
|
||||
return $exactMatch[$segmentName];
|
||||
}
|
||||
|
||||
if (stripos($segmentName, 'pageurl') !== false) {
|
||||
$actionType = Action::TYPE_PAGE_URL;
|
||||
return $actionType;
|
||||
} elseif (stripos($segmentName, 'pagetitle') !== false) {
|
||||
$actionType = Action::TYPE_PAGE_TITLE;
|
||||
return $actionType;
|
||||
} elseif (stripos($segmentName, 'sitesearch') !== false) {
|
||||
$actionType = Action::TYPE_SITE_SEARCH;
|
||||
return $actionType;
|
||||
} else {
|
||||
throw new \Exception("We cannot guess the action type from the segment $segmentName.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will sanitize or not if it's needed for the specified action type
|
||||
*
|
||||
* URLs (Download URL, Outlink URL) are stored raw (unsanitized)
|
||||
* while other action types are stored Sanitized
|
||||
*
|
||||
* @param $actionType
|
||||
* @param $actionString
|
||||
* @return string
|
||||
*/
|
||||
private static function normaliseActionString($actionType, $actionString)
|
||||
{
|
||||
$actionString = Common::unsanitizeInputValue($actionString);
|
||||
|
||||
if (self::isActionTypeStoredUnsanitized($actionType)) {
|
||||
return $actionString;
|
||||
}
|
||||
|
||||
return Common::sanitizeInputValue($actionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $actionType
|
||||
* @return bool
|
||||
*/
|
||||
private static function isActionTypeStoredUnsanitized($actionType)
|
||||
{
|
||||
$actionsTypesStoredUnsanitized = array(
|
||||
Action::TYPE_DOWNLOAD,
|
||||
Action::TYPE_OUTLINK,
|
||||
Action::TYPE_PAGE_URL,
|
||||
Action::TYPE_CONTENT,
|
||||
);
|
||||
|
||||
return in_array($actionType, $actionsTypesStoredUnsanitized);
|
||||
}
|
||||
}
|
160
msd2/tracking/piwik/core/Tracker/TableLogAction/Cache.php
Normal file
160
msd2/tracking/piwik/core/Tracker/TableLogAction/Cache.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?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\TableLogAction;
|
||||
|
||||
use Piwik\Common;
|
||||
use Piwik\Config;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Cache
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $isEnabled;
|
||||
|
||||
/**
|
||||
* @var int cache lifetime in seconds
|
||||
*/
|
||||
protected $lifetime;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @var \Piwik\Cache\Lazy
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
public function __construct(LoggerInterface $logger, Config $config, \Piwik\Cache\Lazy $cache)
|
||||
{
|
||||
$this->isEnabled = (bool)$config->General['enable_segments_subquery_cache'];
|
||||
$this->limitActionIds = $config->General['segments_subquery_cache_limit'];
|
||||
$this->lifetime = $config->General['segments_subquery_cache_ttl'];
|
||||
$this->logger = $logger;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $valueToMatch
|
||||
* @param $sql
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getIdActionFromSegment($valueToMatch, $sql)
|
||||
{
|
||||
if (!$this->isEnabled) {
|
||||
return array(
|
||||
// mark that the returned value is an sql-expression instead of a literal value
|
||||
'SQL' => $sql,
|
||||
'bind' => $valueToMatch,
|
||||
);
|
||||
}
|
||||
|
||||
$ids = self::getIdsFromCache($valueToMatch, $sql);
|
||||
|
||||
if(is_null($ids)) {
|
||||
// Too Big To Cache, issue SQL as subquery instead
|
||||
return array(
|
||||
'SQL' => $sql,
|
||||
'bind' => $valueToMatch,
|
||||
);
|
||||
}
|
||||
|
||||
if(count($ids) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$sql = Common::getSqlStringFieldsArray($ids);
|
||||
$bind = $ids;
|
||||
|
||||
return array(
|
||||
// mark that the returned value is an sql-expression instead of a literal value
|
||||
'SQL' => $sql,
|
||||
'bind' => $bind,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $valueToMatch
|
||||
* @param $sql
|
||||
* @return array of IDs, or null if the returnset is too big to cache
|
||||
*/
|
||||
private function getIdsFromCache($valueToMatch, $sql)
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($valueToMatch, $sql);
|
||||
|
||||
if ($this->cache->contains($cacheKey) === true) { // TODO: hits
|
||||
$this->logger->debug("Segment subquery cache HIT (for '$valueToMatch' and SQL '$sql)");
|
||||
return $this->cache->fetch($cacheKey);
|
||||
}
|
||||
|
||||
$ids = $this->fetchActionIdsFromDb($valueToMatch, $sql);
|
||||
|
||||
if($this->isTooBigToCache($ids)) {
|
||||
$this->logger->debug("Segment subquery cache SKIPPED SAVE (too many IDs returned by subquery: %s ids)'", array(count($ids)));
|
||||
$this->cache->save($cacheKey, $ids = null, $this->lifetime);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->cache->save($cacheKey, $ids, $this->lifetime);
|
||||
$this->logger->debug("Segment subquery cache SAVE (for '$valueToMatch' and SQL '$sql')'");
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $valueToMatch
|
||||
* @param $sql
|
||||
* @return string
|
||||
* @throws
|
||||
*/
|
||||
private function getCacheKey($valueToMatch, $sql)
|
||||
{
|
||||
if(is_array($valueToMatch)) {
|
||||
throw new \Exception("value to match is an array: this is not expected");
|
||||
}
|
||||
|
||||
$uniqueKey = md5($sql . $valueToMatch);
|
||||
$cacheKey = 'TableLogAction.getIdActionFromSegment.' . $uniqueKey;
|
||||
return $cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $valueToMatch
|
||||
* @param $sql
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function fetchActionIdsFromDb($valueToMatch, $sql)
|
||||
{
|
||||
$idActions = \Piwik\Db::fetchAll($sql, $valueToMatch);
|
||||
|
||||
$ids = array();
|
||||
foreach ($idActions as $idAction) {
|
||||
$ids[] = $idAction['idaction'];
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ids
|
||||
* @return bool
|
||||
*/
|
||||
private function isTooBigToCache($ids)
|
||||
{
|
||||
return count($ids) > $this->limitActionIds;
|
||||
}
|
||||
}
|
270
msd2/tracking/piwik/core/Tracker/TrackerCodeGenerator.php
Normal file
270
msd2/tracking/piwik/core/Tracker/TrackerCodeGenerator.php
Normal file
@ -0,0 +1,270 @@
|
||||
<?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\Common;
|
||||
use Piwik\DbHelper;
|
||||
use Piwik\Option;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugins\CustomVariables\CustomVariables;
|
||||
use Piwik\Plugins\SitesManager\API as APISitesManager;
|
||||
use Piwik\SettingsPiwik;
|
||||
use Piwik\View;
|
||||
|
||||
/**
|
||||
* Generates the Javascript code to be inserted on every page of the website to track.
|
||||
*/
|
||||
class TrackerCodeGenerator
|
||||
{
|
||||
/**
|
||||
* whether matomo.js|php should be forced over piwik.js|php
|
||||
* @var bool
|
||||
*/
|
||||
private $shouldForceMatomoEndpoint = false;
|
||||
|
||||
public function forceMatomoEndpoint()
|
||||
{
|
||||
$this->shouldForceMatomoEndpoint = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $idSite
|
||||
* @param string $piwikUrl http://path/to/piwik/site/
|
||||
* @param bool $mergeSubdomains
|
||||
* @param bool $groupPageTitlesByDomain
|
||||
* @param bool $mergeAliasUrls
|
||||
* @param array $visitorCustomVariables
|
||||
* @param array $pageCustomVariables
|
||||
* @param string $customCampaignNameQueryParam
|
||||
* @param string $customCampaignKeywordParam
|
||||
* @param bool $doNotTrack
|
||||
* @param bool $disableCookies
|
||||
* @param bool $trackNoScript
|
||||
* @return string Javascript code.
|
||||
*/
|
||||
public function generate(
|
||||
$idSite,
|
||||
$piwikUrl,
|
||||
$mergeSubdomains = false,
|
||||
$groupPageTitlesByDomain = false,
|
||||
$mergeAliasUrls = false,
|
||||
$visitorCustomVariables = null,
|
||||
$pageCustomVariables = null,
|
||||
$customCampaignNameQueryParam = null,
|
||||
$customCampaignKeywordParam = null,
|
||||
$doNotTrack = false,
|
||||
$disableCookies = false,
|
||||
$trackNoScript = false,
|
||||
$crossDomain = false
|
||||
) {
|
||||
// changes made to this code should be mirrored in plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js var generateJsCode
|
||||
|
||||
if (substr($piwikUrl, 0, 4) !== 'http') {
|
||||
$piwikUrl = 'http://' . $piwikUrl;
|
||||
}
|
||||
preg_match('~^(http|https)://(.*)$~D', $piwikUrl, $matches);
|
||||
$piwikUrl = rtrim(@$matches[2], "/");
|
||||
|
||||
// Build optional parameters to be added to text
|
||||
$options = '';
|
||||
$optionsBeforeTrackerUrl = '';
|
||||
if ($groupPageTitlesByDomain) {
|
||||
$options .= ' _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);' . "\n";
|
||||
}
|
||||
if ($crossDomain) {
|
||||
// When enabling cross domain, we also need to call `setDomains`
|
||||
$mergeAliasUrls = true;
|
||||
}
|
||||
if ($mergeSubdomains || $mergeAliasUrls) {
|
||||
$options .= $this->getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls);
|
||||
}
|
||||
|
||||
if ($crossDomain) {
|
||||
$options .= ' _paq.push(["enableCrossDomainLinking"]);' . "\n";
|
||||
}
|
||||
|
||||
$maxCustomVars = CustomVariables::getNumUsableCustomVariables();
|
||||
|
||||
if ($visitorCustomVariables && count($visitorCustomVariables) > 0) {
|
||||
$options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each visitor' . "\n";
|
||||
$index = 1;
|
||||
foreach ($visitorCustomVariables as $visitorCustomVariable) {
|
||||
if (empty($visitorCustomVariable)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options .= sprintf(
|
||||
' _paq.push(["setCustomVariable", %d, %s, %s, "visit"]);%s',
|
||||
$index++,
|
||||
json_encode($visitorCustomVariable[0]),
|
||||
json_encode($visitorCustomVariable[1]),
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
if ($pageCustomVariables && count($pageCustomVariables) > 0) {
|
||||
$options .= ' // you can set up to ' . $maxCustomVars . ' custom variables for each action (page view, download, click, site search)' . "\n";
|
||||
$index = 1;
|
||||
foreach ($pageCustomVariables as $pageCustomVariable) {
|
||||
if (empty($pageCustomVariable)) {
|
||||
continue;
|
||||
}
|
||||
$options .= sprintf(
|
||||
' _paq.push(["setCustomVariable", %d, %s, %s, "page"]);%s',
|
||||
$index++,
|
||||
json_encode($pageCustomVariable[0]),
|
||||
json_encode($pageCustomVariable[1]),
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
if ($customCampaignNameQueryParam) {
|
||||
$options .= ' _paq.push(["setCampaignNameKey", '
|
||||
. json_encode($customCampaignNameQueryParam) . ']);' . "\n";
|
||||
}
|
||||
if ($customCampaignKeywordParam) {
|
||||
$options .= ' _paq.push(["setCampaignKeywordKey", '
|
||||
. json_encode($customCampaignKeywordParam) . ']);' . "\n";
|
||||
}
|
||||
if ($doNotTrack) {
|
||||
$options .= ' _paq.push(["setDoNotTrack", true]);' . "\n";
|
||||
}
|
||||
if ($disableCookies) {
|
||||
$options .= ' _paq.push(["disableCookies"]);' . "\n";
|
||||
}
|
||||
|
||||
$codeImpl = array(
|
||||
'idSite' => $idSite,
|
||||
// TODO why sanitizeInputValue() and not json_encode?
|
||||
'piwikUrl' => Common::sanitizeInputValue($piwikUrl),
|
||||
'options' => $options,
|
||||
'optionsBeforeTrackerUrl' => $optionsBeforeTrackerUrl,
|
||||
'protocol' => '//',
|
||||
'loadAsync' => true,
|
||||
'trackNoScript' => $trackNoScript,
|
||||
'matomoJsFilename' => $this->getJsTrackerEndpoint(),
|
||||
'matomoPhpFilename' => $this->getPhpTrackerEndpoint(),
|
||||
);
|
||||
|
||||
if (SettingsPiwik::isHttpsForced()) {
|
||||
$codeImpl['protocol'] = 'https://';
|
||||
}
|
||||
|
||||
$parameters = compact('mergeSubdomains', 'groupPageTitlesByDomain', 'mergeAliasUrls', 'visitorCustomVariables',
|
||||
'pageCustomVariables', 'customCampaignNameQueryParam', 'customCampaignKeywordParam',
|
||||
'doNotTrack');
|
||||
|
||||
/**
|
||||
* Triggered when generating JavaScript tracking code server side. Plugins can use
|
||||
* this event to customise the JavaScript tracking code that is displayed to the
|
||||
* user.
|
||||
*
|
||||
* @param array &$codeImpl An array containing snippets of code that the event handler
|
||||
* can modify. Will contain the following elements:
|
||||
*
|
||||
* - **idSite**: The ID of the site being tracked.
|
||||
* - **piwikUrl**: The tracker URL to use.
|
||||
* - **options**: A string of JavaScript code that customises
|
||||
* the JavaScript tracker.
|
||||
* - **optionsBeforeTrackerUrl**: A string of Javascript code that customises
|
||||
* the JavaScript tracker inside of anonymous function before
|
||||
* adding setTrackerUrl into paq.
|
||||
* - **protocol**: Piwik url protocol.
|
||||
* - **loadAsync**: boolean whether piwik.js should be loaded syncronous or asynchronous
|
||||
*
|
||||
* The **httpsPiwikUrl** element can be set if the HTTPS
|
||||
* domain is different from the normal domain.
|
||||
* @param array $parameters The parameters supplied to `TrackerCodeGenerator::generate()`.
|
||||
*/
|
||||
Piwik::postEvent('Piwik.getJavascriptCode', array(&$codeImpl, $parameters));
|
||||
|
||||
$setTrackerUrl = 'var u="' . $codeImpl['protocol'] . '{$piwikUrl}/";';
|
||||
|
||||
if (!empty($codeImpl['httpsPiwikUrl'])) {
|
||||
$setTrackerUrl = 'var u=((document.location.protocol === "https:") ? "https://{$httpsPiwikUrl}/" : "http://{$piwikUrl}/");';
|
||||
$codeImpl['httpsPiwikUrl'] = rtrim($codeImpl['httpsPiwikUrl'], "/");
|
||||
}
|
||||
$codeImpl = array('setTrackerUrl' => htmlentities($setTrackerUrl, ENT_COMPAT | ENT_HTML401, 'UTF-8')) + $codeImpl;
|
||||
|
||||
$view = new View('@Morpheus/javascriptCode');
|
||||
$view->disableCacheBuster();
|
||||
$view->loadAsync = $codeImpl['loadAsync'];
|
||||
$view->trackNoScript = $codeImpl['trackNoScript'];
|
||||
$jsCode = $view->render();
|
||||
$jsCode = htmlentities($jsCode, ENT_COMPAT | ENT_HTML401, 'UTF-8');
|
||||
|
||||
foreach ($codeImpl as $keyToReplace => $replaceWith) {
|
||||
$jsCode = str_replace('{$' . $keyToReplace . '}', $replaceWith, $jsCode);
|
||||
}
|
||||
|
||||
return $jsCode;
|
||||
}
|
||||
|
||||
public function getJsTrackerEndpoint()
|
||||
{
|
||||
$name = 'matomo.js';
|
||||
if ($this->shouldPreferPiwikEndpoint()) {
|
||||
$name = 'piwik.js';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function getPhpTrackerEndpoint()
|
||||
{
|
||||
$name = 'matomo.php';
|
||||
if ($this->shouldPreferPiwikEndpoint()) {
|
||||
$name = 'piwik.php';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function shouldPreferPiwikEndpoint()
|
||||
{
|
||||
if ($this->shouldForceMatomoEndpoint) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only since 3.7.0 we use the default matomo.js|php... for all other installs we need to keep BC
|
||||
return DbHelper::wasMatomoInstalledBeforeVersion('3.7.0-b1');
|
||||
}
|
||||
|
||||
private function getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls)
|
||||
{
|
||||
try {
|
||||
$websiteUrls = APISitesManager::getInstance()->getSiteUrlsFromId($idSite);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
// We need to parse_url to isolate hosts
|
||||
$websiteHosts = array();
|
||||
$firstHost = null;
|
||||
foreach ($websiteUrls as $site_url) {
|
||||
$referrerParsed = parse_url($site_url);
|
||||
|
||||
if (!isset($firstHost)) {
|
||||
$firstHost = $referrerParsed['host'];
|
||||
}
|
||||
|
||||
$url = $referrerParsed['host'];
|
||||
if (!empty($referrerParsed['path'])) {
|
||||
$url .= $referrerParsed['path'];
|
||||
}
|
||||
$websiteHosts[] = $url;
|
||||
}
|
||||
$options = '';
|
||||
if ($mergeSubdomains && !empty($firstHost)) {
|
||||
$options .= ' _paq.push(["setCookieDomain", "*.' . $firstHost . '"]);' . "\n";
|
||||
}
|
||||
if ($mergeAliasUrls && !empty($websiteHosts)) {
|
||||
$urls = '["*.' . implode('","*.', $websiteHosts) . '"]';
|
||||
$options .= ' _paq.push(["setDomains", ' . $urls . ']);' . "\n";
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
39
msd2/tracking/piwik/core/Tracker/TrackerConfig.php
Normal file
39
msd2/tracking/piwik/core/Tracker/TrackerConfig.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?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\Config;
|
||||
use Piwik\Tracker;
|
||||
|
||||
class TrackerConfig
|
||||
{
|
||||
/**
|
||||
* Update Tracker config
|
||||
*
|
||||
* @param string $name Setting name
|
||||
* @param mixed $value Value
|
||||
*/
|
||||
public static function setConfigValue($name, $value)
|
||||
{
|
||||
$section = self::getConfig();
|
||||
$section[$name] = $value;
|
||||
Config::getInstance()->Tracker = $section;
|
||||
}
|
||||
|
||||
public static function getConfigValue($name)
|
||||
{
|
||||
$config = self::getConfig();
|
||||
return $config[$name];
|
||||
}
|
||||
|
||||
private static function getConfig()
|
||||
{
|
||||
return Config::getInstance()->Tracker;
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
48
msd2/tracking/piwik/core/Tracker/Visit/Factory.php
Normal file
48
msd2/tracking/piwik/core/Tracker/Visit/Factory.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?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\Visit;
|
||||
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Tracker\Visit;
|
||||
use Piwik\Tracker\VisitInterface;
|
||||
use Exception;
|
||||
|
||||
class Factory
|
||||
{
|
||||
/**
|
||||
* Returns the Tracker_Visit object.
|
||||
* This method can be overwritten to use a different Tracker_Visit object
|
||||
*
|
||||
* @throws Exception
|
||||
* @return \Piwik\Tracker\Visit
|
||||
*/
|
||||
public static function make()
|
||||
{
|
||||
$visit = null;
|
||||
|
||||
/**
|
||||
* Triggered before a new **visit tracking object** is created. Subscribers to this
|
||||
* event can force the use of a custom visit tracking object that extends from
|
||||
* {@link Piwik\Tracker\VisitInterface}.
|
||||
*
|
||||
* @param \Piwik\Tracker\VisitInterface &$visit Initialized to null, but can be set to
|
||||
* a new visit object. If it isn't modified
|
||||
* Piwik uses the default class.
|
||||
*/
|
||||
Piwik::postEvent('Tracker.makeNewVisitObject', array(&$visit));
|
||||
|
||||
if (!isset($visit)) {
|
||||
$visit = new Visit();
|
||||
} elseif (!($visit instanceof VisitInterface)) {
|
||||
throw new Exception("The Visit object set in the plugin must implement VisitInterface");
|
||||
}
|
||||
|
||||
return $visit;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?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\Visit;
|
||||
|
||||
use Piwik\Cache;
|
||||
use Piwik\Common;
|
||||
use Piwik\Option;
|
||||
use Piwik\Tracker\Request;
|
||||
|
||||
/**
|
||||
* Filters out tracking requests issued by spammers.
|
||||
*/
|
||||
class ReferrerSpamFilter
|
||||
{
|
||||
const OPTION_STORAGE_NAME = 'referrer_spam_blacklist';
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $spammerList;
|
||||
|
||||
/**
|
||||
* Check if the request is from a known spammer host.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
public function isSpam(Request $request)
|
||||
{
|
||||
$spammers = $this->getSpammerListFromCache();
|
||||
|
||||
$referrerUrl = $request->getParam('urlref');
|
||||
|
||||
foreach ($spammers as $spammerHost) {
|
||||
if (stripos($referrerUrl, $spammerHost) !== false) {
|
||||
Common::printDebug('Referrer URL is a known spam: ' . $spammerHost);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getSpammerListFromCache()
|
||||
{
|
||||
$cache = Cache::getEagerCache();
|
||||
$cacheId = 'ReferrerSpamFilter-' . self::OPTION_STORAGE_NAME;
|
||||
|
||||
if ($cache->contains($cacheId)) {
|
||||
$list = $cache->fetch($cacheId);
|
||||
} else {
|
||||
$list = $this->loadSpammerList();
|
||||
$cache->save($cacheId, $list);
|
||||
}
|
||||
|
||||
if(!is_array($list)) {
|
||||
Common::printDebug('Warning: could not read list of spammers from cache.');
|
||||
return array();
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function loadSpammerList()
|
||||
{
|
||||
if ($this->spammerList !== null) {
|
||||
return $this->spammerList;
|
||||
}
|
||||
|
||||
// Read first from the auto-updated list in database
|
||||
$list = Option::get(self::OPTION_STORAGE_NAME);
|
||||
|
||||
if ($list) {
|
||||
$this->spammerList = Common::safe_unserialize($list);
|
||||
} else {
|
||||
// Fallback to reading the bundled list
|
||||
$file = PIWIK_VENDOR_PATH . '/matomo/referrer-spam-blacklist/spammers.txt';
|
||||
$this->spammerList = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
}
|
||||
|
||||
return $this->spammerList;
|
||||
}
|
||||
}
|
73
msd2/tracking/piwik/core/Tracker/Visit/VisitProperties.php
Normal file
73
msd2/tracking/piwik/core/Tracker/Visit/VisitProperties.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?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\Visit;
|
||||
|
||||
/**
|
||||
* Holds temporary data for tracking requests.
|
||||
*/
|
||||
class VisitProperties
|
||||
{
|
||||
/**
|
||||
* Information about the current visit. This array holds the column values that will be inserted or updated
|
||||
* in the `log_visit` table, or the values for the last known visit of the current visitor.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $visitInfo = array();
|
||||
|
||||
/**
|
||||
* Returns a visit property, or `null` if none is set.
|
||||
*
|
||||
* @param string $name The property name.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProperty($name)
|
||||
{
|
||||
return isset($this->visitInfo[$name]) ? $this->visitInfo[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all visit properties by reference.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function &getProperties()
|
||||
{
|
||||
return $this->visitInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a visit property.
|
||||
*
|
||||
* @param string $name The property name.
|
||||
* @param mixed $value The property value.
|
||||
*/
|
||||
public function setProperty($name, $value)
|
||||
{
|
||||
$this->visitInfo[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets all visit properties.
|
||||
*/
|
||||
public function clearProperties()
|
||||
{
|
||||
$this->visitInfo = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all visit properties.
|
||||
*
|
||||
* @param array $properties
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->visitInfo = $properties;
|
||||
}
|
||||
}
|
367
msd2/tracking/piwik/core/Tracker/VisitExcluded.php
Normal file
367
msd2/tracking/piwik/core/Tracker/VisitExcluded.php
Normal file
@ -0,0 +1,367 @@
|
||||
<?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\Cache as PiwikCache;
|
||||
use Piwik\Common;
|
||||
use Piwik\DeviceDetectorFactory;
|
||||
use Piwik\Exception\UnexpectedWebsiteFoundException;
|
||||
use Piwik\Network\IP;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Plugins\SitesManager\SiteUrls;
|
||||
use Piwik\Tracker\Visit\ReferrerSpamFilter;
|
||||
|
||||
/**
|
||||
* This class contains the logic to exclude some visitors from being tracked as per user settings
|
||||
*/
|
||||
class VisitExcluded
|
||||
{
|
||||
/**
|
||||
* @var ReferrerSpamFilter
|
||||
*/
|
||||
private $spamFilter;
|
||||
|
||||
private $siteCache = array();
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->spamFilter = new ReferrerSpamFilter();
|
||||
$this->request = $request;
|
||||
|
||||
try {
|
||||
$this->idSite = $request->getIdSite();
|
||||
} catch (UnexpectedWebsiteFoundException $e){
|
||||
// most checks will still work on a global scope and we still want to be able to test if this is a valid
|
||||
// visit or not
|
||||
$this->idSite = 0;
|
||||
}
|
||||
$userAgent = $request->getUserAgent();
|
||||
$this->userAgent = Common::unsanitizeInputValue($userAgent);
|
||||
$this->ip = $request->getIp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the current visitor is excluded from the statistics.
|
||||
*
|
||||
* Plugins can for example exclude visitors based on the
|
||||
* - IP
|
||||
* - If a given cookie is found
|
||||
*
|
||||
* @return bool True if the visit must not be saved, false otherwise
|
||||
*/
|
||||
public function isExcluded()
|
||||
{
|
||||
$excluded = false;
|
||||
|
||||
if ($this->isNonHumanBot()) {
|
||||
Common::printDebug('Search bot detected, visit excluded');
|
||||
$excluded = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Requests built with piwik.js will contain a rec=1 parameter. This is used as
|
||||
* an indication that the request is made by a JS enabled device. By default, Piwik
|
||||
* doesn't track non-JS visitors.
|
||||
*/
|
||||
if (!$excluded) {
|
||||
$toRecord = $this->request->getParam($parameterForceRecord = 'rec');
|
||||
if (!$toRecord) {
|
||||
Common::printDebug(@$_SERVER['REQUEST_METHOD'] . ' parameter ' . $parameterForceRecord . ' not found in URL, request excluded');
|
||||
$excluded = true;
|
||||
Common::printDebug("'$parameterForceRecord' parameter not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered on every tracking request.
|
||||
*
|
||||
* This event can be used to tell the Tracker not to record this particular action or visit.
|
||||
*
|
||||
* @param bool &$excluded Whether the request should be excluded or not. Initialized
|
||||
* to `false`. Event subscribers should set it to `true` in
|
||||
* order to exclude the request.
|
||||
* @param Request $request The request object which contains all of the request's information
|
||||
*
|
||||
*/
|
||||
Piwik::postEvent('Tracker.isExcludedVisit', array(&$excluded, $this->request));
|
||||
|
||||
/*
|
||||
* Following exclude operations happen after the hook.
|
||||
* These are of higher priority and should not be overwritten by plugins.
|
||||
*/
|
||||
|
||||
// Checking if the Piwik ignore cookie is set
|
||||
if (!$excluded) {
|
||||
$excluded = $this->isIgnoreCookieFound();
|
||||
if ($excluded) {
|
||||
Common::printDebug("Ignore cookie found.");
|
||||
}
|
||||
}
|
||||
|
||||
// Checking for excluded IPs
|
||||
if (!$excluded) {
|
||||
$excluded = $this->isVisitorIpExcluded();
|
||||
if ($excluded) {
|
||||
Common::printDebug("IP excluded.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user agent should be excluded
|
||||
if (!$excluded) {
|
||||
$excluded = $this->isUserAgentExcluded();
|
||||
if ($excluded) {
|
||||
Common::printDebug("User agent excluded.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Referrer URL is a known spam
|
||||
if (!$excluded) {
|
||||
$excluded = $this->isReferrerSpamExcluded();
|
||||
if ($excluded) {
|
||||
Common::printDebug("Referrer URL is blacklisted as spam.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if request URL is excluded
|
||||
if (!$excluded) {
|
||||
$excluded = $this->isUrlExcluded();
|
||||
if ($excluded) {
|
||||
Common::printDebug("Unknown URL is not allowed to track.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$excluded) {
|
||||
if ($this->isPrefetchDetected()) {
|
||||
$excluded = true;
|
||||
Common::printDebug("Prefetch request detected, not a real visit so we Ignore this visit/pageview");
|
||||
}
|
||||
}
|
||||
|
||||
if ($excluded) {
|
||||
Common::printDebug("Visitor excluded.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function isPrefetchDetected()
|
||||
{
|
||||
return (isset($_SERVER["HTTP_X_PURPOSE"])
|
||||
&& in_array($_SERVER["HTTP_X_PURPOSE"], array("preview", "instant")))
|
||||
|| (isset($_SERVER['HTTP_X_MOZ'])
|
||||
&& $_SERVER['HTTP_X_MOZ'] == "prefetch");
|
||||
}
|
||||
|
||||
/**
|
||||
* Live/Bing/MSN bot and Googlebot are evolving to detect cloaked websites.
|
||||
* As a result, these sophisticated bots exhibit characteristics of
|
||||
* browsers (cookies enabled, executing JavaScript, etc).
|
||||
*
|
||||
* @see \DeviceDetector\Parser\Bot
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isNonHumanBot()
|
||||
{
|
||||
$allowBots = $this->request->getParam('bots');
|
||||
|
||||
$deviceDetector = DeviceDetectorFactory::getInstance($this->userAgent);
|
||||
|
||||
return !$allowBots
|
||||
&& ($deviceDetector->isBot() || $this->isIpInRange());
|
||||
}
|
||||
|
||||
private function isIpInRange()
|
||||
{
|
||||
$cache = PiwikCache::getTransientCache();
|
||||
|
||||
$ip = IP::fromBinaryIP($this->ip);
|
||||
$key = 'VisitExcludedIsIpInRange' . $ip->toString();
|
||||
|
||||
if ($cache->contains($key)) {
|
||||
$isInRanges = $cache->fetch($key);
|
||||
} else {
|
||||
if ($this->isChromeDataSaverUsed($ip)) {
|
||||
$isInRanges = false;
|
||||
} else {
|
||||
$isInRanges = $ip->isInRanges($this->getBotIpRanges());
|
||||
}
|
||||
|
||||
$cache->save($key, $isInRanges);
|
||||
}
|
||||
|
||||
return $isInRanges;
|
||||
}
|
||||
|
||||
private function isChromeDataSaverUsed(IP $ip)
|
||||
{
|
||||
// see https://github.com/piwik/piwik/issues/7733
|
||||
return !empty($_SERVER['HTTP_VIA'])
|
||||
&& false !== strpos(strtolower($_SERVER['HTTP_VIA']), 'chrome-compression-proxy')
|
||||
&& $ip->isInRanges($this->getGoogleBotIpRanges());
|
||||
}
|
||||
|
||||
protected function getBotIpRanges()
|
||||
{
|
||||
return array_merge($this->getGoogleBotIpRanges(), array(
|
||||
// Live/Bing/MSN
|
||||
'64.4.0.0/18',
|
||||
'65.52.0.0/14',
|
||||
'157.54.0.0/15',
|
||||
'157.56.0.0/14',
|
||||
'157.60.0.0/16',
|
||||
'207.46.0.0/16',
|
||||
'207.68.128.0/18',
|
||||
'207.68.192.0/20',
|
||||
'131.253.26.0/20',
|
||||
'131.253.24.0/20',
|
||||
|
||||
// Yahoo
|
||||
'72.30.198.0/20',
|
||||
'72.30.196.0/20',
|
||||
'98.137.207.0/20',
|
||||
// Chinese bot hammering websites
|
||||
'1.202.218.8'
|
||||
));
|
||||
}
|
||||
|
||||
private function getGoogleBotIpRanges()
|
||||
{
|
||||
return array(
|
||||
'216.239.32.0/19',
|
||||
'64.233.160.0/19',
|
||||
'66.249.80.0/20',
|
||||
'72.14.192.0/18',
|
||||
'209.85.128.0/17',
|
||||
'66.102.0.0/20',
|
||||
'74.125.0.0/16',
|
||||
'64.18.0.0/20',
|
||||
'207.126.144.0/20',
|
||||
'173.194.0.0/16'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for the ignore cookie that users can set in the Piwik admin screen.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isIgnoreCookieFound()
|
||||
{
|
||||
if (IgnoreCookie::isIgnoreCookieFound()) {
|
||||
Common::printDebug('Matomo ignore cookie was found, visit not tracked.');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the visitor ip is in the excluded list
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isVisitorIpExcluded()
|
||||
{
|
||||
$excludedIps = $this->getAttributes('excluded_ips', 'global_excluded_ips');
|
||||
|
||||
if (!empty($excludedIps)) {
|
||||
$ip = IP::fromBinaryIP($this->ip);
|
||||
if ($ip->isInRanges($excludedIps)) {
|
||||
Common::printDebug('Visitor IP ' . $ip->toString() . ' is excluded from being tracked');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getAttributes($siteAttribute, $globalAttribute)
|
||||
{
|
||||
if (!isset($this->siteCache[$this->idSite])) {
|
||||
$this->siteCache[$this->idSite] = array();
|
||||
}
|
||||
try {
|
||||
if (empty($this->siteCache[$this->idSite])) {
|
||||
$this->siteCache[$this->idSite] = Cache::getCacheWebsiteAttributes($this->idSite);
|
||||
}
|
||||
if (isset($this->siteCache[$this->idSite][$siteAttribute])) {
|
||||
return $this->siteCache[$this->idSite][$siteAttribute];
|
||||
}
|
||||
} catch (UnexpectedWebsiteFoundException $e) {
|
||||
$cached = Cache::getCacheGeneral();
|
||||
if ($globalAttribute && isset($cached[$globalAttribute])) {
|
||||
return $cached[$globalAttribute];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if request URL is excluded
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUrlExcluded()
|
||||
{
|
||||
$excludedUrls = $this->getAttributes('exclude_unknown_urls', null);
|
||||
$siteUrls = $this->getAttributes('urls', null);
|
||||
|
||||
if (!empty($excludedUrls) && !empty($siteUrls)) {
|
||||
$url = $this->request->getParam('url');
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
$trackingUrl = new SiteUrls();
|
||||
$urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $siteUrls));
|
||||
|
||||
$idSites = $trackingUrl->getIdSitesMatchingUrl($parsedUrl, $urls);
|
||||
$isUrlExcluded = !isset($idSites) || !in_array($this->idSite, $idSites);
|
||||
|
||||
return $isUrlExcluded;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified user agent should be excluded for the current site or not.
|
||||
*
|
||||
* Visits whose user agent string contains one of the excluded_user_agents strings for the
|
||||
* site being tracked (or one of the global strings) will be excluded.
|
||||
*
|
||||
* @internal param string $this ->userAgent The user agent string.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUserAgentExcluded()
|
||||
{
|
||||
$excludedAgents = $this->getAttributes('excluded_user_agents', 'global_excluded_user_agents');
|
||||
|
||||
if (!empty($excludedAgents)) {
|
||||
foreach ($excludedAgents as $excludedUserAgent) {
|
||||
// if the excluded user agent string part is in this visit's user agent, this visit should be excluded
|
||||
if (stripos($this->userAgent, $excludedUserAgent) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Referrer is a known spammer.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isReferrerSpamExcluded()
|
||||
{
|
||||
return $this->spamFilter->isSpam($this->request);
|
||||
}
|
||||
}
|
32
msd2/tracking/piwik/core/Tracker/VisitInterface.php
Normal file
32
msd2/tracking/piwik/core/Tracker/VisitInterface.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that track visit information for the Tracker.
|
||||
*
|
||||
*/
|
||||
interface VisitInterface
|
||||
{
|
||||
/**
|
||||
* Stores the object describing the current tracking request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function setRequest(Request $request);
|
||||
|
||||
/**
|
||||
* Tracks a visit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle();
|
||||
}
|
60
msd2/tracking/piwik/core/Tracker/Visitor.php
Normal file
60
msd2/tracking/piwik/core/Tracker/Visitor.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?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\Config;
|
||||
use Piwik\Tracker;
|
||||
use Piwik\Tracker\Visit\VisitProperties;
|
||||
|
||||
class Visitor
|
||||
{
|
||||
private $visitorKnown = false;
|
||||
public $visitProperties;
|
||||
|
||||
public function __construct(VisitProperties $visitProperties, $isVisitorKnown = false)
|
||||
{
|
||||
$this->visitProperties = $visitProperties;
|
||||
$this->setIsVisitorKnown($isVisitorKnown);
|
||||
}
|
||||
|
||||
public static function makeFromVisitProperties(VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
$isKnown = $request->getMetadata('CoreHome', 'isVisitorKnown');
|
||||
return new Visitor($visitProperties, $isKnown);
|
||||
}
|
||||
|
||||
public function setVisitorColumn($column, $value)
|
||||
{
|
||||
$this->visitProperties->setProperty($column, $value);
|
||||
}
|
||||
|
||||
public function getVisitorColumn($column)
|
||||
{
|
||||
if (array_key_exists($column, $this->visitProperties->getProperties())) {
|
||||
return $this->visitProperties->getProperty($column);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isVisitorKnown()
|
||||
{
|
||||
return $this->visitorKnown === true;
|
||||
}
|
||||
|
||||
public function isNewVisit()
|
||||
{
|
||||
return !$this->isVisitorKnown();
|
||||
}
|
||||
|
||||
private function setIsVisitorKnown($isVisitorKnown)
|
||||
{
|
||||
return $this->visitorKnown = $isVisitorKnown;
|
||||
}
|
||||
}
|
16
msd2/tracking/piwik/core/Tracker/VisitorNotFoundInDb.php
Normal file
16
msd2/tracking/piwik/core/Tracker/VisitorNotFoundInDb.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
*/
|
||||
class VisitorNotFoundInDb extends \Exception
|
||||
{
|
||||
}
|
280
msd2/tracking/piwik/core/Tracker/VisitorRecognizer.php
Normal file
280
msd2/tracking/piwik/core/Tracker/VisitorRecognizer.php
Normal file
@ -0,0 +1,280 @@
|
||||
<?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\Common;
|
||||
use Piwik\EventDispatcher;
|
||||
use Piwik\Plugin\Dimension\VisitDimension;
|
||||
use Piwik\Plugins\CustomVariables\CustomVariables;
|
||||
use Piwik\Tracker\Visit\VisitProperties;
|
||||
|
||||
/**
|
||||
* Tracker service that finds the last known visit for the visitor being tracked.
|
||||
*/
|
||||
class VisitorRecognizer
|
||||
{
|
||||
/**
|
||||
* Local variable cache for the getVisitFieldsPersist() method.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $visitFieldsToSelect;
|
||||
|
||||
/**
|
||||
* See http://piwik.org/faq/how-to/faq_175/.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $trustCookiesOnly;
|
||||
|
||||
/**
|
||||
* Length of a visit in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $visitStandardLength;
|
||||
|
||||
/**
|
||||
* Number of seconds that have to pass after an action before a new action from the same visitor is
|
||||
* considered a new visit. Defaults to $visitStandardLength.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $lookBackNSecondsCustom;
|
||||
|
||||
/**
|
||||
* Forces all requests to result in new visits. For debugging only.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $trackerAlwaysNewVisitor;
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
private $model;
|
||||
|
||||
/**
|
||||
* @var EventDispatcher
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $visitRow;
|
||||
|
||||
public function __construct($trustCookiesOnly, $visitStandardLength, $lookbackNSecondsCustom, $trackerAlwaysNewVisitor,
|
||||
Model $model, EventDispatcher $eventDispatcher)
|
||||
{
|
||||
$this->trustCookiesOnly = $trustCookiesOnly;
|
||||
$this->visitStandardLength = $visitStandardLength;
|
||||
$this->lookBackNSecondsCustom = $lookbackNSecondsCustom;
|
||||
$this->trackerAlwaysNewVisitor = $trackerAlwaysNewVisitor;
|
||||
|
||||
$this->model = $model;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function setTrustCookiesOnly($trustCookiesOnly)
|
||||
{
|
||||
$this->trustCookiesOnly = $trustCookiesOnly;
|
||||
}
|
||||
|
||||
public function findKnownVisitor($configId, VisitProperties $visitProperties, Request $request)
|
||||
{
|
||||
$idSite = $request->getIdSite();
|
||||
$idVisitor = $request->getVisitorId();
|
||||
|
||||
$isVisitorIdToLookup = !empty($idVisitor);
|
||||
|
||||
if ($isVisitorIdToLookup) {
|
||||
$visitProperties->setProperty('idvisitor', $idVisitor);
|
||||
Common::printDebug("Matching visitors with: visitorId=" . bin2hex($idVisitor) . " OR configId=" . bin2hex($configId));
|
||||
} else {
|
||||
Common::printDebug("Visitor doesn't have the piwik cookie...");
|
||||
}
|
||||
|
||||
$persistedVisitAttributes = $this->getVisitorFieldsPersist();
|
||||
|
||||
$shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, $request);
|
||||
list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit($request);
|
||||
|
||||
$visitRow = $this->model->findVisitor($idSite, $configId, $idVisitor, $persistedVisitAttributes, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead);
|
||||
$this->visitRow = $visitRow;
|
||||
|
||||
$isNewVisitForced = $request->getParam('new_visit');
|
||||
$isNewVisitForced = !empty($isNewVisitForced);
|
||||
$enforceNewVisit = $isNewVisitForced || $this->trackerAlwaysNewVisitor;
|
||||
if($isNewVisitForced) {
|
||||
Common::printDebug("-> New visit forced: &new_visit=1 in request");
|
||||
}
|
||||
if($this->trackerAlwaysNewVisitor) {
|
||||
Common::printDebug("-> New visit forced: Debug.tracker_always_new_visitor = 1 in config.ini.php");
|
||||
}
|
||||
|
||||
if (!$enforceNewVisit
|
||||
&& $visitRow
|
||||
&& count($visitRow) > 0
|
||||
) {
|
||||
$visitProperties->setProperty('visit_last_action_time', strtotime($visitRow['visit_last_action_time']));
|
||||
$visitProperties->setProperty('visit_first_action_time', strtotime($visitRow['visit_first_action_time']));
|
||||
$visitProperties->setProperty('idvisitor', $visitRow['idvisitor']);
|
||||
$visitProperties->setProperty('user_id', $visitRow['user_id']);
|
||||
|
||||
Common::printDebug("The visitor is known (idvisitor = " . bin2hex($visitProperties->getProperty('idvisitor')) . ",
|
||||
config_id = " . bin2hex($configId) . ",
|
||||
last action = " . date("r", $visitProperties->getProperty('visit_last_action_time')) . ",
|
||||
first action = " . date("r", $visitProperties->getProperty('visit_first_action_time')) . ")");
|
||||
|
||||
return true;
|
||||
} else {
|
||||
Common::printDebug("The visitor was not matched with an existing visitor...");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateVisitPropertiesFromLastVisitRow(VisitProperties $visitProperties)
|
||||
{
|
||||
// These values will be used throughout the request
|
||||
foreach ($this->getVisitorFieldsPersist() as $field) {
|
||||
if ($field == 'visit_last_action_time' || $field == 'visit_first_action_time') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visitProperties->setProperty($field, $this->visitRow[$field]);
|
||||
}
|
||||
|
||||
Common::printDebug("The visit is part of an existing visit (
|
||||
idvisit = {$visitProperties->getProperty('idvisit')},
|
||||
visit_goal_buyer' = " . $visitProperties->getProperty('visit_goal_buyer') . ")");
|
||||
}
|
||||
|
||||
protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, Request $request)
|
||||
{
|
||||
$isForcedUserIdMustMatch = (false !== $request->getForcedUserId());
|
||||
|
||||
// This setting would be enabled for Intranet websites, to ensure that visitors using all the same computer config, same IP
|
||||
// are not counted as 1 visitor. In this case, we want to enforce and trust the visitor ID from the cookie.
|
||||
if (($isVisitorIdToLookup || $isForcedUserIdMustMatch) && $this->trustCookiesOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($isForcedUserIdMustMatch) {
|
||||
// if &iud was set, we must try and match both idvisitor and config_id
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a &cid= was set, we force to select this visitor (or create a new one)
|
||||
$isForcedVisitorIdMustMatch = ($request->getForcedVisitorId() != null);
|
||||
|
||||
if ($isForcedVisitorIdMustMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$isVisitorIdToLookup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, we look back 30 minutes to find a previous visitor (for performance reasons).
|
||||
* In some cases, it is useful to look back and count unique visitors more accurately. You can set custom lookback window in
|
||||
* [Tracker] window_look_back_for_visitor
|
||||
*
|
||||
* The returned value is the window range (Min, max) that the matched visitor should fall within
|
||||
*
|
||||
* @return array( datetimeMin, datetimeMax )
|
||||
*/
|
||||
protected function getWindowLookupThisVisit(Request $request)
|
||||
{
|
||||
$lookAheadNSeconds = $this->visitStandardLength;
|
||||
$lookBackNSeconds = $this->visitStandardLength;
|
||||
if ($this->lookBackNSecondsCustom > $lookBackNSeconds) {
|
||||
$lookBackNSeconds = $this->lookBackNSecondsCustom;
|
||||
}
|
||||
|
||||
$timeLookBack = date('Y-m-d H:i:s', $request->getCurrentTimestamp() - $lookBackNSeconds);
|
||||
$timeLookAhead = date('Y-m-d H:i:s', $request->getCurrentTimestamp() + $lookAheadNSeconds);
|
||||
|
||||
return array($timeLookBack, $timeLookAhead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getVisitorFieldsPersist()
|
||||
{
|
||||
if (is_null($this->visitFieldsToSelect)) {
|
||||
$fields = array(
|
||||
'idvisitor',
|
||||
'idvisit',
|
||||
'user_id',
|
||||
|
||||
'visit_exit_idaction_url',
|
||||
'visit_exit_idaction_name',
|
||||
'visitor_returning',
|
||||
'visitor_days_since_first',
|
||||
'visitor_days_since_order',
|
||||
'visitor_count_visits',
|
||||
'visit_goal_buyer',
|
||||
|
||||
'location_country',
|
||||
'location_region',
|
||||
'location_city',
|
||||
'location_latitude',
|
||||
'location_longitude',
|
||||
|
||||
'referer_name',
|
||||
'referer_keyword',
|
||||
'referer_type',
|
||||
);
|
||||
|
||||
$dimensions = VisitDimension::getAllDimensions();
|
||||
|
||||
foreach ($dimensions as $dimension) {
|
||||
if ($dimension->hasImplementedEvent('onExistingVisit') || $dimension->hasImplementedEvent('onAnyGoalConversion')) {
|
||||
$fields[] = $dimension->getColumnName();
|
||||
}
|
||||
|
||||
foreach ($dimension->getRequiredVisitFields() as $field) {
|
||||
$fields[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This event collects a list of [visit entity](/guides/persistence-and-the-mysql-backend#visits) properties that should be loaded when reading
|
||||
* the existing visit. Properties that appear in this list will be available in other tracking
|
||||
* events such as 'onExistingVisit'.
|
||||
*
|
||||
* Plugins can use this event to load additional visit entity properties for later use during tracking.
|
||||
*
|
||||
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
$this->eventDispatcher->postEvent('Tracker.getVisitFieldsToPersist', array(&$fields));
|
||||
|
||||
array_unshift($fields, 'visit_first_action_time');
|
||||
array_unshift($fields, 'visit_last_action_time');
|
||||
|
||||
for ($index = 1; $index <= CustomVariables::getNumUsableCustomVariables(); $index++) {
|
||||
$fields[] = 'custom_var_k' . $index;
|
||||
$fields[] = 'custom_var_v' . $index;
|
||||
}
|
||||
|
||||
$this->visitFieldsToSelect = array_unique($fields);
|
||||
}
|
||||
|
||||
return $this->visitFieldsToSelect;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user