update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit d885a4baa9
5833 changed files with 418860 additions and 226988 deletions

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,6 +12,8 @@ namespace Piwik\Tracker;
use Exception;
use Piwik\Common;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Plugin\Manager;
use Piwik\Tracker;
/**
@ -20,70 +22,132 @@ use Piwik\Tracker;
*/
abstract class Action
{
const TYPE_PAGE_URL = 1;
const TYPE_OUTLINK = 2;
const TYPE_DOWNLOAD = 3;
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_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 = 10; // Alias TYPE_EVENT_CATEGORY
const TYPE_EVENT_CATEGORY = 10;
const TYPE_EVENT_ACTION = 11;
const TYPE_EVENT_NAME = 12;
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_CONTENT,
self::TYPE_SITE_SEARCH,
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 ActionClickUrl|ActionPageview|ActionSiteSearch
* @return Action
*/
static public function factory(Request $request)
public static function factory(Request $request)
{
$downloadUrl = $request->getParam('download');
if (!empty($downloadUrl)) {
return new ActionClickUrl(self::TYPE_DOWNLOAD, $downloadUrl, $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;
}
}
$outlinkUrl = $request->getParam('link');
if (!empty($outlinkUrl)) {
return new ActionClickUrl(self::TYPE_OUTLINK, $outlinkUrl, $request);
}
$url = $request->getParam('url');
$eventCategory = $request->getParam('e_c');
$eventAction = $request->getParam('e_a');
if(strlen($eventCategory) > 0 && strlen($eventAction) > 0 ) {
return new ActionEvent($eventCategory, $eventAction, $url, $request);
}
$action = new ActionSiteSearch($url, $request);
if ($action->isSearchDetected()) {
if (!empty($action)) {
return $action;
}
return new ActionPageview($url, $request);
return new ActionPageview($request);
}
/**
* @var Request
*/
protected $request;
private static function getPriority(Action $actionType)
{
$key = array_search($actionType->getActionType(), self::$factoryPriority);
private $idLinkVisitAction;
private $actionIdsCached = array();
private $actionName;
private $actionType;
private $actionUrl;
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;
$this->request = $request;
}
/**
@ -96,6 +160,14 @@ abstract class Action
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;
@ -108,8 +180,7 @@ abstract class Action
public function getCustomVariables()
{
$customVariables = $this->request->getCustomVariables($scope = 'page');
return $customVariables;
return $this->request->getCustomVariables($scope = 'page');
}
// custom_float column
@ -118,24 +189,28 @@ abstract class Action
return false;
}
protected function setActionName($name)
{
$name = PageUrl::cleanupString((string)$name);
$this->actionName = $name;
$this->actionName = PageUrl::cleanupString((string)$name);
}
protected function setActionUrl($url)
{
$urlBefore = $url;
$this->rawActionUrl = PageUrl::getUrlIfLookValid($url);
$url = PageUrl::excludeQueryParametersFromUrl($url, $this->request->getIdSite());
if ($url != $urlBefore) {
Common::printDebug(' Before was "' . $urlBefore . '"');
$this->actionUrl = PageUrl::getUrlIfLookValid($url);
if ($url != $this->rawActionUrl) {
Common::printDebug(' Before was "' . $this->rawActionUrl . '"');
Common::printDebug(' After is "' . $url . '"');
}
}
protected function setActionUrlWithoutExcludingParameters($url)
{
$url = PageUrl::getUrlIfLookValid($url);
$this->rawActionUrl = $url;
$this->actionUrl = $url;
}
@ -144,14 +219,26 @@ abstract class Action
protected function getUrlAndType()
{
$url = $this->getActionUrl();
if (!empty($url)) {
// normalize urls by stripping protocol and www
$url = PageUrl::normalizeUrl($url);
return array($url['url'], Tracker\Action::TYPE_PAGE_URL, $url['prefixId']);
return array($url['url'], self::TYPE_PAGE_URL, $url['prefixId']);
}
return false;
}
public function setCustomField($field, $value)
{
$this->customFields[$field] = $value;
}
public function getCustomFields()
{
return $this->customFields;
}
public function getIdActionUrl()
{
$idUrl = $this->actionIdsCached['idaction_url'];
@ -159,7 +246,6 @@ abstract class Action
return (int)$idUrl;
}
public function getIdActionUrlForEntryAndExitIds()
{
return $this->getIdActionUrl();
@ -172,9 +258,10 @@ abstract class Action
public function getIdActionName()
{
if(!isset($this->actionIdsCached['idaction_name'])) {
if (!isset($this->actionIdsCached['idaction_name'])) {
return false;
}
return $this->actionIdsCached['idaction_name'];
}
@ -188,24 +275,17 @@ abstract class Action
return $this->idLinkVisitAction;
}
public function writeDebugInfo()
{
$type = self::getTypeAsString($this->getActionType());
Common::printDebug("Action is a $type,
Action name = " . $this->getActionName() . ",
Action URL = " . $this->getActionUrl());
return true;
}
public static function getTypeAsString($type)
{
$class = new \ReflectionClass("\\Piwik\\Tracker\\Action");
$class = new \ReflectionClass("\\Piwik\\Tracker\\Action");
$constants = $class->getConstants();
$typeId = array_search($type, $constants);
if($typeId === false) {
if (false === $typeId) {
throw new Exception("Unexpected action type " . $type);
}
return str_replace('TYPE_', '', $typeId);
}
@ -220,13 +300,38 @@ abstract class Action
*/
public function loadIdsFromLogActionTable()
{
if(!empty($this->actionIdsCached)) {
if (!empty($this->actionIdsCached)) {
return;
}
$actions = $this->getActionsToLookup();
/** @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, 'count');
if(empty($actions)) {
if (empty($actions)) {
return;
}
@ -239,72 +344,101 @@ abstract class Action
/**
* Records in the DB the association between the visit and this action.
*
* @param int $idVisit is the ID of the current visit in the DB table log_visit
* @param $visitorIdCookie
* @param int $idReferrerActionUrl is the ID of the last action done by the current visit.
* @param $idReferrerActionName
* @param int $timeSpentReferrerAction is the number of seconds since the last action was done.
* It is directly related to idReferrerActionUrl.
* @param Visitor $visitor
*/
public function record($idVisit, $visitorIdCookie, $idReferrerActionUrl, $idReferrerActionName, $timeSpentReferrerAction)
public function record(Visitor $visitor, $idReferrerActionUrl, $idReferrerActionName)
{
$this->loadIdsFromLogActionTable();
$idActionName = in_array($this->getActionType(), array(Tracker\Action::TYPE_PAGE_TITLE,
Tracker\Action::TYPE_PAGE_URL,
Tracker\Action::TYPE_SITE_SEARCH
))
? (int)$this->getIdActionName()
: null;
$visitAction = array(
'idvisit' => $idVisit,
'idsite' => $this->request->getIdSite(),
'idvisitor' => $visitorIdCookie,
'server_time' => Tracker::getDatetimeFromTimestamp($this->request->getCurrentTimestamp()),
'idaction_url' => $this->getIdActionUrl(),
'idaction_name' => $idActionName,
'idaction_url_ref' => $idReferrerActionUrl,
'idaction_name_ref' => $idReferrerActionName,
'time_spent_ref_action' => $timeSpentReferrerAction
'idvisit' => $visitor->getVisitorColumn('idvisit'),
'idsite' => $this->request->getIdSite(),
'idvisitor' => $visitor->getVisitorColumn('idvisitor'),
'idaction_url' => $this->getIdActionUrl(),
'idaction_url_ref' => $idReferrerActionUrl,
'idaction_name_ref' => $idReferrerActionName
);
foreach($this->actionIdsCached as $field => $idAction) {
$visitAction[$field] = $idAction;
/** @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 (!empty($customValue)) {
$visitAction[self::DB_COLUMN_CUSTOM_FLOAT] = $customValue;
$visitAction[self::DB_COLUMN_CUSTOM_FLOAT] = Common::forceDotAsSeparatorForDecimalPoint($customValue);
}
$customVariables = $this->getCustomVariables();
if (!empty($customVariables)) {
Common::printDebug("Page level Custom Variables: ");
Common::printDebug($customVariables);
}
$visitAction = array_merge($visitAction, $this->customFields);
$visitAction = array_merge($visitAction, $customVariables);
$fields = implode(", ", array_keys($visitAction));
$bind = array_values($visitAction);
$values = Common::getSqlStringFieldsArray($visitAction);
$this->idLinkVisitAction = $this->getModel()->createAction($visitAction);
$sql = "INSERT INTO " . Common::prefixTable('log_link_visit_action') . " ($fields) VALUES ($values)";
Tracker::getDatabase()->query($sql, $bind);
$this->idLinkVisitAction = Tracker::getDatabase()->lastInsertId();
$visitAction['idlink_va'] = $this->idLinkVisitAction;
Common::printDebug("Inserted new action:");
Common::printDebug($visitAction);
$visitActionDebug = $visitAction;
$visitActionDebug['idvisitor'] = bin2hex($visitActionDebug['idvisitor']);
Common::printDebug($visitActionDebug);
/**
* Triggered after successfully persisting a [visit action entity](/guides/persistence-and-the-mysql-backend#visit-actions).
*
*
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
*
* @param Action $tracker Action The Action tracker instance.
* @param array $visitAction The visit action entity that was persisted. Read
* [this](/guides/persistence-and-the-mysql-backend#visit-actions) to see what it contains.
* @deprecated
*/
Piwik::postEvent('Tracker.recordAction', array($trackerAction = $this, $visitAction));
}
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);
}
}

View file

@ -1,63 +0,0 @@
<?php
/**
* Piwik - Open source web analytics
*
* @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\Tracker;
/**
* This class represents a download or an outlink.
* This is a particular type of Action: it has no 'name'
*
*/
class ActionClickUrl extends Action
{
function __construct($type, $url, Request $request)
{
parent::__construct($type, $request);
$this->setActionUrl($url);
}
protected function getActionsToLookup()
{
return array(
// Note: we do not normalize download/oulink URL
'idaction_url' => array($this->getActionUrl(), $this->getActionType())
);
}
function writeDebugInfo()
{
parent::writeDebugInfo();
if (self::detectActionIsOutlinkOnAliasHost($this, $this->request->getIdSite())) {
Common::printDebug("INFO: The outlink URL host is one of the known host for this website. ");
}
}
/**
* Detect whether action is an outlink given host aliases
*
* @param Action $action
* @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website
*/
public static function detectActionIsOutlinkOnAliasHost(Action $action, $idSite)
{
if ($action->getActionType() != Action::TYPE_OUTLINK) {
return false;
}
$decodedActionUrl = $action->getActionUrl();
$actionUrlParsed = @parse_url($decodedActionUrl);
if (!isset($actionUrlParsed['host'])) {
return false;
}
return Visit::isHostKnownAliasHost($actionUrlParsed['host'], $idSite);
}
}

View file

@ -1,78 +0,0 @@
<?php
/**
* Piwik - Open source web analytics
*
* @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\Tracker;
/**
* An Event is composed of a URL, a Category name, an Action name, and optionally a Name and Value.
*
*/
class ActionEvent extends Action
{
function __construct($eventCategory, $eventAction, $url, Request $request)
{
parent::__construct(Action::TYPE_EVENT, $request);
$this->setActionUrl($url);
$this->eventCategory = trim($eventCategory);
$this->eventAction = trim($eventAction);
$this->eventName = trim($request->getParam('e_n'));
$this->eventValue = trim($request->getParam('e_v'));
}
function getCustomFloatValue()
{
return $this->eventValue;
}
protected function getActionsToLookup()
{
$actions = array(
'idaction_url' => $this->getUrlAndType()
);
if(strlen($this->eventName) > 0) {
$actions['idaction_name'] = array($this->eventName, Action::TYPE_EVENT_NAME);
}
if(strlen($this->eventCategory) > 0) {
$actions['idaction_event_category'] = array($this->eventCategory, Action::TYPE_EVENT_CATEGORY);
}
if(strlen($this->eventAction) > 0) {
$actions['idaction_event_action'] = array($this->eventAction, Action::TYPE_EVENT_ACTION);
}
return $actions;
}
// Do not track this Event URL as Entry/Exit Page URL (leave the existing entry/exit)
public function getIdActionUrlForEntryAndExitIds()
{
return false;
}
// Do not track this Event Name as Entry/Exit Page Title (leave the existing entry/exit)
public function getIdActionNameForEntryAndExitIds()
{
return false;
}
public function writeDebugInfo()
{
$write = parent::writeDebugInfo();
if($write) {
Common::printDebug("Event Category = " . $this->eventCategory . ",
Event Action = " . $this->eventAction . ",
Event Name = " . $this->eventName . ",
Event Value = " . $this->getCustomFloatValue());
}
return $write;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,8 +11,6 @@ namespace Piwik\Tracker;
use Piwik\Config;
use Piwik\Tracker;
/**
* This class represents a page view, tracking URL, page title and generation time.
*
@ -21,10 +19,11 @@ class ActionPageview extends Action
{
protected $timeGeneration = false;
function __construct($url, Request $request)
public function __construct(Request $request)
{
parent::__construct(Action::TYPE_PAGE_URL, $request);
$url = $request->getParam('url');
$this->setActionUrl($url);
$actionName = $request->getParam('action_name');
@ -38,34 +37,54 @@ class ActionPageview extends Action
{
return array(
'idaction_name' => array($this->getActionName(), Action::TYPE_PAGE_TITLE),
'idaction_url' => $this->getUrlAndType()
'idaction_url' => $this->getUrlAndType()
);
}
function getCustomFloatValue()
public function getCustomFloatValue()
{
return $this->request->getPageGenerationTime();
}
protected function cleanupActionName($actionName)
public static function shouldHandle(Request $request)
{
return true;
}
private function cleanupActionName($actionName)
{
// get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067)
$actionCategoryDelimiter = isset(Config::getInstance()->General['action_category_delimiter'])
? Config::getInstance()->General['action_category_delimiter']
: Config::getInstance()->General['action_url_category_delimiter'];
$actionCategoryDelimiter = $this->getActionCategoryDelimiter();
// create an array of the categories delimited by the delimiter
$split = explode($actionCategoryDelimiter, $actionName);
$split = $this->trimEveryCategory($split);
$split = $this->removeEmptyCategories($split);
// trim every category
$split = array_map('trim', $split);
// remove empty categories
$split = array_filter($split, 'strlen');
// rebuild the name from the array of cleaned categories
$actionName = implode($actionCategoryDelimiter, $split);
return $actionName;
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_url_category_delimiter'];
}
}

View file

@ -1,254 +0,0 @@
<?php
/**
* Piwik - Open source web analytics
*
* @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\Tracker;
use Piwik\UrlHelper;
/**
* This class represents a search on the site.
* - Its name is the search keyword
* - by default the URL is not recorded (since it's not used)
* - tracks site search result count and site search category as custom variables
*
*/
class ActionSiteSearch extends Action
{
private $searchCategory = false;
private $searchCount = false;
const CVAR_KEY_SEARCH_CATEGORY = '_pk_scat';
const CVAR_KEY_SEARCH_COUNT = '_pk_scount';
const CVAR_INDEX_SEARCH_CATEGORY = '4';
const CVAR_INDEX_SEARCH_COUNT = '5';
function __construct($url, Request $request)
{
parent::__construct(Action::TYPE_SITE_SEARCH, $request);
$this->originalUrl = $url;
}
protected function getActionsToLookup()
{
return array(
'idaction_name' => array($this->getActionName(), Action::TYPE_SITE_SEARCH),
);
}
public function getIdActionUrl()
{
// Site Search, by default, will not track URL. We do not want URL to appear as "Page URL not defined"
// so we specifically set it to NULL in the table (the archiving query does IS NOT NULL)
return null;
}
public function getCustomFloatValue()
{
return $this->request->getPageGenerationTime();
}
function isSearchDetected()
{
$siteSearch = $this->detectSiteSearch($this->originalUrl);
if(empty($siteSearch)) {
return false;
}
list($actionName, $url, $category, $count) = $siteSearch;
if (!empty($category)) {
$this->searchCategory = trim($category);
}
if ($count !== false) {
$this->searchCount = $count;
}
$this->setActionName($actionName);
$this->setActionUrl($url);
return true;
}
public function getCustomVariables()
{
$customVariables = parent::getCustomVariables();
// Enrich Site Search actions with Custom Variables, overwriting existing values
if (!empty($this->searchCategory)) {
if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY])) {
Common::printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_CATEGORY . " for this page view");
}
$customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY] = self::CVAR_KEY_SEARCH_CATEGORY;
$customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_CATEGORY] = Request::truncateCustomVariable($this->searchCategory);
}
if ($this->searchCount !== false) {
if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT])) {
Common::printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_COUNT . " for this page view");
}
$customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT] = self::CVAR_KEY_SEARCH_COUNT;
$customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_COUNT] = (int)$this->searchCount;
}
return $customVariables;
}
protected function detectSiteSearchFromUrl($website, $parsedUrl)
{
$doRemoveSearchParametersFromUrl = true;
$separator = '&';
$count = $actionName = $categoryName = false;
$keywordParameters = isset($website['sitesearch_keyword_parameters'])
? $website['sitesearch_keyword_parameters']
: array();
$queryString = (!empty($parsedUrl['query']) ? $parsedUrl['query'] : '') . (!empty($parsedUrl['fragment']) ? $separator . $parsedUrl['fragment'] : '');
$parametersRaw = UrlHelper::getArrayFromQueryString($queryString);
// strtolower the parameter names for smooth site search detection
$parameters = array();
foreach ($parametersRaw as $k => $v) {
$parameters[Common::mb_strtolower($k)] = $v;
}
// decode values if they were sent from a client using another charset
$pageEncoding = $this->request->getParam('cs');
PageUrl::reencodeParameters($parameters, $pageEncoding);
// Detect Site Search keyword
foreach ($keywordParameters as $keywordParameterRaw) {
$keywordParameter = Common::mb_strtolower($keywordParameterRaw);
if (!empty($parameters[$keywordParameter])) {
$actionName = $parameters[$keywordParameter];
break;
}
}
if (empty($actionName)) {
return false;
}
$categoryParameters = isset($website['sitesearch_category_parameters'])
? $website['sitesearch_category_parameters']
: array();
foreach ($categoryParameters as $categoryParameterRaw) {
$categoryParameter = Common::mb_strtolower($categoryParameterRaw);
if (!empty($parameters[$categoryParameter])) {
$categoryName = $parameters[$categoryParameter];
break;
}
}
if (isset($parameters['search_count'])
&& $this->isValidSearchCount($parameters['search_count'])
) {
$count = $parameters['search_count'];
}
// Remove search kwd from URL
if ($doRemoveSearchParametersFromUrl) {
// @see excludeQueryParametersFromUrl()
// Excluded the detected parameters from the URL
$parametersToExclude = array($categoryParameterRaw, $keywordParameterRaw);
if(isset($parsedUrl['query'])) {
$parsedUrl['query'] = UrlHelper::getQueryStringWithExcludedParameters(UrlHelper::getArrayFromQueryString($parsedUrl['query']), $parametersToExclude);
}
if(isset($parsedUrl['fragment'])) {
$parsedUrl['fragment'] = UrlHelper::getQueryStringWithExcludedParameters(UrlHelper::getArrayFromQueryString($parsedUrl['fragment']), $parametersToExclude);
}
}
$url = UrlHelper::getParseUrlReverse($parsedUrl);
if (is_array($actionName)) {
$actionName = reset($actionName);
}
$actionName = trim(urldecode($actionName));
if (empty($actionName)) {
return false;
}
if (is_array($categoryName)) {
$categoryName = reset($categoryName);
}
$categoryName = trim(urldecode($categoryName));
return array($url, $actionName, $categoryName, $count);
}
protected function isValidSearchCount($count)
{
return is_numeric($count) && $count >= 0;
}
protected function detectSiteSearch($originalUrl)
{
$website = Cache::getCacheWebsiteAttributes($this->request->getIdSite());
if (empty($website['sitesearch'])) {
Common::printDebug("Internal 'Site Search' tracking is not enabled for this site. ");
return false;
}
$actionName = $url = $categoryName = $count = false;
$originalUrl = PageUrl::cleanupUrl($originalUrl);
// Detect Site search from Tracking API parameters rather than URL
$searchKwd = $this->request->getParam('search');
if (!empty($searchKwd)) {
$actionName = $searchKwd;
$isCategoryName = $this->request->getParam('search_cat');
if (!empty($isCategoryName)) {
$categoryName = $isCategoryName;
}
$isCount = $this->request->getParam('search_count');
if ($this->isValidSearchCount($isCount)) {
$count = $isCount;
}
}
if (empty($actionName)) {
$parsedUrl = @parse_url($originalUrl);
// Detect Site Search from URL query parameters
if (!empty($parsedUrl['query']) || !empty($parsedUrl['fragment'])) {
// array($url, $actionName, $categoryName, $count);
$searchInfo = $this->detectSiteSearchFromUrl($website, $parsedUrl);
if (!empty($searchInfo)) {
list ($url, $actionName, $categoryName, $count) = $searchInfo;
}
}
}
$actionName = trim($actionName);
$categoryName = trim($categoryName);
if (empty($actionName)) {
Common::printDebug("(this is not a Site Search request)");
return false;
}
Common::printDebug("Detected Site Search keyword '$actionName'. ");
if (!empty($categoryName)) {
Common::printDebug("- Detected Site Search Category '$categoryName'. ");
}
if ($count !== false) {
Common::printDebug("- Search Results Count was '$count'. ");
}
if ($url != $originalUrl) {
Common::printDebug("NOTE: The Page URL was changed / removed, during the Site Search detection, was '$originalUrl', now is '$url'");
}
return array(
$actionName,
$url,
$categoryName,
$count
);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,11 +8,11 @@
*/
namespace Piwik\Tracker;
use Piwik\Access;
use Piwik\ArchiveProcessor\Rules;
use Piwik\CacheFile;
use Piwik\Cache as PiwikCache;
use Piwik\Common;
use Piwik\Config;
use Piwik\Log;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Tracker;
@ -23,19 +23,29 @@ use Piwik\Tracker;
*/
class Cache
{
private static $cacheIdGeneral = 'general';
/**
* Public for tests only
* @var CacheFile
* @var \Piwik\Cache\Lazy
*/
static public $trackerCache = null;
public static $cache;
static protected function getInstance()
/**
* @return \Piwik\Cache\Lazy
*/
private static function getCache()
{
if (is_null(self::$trackerCache)) {
$ttl = Config::getInstance()->Tracker['tracker_cache_file_ttl'];
self::$trackerCache = new CacheFile('tracker', $ttl);
if (is_null(self::$cache)) {
self::$cache = PiwikCache::getLazyCache();
}
return self::$trackerCache;
return self::$cache;
}
private static function getTtl()
{
return Config::getInstance()->Tracker['tracker_cache_file_ttl'];
}
/**
@ -44,67 +54,68 @@ class Cache
* @param int $idSite
* @return array
*/
static function getCacheWebsiteAttributes($idSite)
public static function getCacheWebsiteAttributes($idSite)
{
if($idSite == 'all') {
return array();
}
$idSite = (int)$idSite;
if($idSite <= 0) {
if ('all' == $idSite) {
return array();
}
$cache = self::getInstance();
if (($cacheContent = $cache->get($idSite)) !== false) {
$idSite = (int) $idSite;
if ($idSite <= 0) {
return array();
}
$cache = self::getCache();
$cacheId = $idSite;
$cacheContent = $cache->fetch($cacheId);
if (false !== $cacheContent) {
return $cacheContent;
}
Tracker::initCorePiwikInTrackerMode();
// save current user privilege and temporarily assume Super User privilege
$isSuperUser = Piwik::hasUserSuperUserAccess();
Piwik::setUserHasSuperUserAccess();
$content = array();
/**
* 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.");
// restore original user privilege
Piwik::setUserHasSuperUserAccess($isSuperUser);
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->set($idSite, $content);
$cache->save($cacheId, $content, self::getTtl());
}
Tracker::restoreTrackerPlugins();
return $content;
}
/**
* Clear general (global) cache
*/
static public function clearCacheGeneral()
public static function clearCacheGeneral()
{
self::getInstance()->delete('general');
self::getCache()->delete(self::$cacheIdGeneral);
}
/**
@ -113,12 +124,12 @@ class Cache
*
* @return array
*/
static public function getCacheGeneral()
public static function getCacheGeneral()
{
$cache = self::getInstance();
$cacheId = 'general';
$cache = self::getCache();
$cacheContent = $cache->fetch(self::$cacheIdGeneral);
if (($cacheContent = $cache->get($cacheId)) !== false) {
if (false !== $cacheContent) {
return $cacheContent;
}
@ -131,26 +142,29 @@ class Cache
/**
* 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 piwik_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;
}
@ -160,12 +174,11 @@ class Cache
* @param mixed $value
* @return bool
*/
static public function setCacheGeneral($value)
public static function setCacheGeneral($value)
{
$cache = self::getInstance();
$cacheId = 'general';
$cache->set($cacheId, $value);
return true;
$cache = self::getCache();
return $cache->save(self::$cacheIdGeneral, $value, self::getTtl());
}
/**
@ -173,11 +186,12 @@ class Cache
*
* @param array|int $idSites Array of idSites to clear cache for
*/
static public function regenerateCacheWebsiteAttributes($idSites = array())
public static function regenerateCacheWebsiteAttributes($idSites = array())
{
if (!is_array($idSites)) {
$idSites = array($idSites);
}
foreach ($idSites as $idSite) {
self::deleteCacheWebsiteAttributes($idSite);
self::getCacheWebsiteAttributes($idSite);
@ -189,17 +203,16 @@ class Cache
*
* @param string $idSite (website ID of the site to clear cache for
*/
static public function deleteCacheWebsiteAttributes($idSite)
public static function deleteCacheWebsiteAttributes($idSite)
{
$idSite = (int)$idSite;
self::getInstance()->delete($idSite);
self::getCache()->delete((int) $idSite);
}
/**
* Deletes all Tracker cache files
*/
static public function deleteTrackerCache()
public static function deleteTrackerCache()
{
self::getInstance()->deleteAll();
self::getCache()->flushAll();
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,8 +11,13 @@ 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.
@ -77,10 +82,14 @@ abstract class Db
*/
protected function recordQueryProfile($query, $timer)
{
if (!isset($this->queriesProfiling[$query])) $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0);
$time = $timer->getTimeMs(2);
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);
}
@ -97,13 +106,13 @@ abstract class Db
self::$profiling = false;
foreach ($this->queriesProfiling as $query => $info) {
$time = $info['sum_time_ms'];
$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";
ON DUPLICATE KEY UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time";
$this->query($queryProfiling, array($query));
}
@ -222,4 +231,63 @@ abstract class Db
* @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;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -25,6 +25,7 @@ class Mysqli extends Db
protected $username;
protected $password;
protected $charset;
protected $activeTransaction = false;
/**
* Builds the DB object
@ -38,13 +39,13 @@ class Mysqli extends Db
$this->host = null;
$this->port = null;
$this->socket = $dbInfo['unix_socket'];
} else if ($dbInfo['port'][0] == '/') {
} elseif ($dbInfo['port'][0] == '/') {
$this->host = null;
$this->port = null;
$this->socket = $dbInfo['port'];
} else {
$this->host = $dbInfo['host'];
$this->port = $dbInfo['port'];
$this->port = (int)$dbInfo['port'];
$this->socket = null;
}
$this->dbname = $dbInfo['dbname'];
@ -72,7 +73,14 @@ class Mysqli extends Db
$timer = $this->initProfiler();
}
$this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket);
$this->connection = mysqli_init();
// 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;
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());
}
@ -204,8 +212,8 @@ class Mysqli extends Db
return $result;
} catch (Exception $e) {
throw new DbException("Error query: " . $e->getMessage() . "
In query: $query
Parameters: " . var_export($parameters, true));
In query: $query
Parameters: " . var_export($parameters, true), $e->getCode());
}
}
@ -231,7 +239,7 @@ class Mysqli extends Db
{
if (!$parameters) {
$parameters = array();
} else if (!is_array($parameters)) {
} elseif (!is_array($parameters)) {
$parameters = array($parameters);
}
@ -276,4 +284,62 @@ class Mysqli extends Db
{
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);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -30,6 +30,8 @@ class Mysql extends Db
protected $password;
protected $charset;
protected $activeTransaction = false;
/**
* Builds the DB object
*
@ -40,14 +42,19 @@ class Mysql extends Db
{
if (isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') {
$this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket'];
} else if (!empty($dbInfo['port']) && $dbInfo['port'][0] == '/') {
} 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'];
$this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null;
if (isset($dbInfo['charset'])) {
$this->charset = $dbInfo['charset'];
$this->dsn .= ';charset=' . $this->charset;
}
}
public function __destruct()
@ -66,8 +73,17 @@ class Mysql extends Db
$timer = $this->initProfiler();
}
$this->connection = @new PDO($this->dsn, $this->username, $this->password, $config = array());
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 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
$config = array(
PDO::MYSQL_ATTR_FOUND_ROWS => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
);
$this->connection = @new PDO($this->dsn, $this->username, $this->password, $config);
// we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked
// the piwik.php would stay waiting for the database... bad!
// we delete the password from this object "just in case" it could be printed
@ -192,9 +208,8 @@ class Mysql extends Db
}
return $sth;
} catch (PDOException $e) {
throw new DbException("Error query: " . $e->getMessage() . "
In query: $query
Parameters: " . var_export($parameters, true));
$message = $e->getMessage() . " In query: $query Parameters: " . var_export($parameters, true);
throw new DbException("Error query: " . $message, (int) $e->getCode());
}
}
@ -234,4 +249,58 @@ class Mysql extends Db
{
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");
}
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,18 +10,19 @@ namespace Piwik\Tracker;
use Exception;
use Piwik\Common;
use Piwik\Config;
use Piwik\Date;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\ConversionDimension;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CustomVariables\CustomVariables;
use Piwik\Tracker;
use Piwik\Tracker\Visit\VisitProperties;
/**
*/
class GoalManager
{
// log_visit.visit_goal_buyer
const TYPE_BUYER_NONE = 0;
const TYPE_BUYER_ORDERED = 1;
const TYPE_BUYER_OPEN_CART = 2;
const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3;
@ -35,89 +36,78 @@ class GoalManager
const REVENUE_PRECISION = 2;
const MAXIMUM_PRODUCT_CATEGORIES = 5;
public $idGoal;
public $requestIsEcommerce;
public $isGoalAnOrder;
// 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;
/**
* @var Action
* TODO: should remove this, but it is used by getGoalColumn which is used by dimensions. should replace w/ value object.
*
* @var array
*/
protected $action = null;
protected $convertedGoals = array();
protected $isThereExistingCartInVisit = false;
/**
* @var Request
*/
protected $request;
protected $orderId;
private $currentGoal = array();
/**
* Constructor
* @param Request $request
*/
public function __construct(Request $request)
public function detectIsThereExistingCartInVisit($visitInformation)
{
$this->request = $request;
$this->init();
}
if (empty($visitInformation['visit_goal_buyer'])) {
return false;
}
function init()
{
$this->orderId = $this->request->getParam('ec_id');
$this->isGoalAnOrder = !empty($this->orderId);
$this->idGoal = $this->request->getParam('idgoal');
$this->requestIsEcommerce = ($this->idGoal == 0);
}
$goalBuyer = $visitInformation['visit_goal_buyer'];
$types = array(GoalManager::TYPE_BUYER_OPEN_CART, GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART);
function getBuyerType($existingType = GoalManager::TYPE_BUYER_NONE)
{
// Was there a Cart for this visit prior to the order?
$this->isThereExistingCartInVisit = in_array($existingType,
array(GoalManager::TYPE_BUYER_OPEN_CART,
GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART));
if (!$this->requestIsEcommerce) {
return $existingType;
}
if ($this->isGoalAnOrder) {
return self::TYPE_BUYER_ORDERED;
}
// request is Add to Cart
if ($existingType == self::TYPE_BUYER_ORDERED
|| $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART
) {
return self::TYPE_BUYER_ORDERED_AND_OPEN_CART;
}
return self::TYPE_BUYER_OPEN_CART;
return in_array($goalBuyer, $types);
}
static public function getGoalDefinitions($idSite)
public static function getGoalDefinitions($idSite)
{
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
if (isset($websiteAttributes['goals'])) {
return $websiteAttributes['goals'];
}
return array();
}
static public function getGoalDefinition($idSite, $idGoal)
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');
}
static public function getGoalIds($idSite)
public static function getGoalIds($idSite)
{
$goals = self::getGoalDefinitions($idSite);
$goals = self::getGoalDefinitions($idSite);
$goalIds = array();
foreach ($goals as $goal) {
$goalIds[] = $goal['idgoal'];
}
return $goalIds;
}
@ -127,109 +117,124 @@ class GoalManager
* @param int $idSite
* @param Action $action
* @throws Exception
* @return int Number of goals matched
* @return array[] Goals matched
*/
function detectGoalsMatchingUrl($idSite, $action)
public function detectGoalsMatchingUrl($idSite, $action)
{
if (!Common::isGoalPluginEnabled()) {
return false;
return array();
}
$decodedActionUrl = $action->getActionUrl();
$actionType = $action->getActionType();
$goals = $this->getGoalDefinitions($idSite);
$convertedGoals = array();
foreach ($goals as $goal) {
$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')
) {
continue;
}
$url = $decodedActionUrl;
// Matching on Page Title
if ($attribute == 'title') {
$url = $action->getActionName();
}
$pattern_type = $goal['pattern_type'];
$match = $this->isUrlMatchingGoal($goal, $pattern_type, $url);
if ($match) {
$goal['url'] = $decodedActionUrl;
$this->convertedGoals[] = $goal;
$convertedUrl = $this->detectGoalMatch($goal, $action);
if (!empty($convertedUrl)) {
$convertedGoals[] = array('url' => $convertedUrl) + $goal;
}
}
return count($this->convertedGoals) > 0;
return $convertedGoals;
}
function detectGoalId($idSite)
/**
* 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 string|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')
|| in_array($attribute, array('event_action', 'event_name', 'event_category')) && $actionType != Action::TYPE_EVENT
) {
return null;
}
switch ($attribute) {
case 'title':
// Matching on Page Title
$url = $action->getActionName();
break;
case 'event_action':
$url = $action->getEventAction();
break;
case 'event_name':
$url = $action->getEventName();
break;
case 'event_category':
$url = $action->getEventCategory();
break;
// url, external_website, file, manually...
default:
$url = $action->getActionUrlRaw();
break;
}
$pattern_type = $goal['pattern_type'];
$match = $this->isUrlMatchingGoal($goal, $pattern_type, $url);
if (!$match) {
return null;
}
return $action->getActionUrl();
}
public function detectGoalId($idSite, Request $request)
{
if (!Common::isGoalPluginEnabled()) {
return false;
return null;
}
$goals = $this->getGoalDefinitions($idSite);
if (!isset($goals[$this->idGoal])) {
return false;
}
$goal = $goals[$this->idGoal];
$url = $this->request->getParam('url');
$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);
$goal['revenue'] = $this->getRevenue($this->request->getGoalRevenue($goal['revenue']));
$this->convertedGoals[] = $goal;
return true;
return $goal;
}
/**
* Records one or several goals matched in this request.
*
* @param int $idSite
* @param Visitor $visitor
* @param array $visitorInformation
* @param array $visitCustomVariables
* @param Action $action
*/
public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action)
public function recordGoals(VisitProperties $visitProperties, Request $request)
{
$referrerTimestamp = $this->request->getParam('_refts');
$referrerUrl = $this->request->getParam('_ref');
$referrerCampaignName = trim(urldecode($this->request->getParam('_rcn')));
$referrerCampaignKeyword = trim(urldecode($this->request->getParam('_rck')));
$browserLanguage = $this->request->getBrowserLanguage();
$visitorInformation = $visitProperties->getProperties();
$visitCustomVariables = $request->getMetadata('CustomVariables', 'visitCustomVariables') ?: array();
$location_country = isset($visitorInformation['location_country'])
? $visitorInformation['location_country']
: Common::getCountry(
$browserLanguage,
$enableLanguageToCountryGuess = Config::getInstance()->Tracker['enable_language_to_country_guess'],
$visitorInformation['location_ip']
);
/** @var Action $action */
$action = $request->getMetadata('Actions', 'action');
$goal = array(
'idvisit' => $visitorInformation['idvisit'],
'idsite' => $idSite,
'idvisitor' => $visitorInformation['idvisitor'],
'server_time' => Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']),
'location_country' => $location_country,
'visitor_returning' => $visitorInformation['visitor_returning'],
'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'],
'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'],
'visitor_count_visits' => $visitorInformation['visitor_count_visits'],
);
$extraLocationCols = array('location_region', 'location_city', 'location_latitude', 'location_longitude');
foreach ($extraLocationCols as $col) {
if (isset($visitorInformation[$col])) {
$goal[$col] = $visitorInformation[$col];
}
}
$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::getMaxCustomVariables();
$maxCustomVariables = CustomVariables::getNumUsableCustomVariables();
for ($i = 1; $i <= $maxCustomVariables; $i++) {
if (isset($visitorInformation['custom_var_k' . $i])
@ -244,60 +249,12 @@ class GoalManager
}
}
// Attributing the correct Referrer to this conversion.
// Priority order is as follows:
// 0) In some cases, the campaign is not passed from the JS so we look it up from the current visit
// 1) Campaign name/kwd parsed in the JS
// 2) Referrer URL stored in the _ref cookie
// 3) If no info from the cookie, attribute to the current visit referrer
// 3) Default values: current referrer
$type = $visitorInformation['referer_type'];
$name = $visitorInformation['referer_name'];
$keyword = $visitorInformation['referer_keyword'];
$time = $visitorInformation['visit_first_action_time'];
// 0) In some (unknown!?) cases the campaign is not found in the attribution cookie, but the URL ref was found.
// In this case we look up if the current visit is credited to a campaign and will credit this campaign rather than the URL ref (since campaigns have higher priority)
if (empty($referrerCampaignName)
&& $type == Common::REFERRER_TYPE_CAMPAIGN
&& !empty($name)
) {
// Use default values per above
} // 1) Campaigns from 1st party cookie
elseif (!empty($referrerCampaignName)) {
$type = Common::REFERRER_TYPE_CAMPAIGN;
$name = $referrerCampaignName;
$keyword = $referrerCampaignKeyword;
$time = $referrerTimestamp;
} // 2) Referrer URL parsing
elseif (!empty($referrerUrl)) {
$referrer = new Referrer();
$referrer = $referrer->getReferrerInformation($referrerUrl, $currentUrl = '', $idSite);
// if the parsed referrer is interesting enough, ie. website or search engine
if (in_array($referrer['referer_type'], array(Common::REFERRER_TYPE_SEARCH_ENGINE, Common::REFERRER_TYPE_WEBSITE))) {
$type = $referrer['referer_type'];
$name = $referrer['referer_name'];
$keyword = $referrer['referer_keyword'];
$time = $referrerTimestamp;
}
}
$this->setCampaignValuesToLowercase($type, $name, $keyword);
$goal += array(
'referer_type' => $type,
'referer_name' => $name,
'referer_keyword' => $keyword,
// this field is currently unused
'referer_visit_server_date' => date("Y-m-d", $time),
);
// some goals are converted, so must be ecommerce Order or Cart Update
if ($this->requestIsEcommerce) {
$this->recordEcommerceGoal($goal, $visitorInformation);
$isRequestEcommerce = $request->getMetadata('Ecommerce', 'isRequestEcommerce');
if ($isRequestEcommerce) {
$this->recordEcommerceGoal($visitProperties, $request, $goal, $action);
} else {
$this->recordStandardGoals($goal, $action, $visitorInformation);
$this->recordStandardGoals($visitProperties, $request, $goal, $action);
}
}
@ -309,10 +266,13 @@ class GoalManager
*/
protected function getRevenue($revenue)
{
if (round($revenue) == $revenue) {
return $revenue;
if (round($revenue) != $revenue) {
$revenue = round($revenue, self::REVENUE_PRECISION);
}
return round($revenue, self::REVENUE_PRECISION);
$revenue = Common::forceDotAsSeparatorForDecimalPoint($revenue);
return $revenue;
}
/**
@ -320,92 +280,107 @@ class GoalManager
* 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($conversion, $visitInformation)
protected function recordEcommerceGoal(VisitProperties $visitProperties, Request $request, $conversion, $action)
{
if ($this->isThereExistingCartInVisit) {
$isThereExistingCartInVisit = $request->getMetadata('Goals', 'isThereExistingCartInVisit');
if ($isThereExistingCartInVisit) {
Common::printDebug("There is an existing cart for this visit");
}
if ($this->isGoalAnOrder) {
$conversion['idgoal'] = self::IDGOAL_ORDER;
$conversion['idorder'] = $this->orderId;
$conversion['buster'] = Common::hashStringToInt($this->orderId);
$conversion['revenue_subtotal'] = $this->getRevenue($this->request->getParam('ec_st'));
$conversion['revenue_tax'] = $this->getRevenue($this->request->getParam('ec_tx'));
$conversion['revenue_shipping'] = $this->getRevenue($this->request->getParam('ec_sh'));
$conversion['revenue_discount'] = $this->getRevenue($this->request->getParam('ec_dt'));
$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;
$debugMessage = 'The conversion is an Ecommerce Cart Update';
$conversionDimensions = ConversionDimension::getAllDimensions();
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onEcommerceCartUpdateConversion', $visitor, $action, $conversion);
}
$conversion['revenue'] = $this->getRevenue($this->request->getGoalRevenue($defaultRevenue = 0));
Common::printDebug($debugMessage . ':' . var_export($conversion, true));
// INSERT or Sync items in the Cart / Order for this visit & order
$items = $this->getEcommerceItemsFromRequest();
if ($items === false) {
$items = $this->getEcommerceItemsFromRequest($request);
if (false === $items) {
return;
}
$itemsCount = 0;
foreach ($items as $item) {
$itemsCount += $item[self::INTERNAL_ITEM_QUANTITY];
$itemsCount += $item[GoalManager::INTERNAL_ITEM_QUANTITY];
}
$conversion['items'] = $itemsCount;
if($this->isThereExistingCartInVisit) {
$updateWhere = array(
'idvisit' => $visitInformation['idvisit'],
'idgoal' => self::IDGOAL_CART,
'buster' => 0,
);
$recorded = $this->updateExistingConversion($conversion, $updateWhere);
if ($isThereExistingCartInVisit) {
$recorded = $this->getModel()->updateConversion(
$visitProperties->getProperty('idvisit'), self::IDGOAL_CART, $conversion);
} else {
$recorded = $this->insertNewConversion($conversion, $visitInformation);
$recorded = $this->insertNewConversion($conversion, $visitProperties->getProperties(), $request);
}
if ($recorded) {
$this->recordEcommerceItems($conversion, $items, $visitInformation);
$this->recordEcommerceItems($conversion, $items);
}
/**
* Triggered after successfully persisting an ecommerce conversion.
*
*
* _Note: Subscribers should be wary of doing any expensive computation here as it may slow
* the tracker down._
*
*
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
*
* @param array $conversion The conversion entity that was just persisted. See what information
* it contains [here](/guides/persistence-and-the-mysql-backend#conversions).
* @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).
* @deprecated
*/
Piwik::postEvent('Tracker.recordEcommerceGoal', array($conversion, $visitInformation));
Piwik::postEvent('Tracker.recordEcommerceGoal', array($conversion, $visitProperties->getProperties()));
}
/**
* Returns Items read from the request string
* @return array|bool
*/
protected function getEcommerceItemsFromRequest()
private function getEcommerceItemsFromRequest(Request $request)
{
$items = Common::unsanitizeInputValue($this->request->getParam('ec_items'));
$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();
}
$items = Common::json_decode($items, $assoc = true);
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;
}
@ -425,23 +400,11 @@ class GoalManager
$itemInCartBySku[$item[0]] = $item;
}
// Select all items currently in the Cart if any
$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 = ?)";
$itemsInDb = $this->getModel()->getAllItemsCurrentlyInTheCart($goal, self::ITEM_IDORDER_ABANDONED_CART);
$bind = array($goal['idvisit'],
isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART,
self::ITEM_IDORDER_ABANDONED_CART
);
$itemsInDb = Tracker::getDatabase()->fetchAll($sql, $bind);
Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true));
Common::printDebug($itemsInDb);
// 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'];
@ -492,27 +455,10 @@ class GoalManager
$itemsToInsert[] = $item;
}
}
$this->insertEcommerceItems($goal, $itemsToInsert);
}
// 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;
/**
* 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.
@ -520,14 +466,15 @@ class GoalManager
* @param array $items
* @return array $cleanedItems
*/
protected function getCleanedEcommerceItems($items)
private function getCleanedEcommerceItems($items)
{
// Clean up the items array
$cleanedItems = array();
foreach ($items as $item) {
$name = $category = $category2 = $category3 = $category4 = $category5 = false;
$price = 0;
$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;
@ -619,6 +566,7 @@ class GoalManager
$item[5] = $actionsLookedUp[$index * $columnsInEachRow + 5];
$item[6] = $actionsLookedUp[$index * $columnsInEachRow + 6];
}
return $cleanedItems;
}
@ -636,29 +584,23 @@ class GoalManager
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);
$updateParts = $sqlBind = array();
foreach ($newRow AS $name => $value) {
$updateParts[] = $name . " = ?";
$sqlBind[] = $value;
}
$sql = 'UPDATE ' . Common::prefixTable('log_conversion_item') . "
SET " . implode($updateParts, ', ') . "
WHERE idvisit = ?
AND idorder = ?
AND idaction_sku = ?";
$sqlBind[] = $newRow['idvisit'];
$sqlBind[] = $item['idorder_original_value'];
$sqlBind[] = $newRow['idaction_sku'];
Tracker::getDatabase()->query($sql, $sqlBind);
$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
@ -673,27 +615,17 @@ class GoalManager
if (empty($itemsToInsert)) {
return;
}
Common::printDebug("Ecommerce items that are added to the cart/order");
Common::printDebug($itemsToInsert);
$sql = "INSERT INTO " . Common::prefixTable('log_conversion_item') . "
(idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted,
idorder, idsite, idvisitor, server_time, idvisit)
VALUES ";
$i = 0;
$bind = array();
$items = array();
foreach ($itemsToInsert as $item) {
if ($i > 0) {
$sql .= ',';
}
$newRow = array_values($this->getItemRowEnriched($goal, $item));
$sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) ";
$i++;
$bind = array_merge($bind, $newRow);
$items[] = $this->getItemRowEnriched($goal, $item);
}
Tracker::getDatabase()->query($sql, $bind);
Common::printDebug($sql);
Common::printDebug($bind);
$this->getModel()->createEcommerceItems($items);
}
protected function getItemRowEnriched($goal, $item)
@ -706,7 +638,7 @@ class GoalManager
'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' => $item[self::INTERNAL_ITEM_PRICE],
'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
@ -718,21 +650,34 @@ class GoalManager
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($goal, $action, $visitorInformation)
protected function recordStandardGoals(VisitProperties $visitProperties, Request $request, $goal, $action)
{
foreach ($this->convertedGoals as $convertedGoal) {
$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'];
$conversion['revenue'] = $this->getRevenue($convertedGoal['revenue']);
$conversion['url'] = $convertedGoal['url'];
if (!is_null($action)) {
$conversion['idaction_url'] = $action->getIdActionUrl();
@ -742,18 +687,24 @@ class GoalManager
// If multiple Goal conversions per visit, set a cache buster
$conversion['buster'] = $convertedGoal['allow_multiple'] == 0
? '0'
: $visitorInformation['visit_last_action_time'];
: $visitProperties->getProperty('visit_last_action_time');
$this->insertNewConversion($conversion, $visitorInformation);
$conversionDimensions = ConversionDimension::getAllDimensions();
$conversion = $this->triggerHookOnDimensions($request, $conversionDimensions, 'onGoalConversion', $visitor, $action, $conversion);
$this->insertNewConversion($conversion, $visitProperties->getProperties(), $request);
/**
* Triggered after successfully recording a non-ecommerce conversion.
*
*
* _Note: Subscribers should be wary of doing any expensive computation here as it may slow
* the tracker down._
*
*
* This event is deprecated, use [Dimensions](http://developer.piwik.org/guides/dimensions) instead.
*
* @param array $conversion The conversion entity that was just persisted. See what information
* it contains [here](/guides/persistence-and-the-mysql-backend#conversions).
* @deprecated
*/
Piwik::postEvent('Tracker.recordStandardGoals', array($conversion));
}
@ -766,34 +717,31 @@ class GoalManager
* @param array $visitInformation
* @return bool
*/
protected function insertNewConversion($conversion, $visitInformation)
protected function insertNewConversion($conversion, $visitInformation, Request $request)
{
/**
* 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.
* @deprecated
*/
Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $this->request));
Piwik::postEvent('Tracker.newConversionInformation', array(&$conversion, $visitInformation, $request));
$newGoalDebug = $conversion;
$newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']);
Common::printDebug($newGoalDebug);
$fields = implode(", ", array_keys($conversion));
$bindFields = Common::getSqlStringFieldsArray($conversion);
$sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . "
($fields) VALUES ($bindFields) ";
$bind = array_values($conversion);
$result = Tracker::getDatabase()->query($sql, $bind);
$wasInserted = $this->getModel()->createConversion($conversion);
// If a record was inserted, we return true
return Tracker::getDatabase()->rowCount($result) > 0;
return $wasInserted;
}
/**
@ -816,47 +764,6 @@ class GoalManager
);
}
protected function updateExistingConversion($newGoal, $updateWhere)
{
$updateParts = $sqlBind = $updateWhereParts = array();
foreach ($newGoal AS $name => $value) {
$updateParts[] = $name . " = ?";
$sqlBind[] = $value;
}
foreach ($updateWhere as $name => $value) {
$updateWhereParts[] = $name . " = ?";
$sqlBind[] = $value;
}
$sql = 'UPDATE ' . Common::prefixTable('log_conversion') . "
SET " . implode($updateParts, ', ') . "
WHERE " . implode($updateWhereParts, ' AND ');
try {
Tracker::getDatabase()->query($sql, $sqlBind);
} catch(Exception $e){
Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage());
return false;
}
return true;
}
/**
* @param $type
* @param $name
* @param $keyword
*/
protected function setCampaignValuesToLowercase($type, &$name, &$keyword)
{
if ($type === Common::REFERRER_TYPE_CAMPAIGN) {
if (!empty($name)) {
$name = Common::mb_strtolower($name);
}
if (!empty($keyword)) {
$keyword = Common::mb_strtolower($keyword);
}
}
}
/**
* @param $goal
* @param $pattern_type
@ -865,6 +772,79 @@ class GoalManager
* @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
* @throws Exception
*/
protected function isGoalPatternMatchingUrl($goal, $pattern_type, $url)
{
switch ($pattern_type) {
case 'regex':

View 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);
}
}
}

View 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;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -22,7 +22,7 @@ class IgnoreCookie
*
* @return Cookie
*/
static public function getTrackingCookie()
public static function getTrackingCookie()
{
$cookie_name = @Config::getInstance()->Tracker['cookie_name'];
$cookie_path = @Config::getInstance()->Tracker['cookie_path'];
@ -35,7 +35,7 @@ class IgnoreCookie
*
* @return Cookie
*/
static public function getIgnoreCookie()
public static function getIgnoreCookie()
{
$cookie_name = @Config::getInstance()->Tracker['ignore_visits_cookie_name'];
$cookie_path = @Config::getInstance()->Tracker['cookie_path'];
@ -46,7 +46,7 @@ class IgnoreCookie
/**
* Set ignore (visit) cookie or deletes it if already present
*/
static public function setIgnoreCookie()
public static function setIgnoreCookie()
{
$ignoreCookie = self::getIgnoreCookie();
if ($ignoreCookie->isCookieFound()) {
@ -65,7 +65,7 @@ class IgnoreCookie
*
* @return bool True if ignore cookie found; false otherwise
*/
static public function isIgnoreCookieFound()
public static function isIgnoreCookieFound()
{
$cookie = self::getIgnoreCookie();
return $cookie->isCookieFound() && $cookie->get('ignore') === '*';

View file

@ -0,0 +1,464 @@
<?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
$whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?";
$bindSql = array(
$timeLookBack,
$timeLookAhead,
$idSite
);
if ($shouldMatchOneFieldOnly && $isVisitorIdToLookup) {
$visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $whereCommon, $bindSql);
} elseif ($shouldMatchOneFieldOnly) {
$visitRow = $this->findVisitorByConfigId($configId, $select, $from, $whereCommon, $bindSql);
} else {
$visitRow = $this->findVisitorByVisitorId($idVisitor, $select, $from, $whereCommon, $bindSql);
if (empty($visitRow)) {
$whereCommon .= ' AND user_id IS NULL ';
$visitRow = $this->findVisitorByConfigId($configId, $select, $from, $whereCommon, $bindSql);
}
}
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 log 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 = ? )";
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,6 +11,7 @@ namespace Piwik\Tracker;
use Piwik\Common;
use Piwik\Config;
use Piwik\Piwik;
use Piwik\UrlHelper;
class PageUrl
@ -37,7 +38,7 @@ class PageUrl
* @static
* @param $originalUrl
* @param $idSite
* @return bool|string
* @return bool|string Returned URL is HTML entities decoded
*/
public static function excludeQueryParametersFromUrl($originalUrl, $idSite)
{
@ -51,19 +52,22 @@ class PageUrl
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
@ -80,23 +84,28 @@ class PageUrl
);
$website = Cache::getCacheWebsiteAttributes($idSite);
$excludedParameters = isset($website['excluded_parameters'])
? $website['excluded_parameters']
: array();
if (!empty($excludedParameters)) {
Common::printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL');
}
$excludedParameters = self::getExcludedParametersFromWebsite($website);
$parametersToExclude = array_merge($excludedParameters,
self::$queryParametersToExclude,
$campaignTrackingParameters);
self::$queryParametersToExclude,
$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 true if URL fragments should be removed for a specific site,
* false if otherwise.
@ -109,7 +118,7 @@ class PageUrl
public static function shouldRemoveURLFragmentFor($idSite)
{
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
return !$websiteAttributes['keep_url_fragment'];
return empty($websiteAttributes['keep_url_fragment']);
}
/**
@ -154,8 +163,9 @@ class PageUrl
if (empty($parsedUrl)) {
return $parsedUrl;
}
if (!empty($parsedUrl['host'])) {
$parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8');
$parsedUrl['host'] = Common::mb_strtolower($parsedUrl['host'], 'UTF-8');
}
if (!empty($parsedUrl['fragment'])) {
@ -176,19 +186,24 @@ class PageUrl
public static function convertMatrixUrl($originalUrl)
{
$posFirstSemiColon = strpos($originalUrl, ";");
if ($posFirstSemiColon === false) {
if (false === $posFirstSemiColon) {
return $originalUrl;
}
$posQuestionMark = strpos($originalUrl, "?");
$replace = ($posQuestionMark === false);
$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;
}
@ -212,10 +227,12 @@ class PageUrl
{
if (is_string($value)) {
$decoded = urldecode($value);
if (@mb_check_encoding($decoded, $encoding)) {
if (function_exists('mb_check_encoding')
&& @mb_check_encoding($decoded, $encoding)) {
$value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding));
}
}
return $value;
}
@ -228,6 +245,7 @@ class PageUrl
$value = PageUrl::reencodeParameterValue($value, $encoding);
}
}
return $queryParameters;
}
@ -247,14 +265,20 @@ class PageUrl
*/
public static function reencodeParameters(&$queryParameters, $encoding = false)
{
// if query params are encoded w/ non-utf8 characters (due to browser bug or whatever),
// encode to UTF-8.
if ($encoding !== false
&& strtolower($encoding) != 'utf-8'
&& function_exists('mb_check_encoding')
) {
$queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding);
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;
}
@ -263,6 +287,7 @@ class PageUrl
$url = Common::unsanitizeInputValue($url);
$url = PageUrl::cleanupString($url);
$url = PageUrl::convertMatrixUrl($url);
return $url;
}
@ -276,6 +301,7 @@ class PageUrl
public static function reconstructNormalizedUrl($url, $prefixId)
{
$map = array_flip(self::$urlPrefixMap);
if ($prefixId !== null && isset($map[$prefixId])) {
$fullUrl = $map[$prefixId] . $url;
} else {
@ -285,7 +311,8 @@ class PageUrl
// Clean up host & hash tags, for URLs
$parsedUrl = @parse_url($fullUrl);
$parsedUrl = PageUrl::cleanupHostAndHashTag($parsedUrl);
$url = UrlHelper::getParseUrlReverse($parsedUrl);
$url = UrlHelper::getParseUrlReverse($parsedUrl);
if (!empty($url)) {
return $url;
}
@ -310,6 +337,7 @@ class PageUrl
);
}
}
return array('url' => $url, 'prefixId' => null);
}
@ -319,10 +347,30 @@ class PageUrl
if (!UrlHelper::isLookLikeUrl($url)) {
Common::printDebug("WARNING: URL looks invalid and is discarded");
$url = false;
return $url;
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;
}
}

View file

@ -1,301 +0,0 @@
<?php
/**
* Piwik - Open source web analytics
*
* @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\UrlHelper;
/**
*/
class Referrer
{
// @see detect*() referrer methods
protected $typeReferrerAnalyzed;
protected $nameReferrerAnalyzed;
protected $keywordReferrerAnalyzed;
protected $referrerHost;
protected $referrerUrl;
protected $referrerUrlParse;
protected $currentUrlParse;
protected $idsite;
// Used to prefix when a adsense referrer is detected
const LABEL_PREFIX_ADWORDS_KEYWORD = '(adwords) ';
const LABEL_ADWORDS_NAME = 'AdWords';
/**
* Returns an array containing the following information:
* - referer_type
* - direct -- absence of referrer URL OR referrer URL has the same host
* - site -- based on the referrer URL
* - search_engine -- based on the referrer URL
* - campaign -- based on campaign URL parameter
*
* - referer_name
* - ()
* - piwik.net -- site host name
* - google.fr -- search engine host name
* - adwords-search -- campaign name
*
* - referer_keyword
* - ()
* - ()
* - my keyword
* - my paid keyword
* - ()
* - ()
*
* - referer_url : the same for all the referrer types
*
* @param string $referrerUrl must be URL Encoded
* @param string $currentUrl
* @param int $idSite
* @return array
*/
public function getReferrerInformation($referrerUrl, $currentUrl, $idSite)
{
$this->idsite = $idSite;
// default values for the referer_* fields
$referrerUrl = Common::unsanitizeInputValue($referrerUrl);
if (!empty($referrerUrl)
&& !UrlHelper::isLookLikeUrl($referrerUrl)
) {
$referrerUrl = '';
}
$currentUrl = PageUrl::cleanupUrl($currentUrl);
$this->referrerUrl = $referrerUrl;
$this->referrerUrlParse = @parse_url($this->referrerUrl);
$this->currentUrlParse = @parse_url($currentUrl);
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY;
$this->nameReferrerAnalyzed = '';
$this->keywordReferrerAnalyzed = '';
$this->referrerHost = '';
if (isset($this->referrerUrlParse['host'])) {
$this->referrerHost = $this->referrerUrlParse['host'];
}
$referrerDetected = $this->detectReferrerCampaign();
if (!$referrerDetected) {
if ($this->detectReferrerDirectEntry()
|| $this->detectReferrerSearchEngine()
) {
$referrerDetected = true;
}
}
if (!empty($this->referrerHost)
&& !$referrerDetected
) {
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_WEBSITE;
$this->nameReferrerAnalyzed = Common::mb_strtolower($this->referrerHost);
}
$referrerInformation = array(
'referer_type' => $this->typeReferrerAnalyzed,
'referer_name' => $this->nameReferrerAnalyzed,
'referer_keyword' => $this->keywordReferrerAnalyzed,
'referer_url' => $this->referrerUrl,
);
return $referrerInformation;
}
/**
* Search engine detection
* @return bool
*/
protected function detectReferrerSearchEngine()
{
$searchEngineInformation = UrlHelper::extractSearchEngineInformationFromUrl($this->referrerUrl);
/**
* Triggered when detecting the search engine of a referrer URL.
*
* Plugins can use this event to provide custom search engine detection
* logic.
*
* @param array &$searchEngineInformation An array with the following information:
*
* - **name**: The search engine name.
* - **keywords**: The search keywords used.
*
* This parameter is initialized to the results
* of Piwik's default search engine detection
* logic.
* @param string referrerUrl The referrer URL from the tracking request.
*/
Piwik::postEvent('Tracker.detectReferrerSearchEngine', array(&$searchEngineInformation, $this->referrerUrl));
if ($searchEngineInformation === false) {
return false;
}
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_SEARCH_ENGINE;
$this->nameReferrerAnalyzed = $searchEngineInformation['name'];
$this->keywordReferrerAnalyzed = $searchEngineInformation['keywords'];
return true;
}
/**
* @param string $string
* @return bool
*/
protected function detectCampaignFromString($string)
{
foreach ($this->campaignNames as $campaignNameParameter) {
$campaignName = trim(urldecode(UrlHelper::getParameterFromQueryString($string, $campaignNameParameter)));
if (!empty($campaignName)) {
break;
}
}
if (empty($campaignName)) {
return false;
}
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_CAMPAIGN;
$this->nameReferrerAnalyzed = $campaignName;
foreach ($this->campaignKeywords as $campaignKeywordParameter) {
$campaignKeyword = UrlHelper::getParameterFromQueryString($string, $campaignKeywordParameter);
if (!empty($campaignKeyword)) {
$this->keywordReferrerAnalyzed = trim(urldecode($campaignKeyword));
break;
}
}
return !empty($this->keywordReferrerAnalyzed);
}
protected function detectReferrerCampaignFromLandingUrl()
{
if (!isset($this->currentUrlParse['query'])
&& !isset($this->currentUrlParse['fragment'])
) {
return false;
}
$campaignParameters = Common::getCampaignParameters();
$this->campaignNames = $campaignParameters[0];
$this->campaignKeywords = $campaignParameters[1];
$found = false;
// 1) Detect campaign from query string
if (isset($this->currentUrlParse['query'])) {
$found = $this->detectCampaignFromString($this->currentUrlParse['query']);
}
// 2) Detect from fragment #hash
if (!$found
&& isset($this->currentUrlParse['fragment'])
) {
$this->detectCampaignFromString($this->currentUrlParse['fragment']);
}
}
/**
* We have previously tried to detect the campaign variables in the URL
* so at this stage, if the referrer host is the current host,
* or if the referrer host is any of the registered URL for this website,
* it is considered a direct entry
* @return bool
*/
protected function detectReferrerDirectEntry()
{
if (!empty($this->referrerHost)) {
// is the referrer host the current host?
if (isset($this->currentUrlParse['host'])) {
$currentHost = mb_strtolower($this->currentUrlParse['host'], 'UTF-8');
if ($currentHost == mb_strtolower($this->referrerHost, 'UTF-8')) {
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY;
return true;
}
}
if (Visit::isHostKnownAliasHost($this->referrerHost, $this->idsite)) {
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_DIRECT_ENTRY;
return true;
}
}
return false;
}
protected function detectCampaignKeywordFromReferrerUrl()
{
if(!empty($this->nameReferrerAnalyzed)
&& !empty($this->keywordReferrerAnalyzed)) {
// keyword is already set, we skip
return true;
}
// Set the Campaign keyword to the keyword found in the Referrer URL if any
if(!empty($this->nameReferrerAnalyzed)) {
$referrerUrlInfo = UrlHelper::extractSearchEngineInformationFromUrl($this->referrerUrl);
if (!empty($referrerUrlInfo['keywords'])) {
$this->keywordReferrerAnalyzed = $referrerUrlInfo['keywords'];
}
}
// Set the keyword, to the hostname found, in a Adsense Referrer URL '&url=' parameter
if (empty($this->keywordReferrerAnalyzed)
&& !empty($this->referrerUrlParse['query'])
&& !empty($this->referrerHost)
&& (strpos($this->referrerHost, 'googleads') !== false || strpos($this->referrerHost, 'doubleclick') !== false)
) {
// This parameter sometimes is found & contains the page with the adsense ad bringing visitor to our site
$value = $this->getParameterValueFromReferrerUrl('url');
if (!empty($value)) {
$parsedAdsenseReferrerUrl = parse_url($value);
if (!empty($parsedAdsenseReferrerUrl['host'])) {
if(empty($this->nameReferrerAnalyzed)) {
$type = $this->getParameterValueFromReferrerUrl('ad_type');
$type = $type ? " ($type)" : '';
$this->nameReferrerAnalyzed = self::LABEL_ADWORDS_NAME . $type;
$this->typeReferrerAnalyzed = Common::REFERRER_TYPE_CAMPAIGN;
}
$this->keywordReferrerAnalyzed = self::LABEL_PREFIX_ADWORDS_KEYWORD . $parsedAdsenseReferrerUrl['host'];
}
}
}
}
/**
* @return string
*/
protected function getParameterValueFromReferrerUrl($adsenseReferrerParameter)
{
$value = trim(urldecode(UrlHelper::getParameterFromQueryString($this->referrerUrlParse['query'], $adsenseReferrerParameter)));
return $value;
}
/**
* @return bool
*/
protected function detectReferrerCampaign()
{
$this->detectReferrerCampaignFromLandingUrl();
$this->detectCampaignKeywordFromReferrerUrl();
if ($this->typeReferrerAnalyzed != Common::REFERRER_TYPE_CAMPAIGN) {
return false;
}
// if we detected a campaign but there is still no keyword set, we set the keyword to the Referrer host
if(empty($this->keywordReferrerAnalyzed)) {
$this->keywordReferrerAnalyzed = $this->referrerHost;
}
$this->keywordReferrerAnalyzed = Common::mb_strtolower($this->keywordReferrerAnalyzed);
$this->nameReferrerAnalyzed = Common::mb_strtolower($this->nameReferrerAnalyzed);
return true;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,12 +11,16 @@ 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\Registry;
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.
@ -24,19 +28,34 @@ use Piwik\Tracker;
*/
class Request
{
private $cdtCache;
private $idSiteCache;
private $paramsCache = array();
/**
* @var array
*/
protected $params;
protected $forcedVisitorId = false;
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';
const CUSTOM_TIMESTAMP_DOES_NOT_REQUIRE_TOKENAUTH_WHEN_NEWER_THAN = 14400; // 4 hours
/**
* @param $params
* @param bool|string $tokenAuth
@ -47,21 +66,45 @@ class Request
$params = array();
}
$this->params = $params;
$this->rawParams = $params;
$this->tokenAuth = $tokenAuth;
$this->timestamp = time();
$this->enforcedIp = false;
$this->isEmptyRequest = empty($params);
// 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'];
$url = $_SERVER['HTTP_REFERER'];
if (!empty($url)) {
$this->params['url'] = $url;
}
}
// check for 4byte utf8 characters in url and replace them with <20>
// @TODO Remove as soon as our database tables use utf8mb4 instead of utf8
if (array_key_exists('url', $this->params) && preg_match('/[\x{10000}-\x{10FFFF}]/u', $this->params['url'])) {
Common::printDebug("Unsupport character detected. Replacing with \xEF\xBF\xBD");
$this->params['url'] = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $this->params['url']);
}
}
/**
* 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;
}
/**
@ -80,21 +123,43 @@ class Request
* 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($tokenAuthFromBulkRequest)
protected function authenticateTrackingApi($tokenAuth)
{
$shouldAuthenticate = Config::getInstance()->Tracker['tracking_requests_require_authentication'];
$shouldAuthenticate = TrackerConfig::getConfigValue('tracking_requests_require_authentication');
if ($shouldAuthenticate) {
$tokenAuth = $tokenAuthFromBulkRequest ? $tokenAuthFromBulkRequest : Common::getRequestVar('token_auth', false, 'string', $this->params);
try {
$idSite = $this->getIdSite();
$this->isAuthenticated = $this->authenticateSuperUserOrAdmin($tokenAuth, $idSite);
} catch (Exception $e) {
$this->isAuthenticated = false;
}
if (!$this->isAuthenticated) {
return;
}
Common::printDebug("token_auth is authenticated!");
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::authenticateSuperUserOrAdmin($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 {
$this->isAuthenticated = true;
Common::printDebug("token_auth authentication not required");
@ -110,9 +175,11 @@ class Request
Piwik::postEvent('Request.initAuthenticationObject');
/** @var \Piwik\Auth $auth */
$auth = Registry::get('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()) {
@ -122,10 +189,12 @@ class Request
// Now checking the list of admin token_auth cached in the Tracker config file
if (!empty($idSite) && $idSite > 0) {
$website = Cache::getCacheWebsiteAttributes($idSite);
if (array_key_exists('admin_token_auth', $website) && in_array($tokenAuth, $website['admin_token_auth'])) {
if (array_key_exists('admin_token_auth', $website) && in_array((string) $tokenAuth, $website['admin_token_auth'])) {
return true;
}
}
Common::printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated");
return false;
@ -137,13 +206,17 @@ class Request
public function getDaysSinceFirstVisit()
{
$cookieFirstVisitTimestamp = $this->getParam('_idts');
if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) {
$cookieFirstVisitTimestamp = $this->getCurrentTimestamp();
}
$daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400, $precision = 0);
if ($daysSinceFirstVisit < 0) {
$daysSinceFirstVisit = 0;
}
return $daysSinceFirstVisit;
}
@ -154,12 +227,14 @@ class Request
{
$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;
}
@ -170,12 +245,14 @@ class Request
{
$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;
}
@ -211,6 +288,15 @@ class Request
'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;
@ -252,75 +338,176 @@ class Request
'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(false, 'int'),
'rec' => array(0, 'int'),
'new_visit' => array(0, 'int'),
// Ecommerce
'ec_id' => array(false, 'string'),
'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('', 'string'),
'ec_items' => array('', 'json'),
// Events
'e_c' => array(false, 'string'),
'e_a' => array(false, 'string'),
'e_n' => array(false, 'string'),
'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(false, 'string'),
'cdt' => array(false, 'string'),
'cid' => array(false, 'string'),
'cip' => array('', 'string'),
'cdt' => array('', 'string'),
'cid' => array('', 'string'),
'uid' => array('', 'string'),
// Actions / pages
'cs' => array(false, 'string'),
'cs' => array('', 'string'),
'download' => array('', 'string'),
'link' => array('', 'string'),
'action_name' => array('', 'string'),
'search' => array('', 'string'),
'search_cat' => array(false, 'string'),
'search_cat' => 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];
$value = Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params);
if ($this->hasParam($name)) {
$this->paramsCache[$name] = Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params);
} else {
$this->paramsCache[$name] = $paramDefaultValue;
}
return $value;
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;
}
protected function isTimestampValid($time)
public function setCurrentTimestamp($timestamp)
{
return $time <= $this->getCurrentTimestamp()
&& $time > $this->getCurrentTimestamp() - 10 * 365 * 86400;
$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 < self::CUSTOM_TIMESTAMP_DOES_NOT_REQUIRE_TOKENAUTH_WHEN_NEWER_THAN;
if (!$isTimestampRecent) {
if (!$this->isAuthenticated()) {
Common::printDebug(sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow));
Common::printDebug("WARN: Tracker API 'cdt' was used with invalid token_auth");
return false;
}
}
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 - 10 * 365 * 86400;
}
public function getIdSite()
{
if (isset($this->idSiteCache)) {
return $this->idSiteCache;
}
$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.
@ -328,18 +515,41 @@ class Request
* request.
*/
Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params));
if ($idSite <= 0) {
throw new Exception('Invalid idSite: \'' . $idSite . '\'');
throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
}
$this->idSiteCache = $idSite;
return $idSite;
}
public function getUserAgent()
{
$default = @$_SERVER['HTTP_USER_AGENT'];
return Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $this->params);
$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') {
@ -348,14 +558,19 @@ class Request
$parameter = 'cvar';
}
$customVar = Common::unsanitizeInputValues(Common::getRequestVar($parameter, '', 'json', $this->params));
$cvar = Common::getRequestVar($parameter, '', 'json', $this->params);
$customVar = Common::unsanitizeInputValues($cvar);
if (!is_array($customVar)) {
return array();
}
$customVariables = array();
$maxCustomVars = CustomVariables::getMaxCustomVariables();
$maxCustomVars = CustomVariables::getNumUsableCustomVariables();
foreach ($customVar as $id => $keyValue) {
$id = (int)$id;
if ($id < 1
|| $id > $maxCustomVars
|| count($keyValue) != 2
@ -364,16 +579,15 @@ class Request
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
$key = self::truncateCustomVariable($keyValue[0]);
$value = self::truncateCustomVariable($keyValue[1]);
$customVariables['custom_var_k' . $id] = $key;
$customVariables['custom_var_v' . $id] = $value;
$customVariables['custom_var_k' . $id] = self::truncateCustomVariable($keyValue[0]);
$customVariables['custom_var_v' . $id] = self::truncateCustomVariable($keyValue[1]);
}
return $customVariables;
@ -397,6 +611,7 @@ class Request
if (!$this->shouldUseThirdPartyCookie()) {
return;
}
Common::printDebug("We manage the cookie...");
$cookie = $this->makeThirdPartyCookie();
@ -417,37 +632,53 @@ class Request
protected function getCookieName()
{
return Config::getInstance()->Tracker['cookie_name'];
return TrackerConfig::getConfigValue('cookie_name');
}
protected function getCookieExpire()
{
return $this->getCurrentTimestamp() + Config::getInstance()->Tracker['cookie_expire'];
return $this->getCurrentTimestamp() + TrackerConfig::getConfigValue('cookie_expire');
}
protected function getCookiePath()
{
return Config::getInstance()->Tracker['cookie_path'];
return TrackerConfig::getConfigValue('cookie_path');
}
/**
* Is the request for a known VisitorId, based on 1st party, 3rd party (optional) cookies or Tracking API forced Visitor ID
* 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;
// Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request?
$idVisitor = $this->getForcedVisitorId();
if (!empty($idVisitor)) {
if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
throw new Exception("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
}
Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
// 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) {
// - By default, reads the first party cookie ID
@ -462,6 +693,7 @@ class Request
}
}
}
// If a third party cookie was not found, we default to the first party cookie
if (!$found) {
$idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
@ -469,78 +701,34 @@ class Request
}
if ($found) {
$truncated = substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
$truncated = $this->truncateIdAsVisitorId($idVisitor);
$binVisitorId = @Common::hex2bin($truncated);
if (!empty($binVisitorId)) {
return $binVisitorId;
}
}
return false;
}
public function getIp()
{
if (!empty($this->enforcedIp)) {
$ipString = $this->enforcedIp;
} else {
$ipString = IP::getIpFromHeader();
}
$ip = IP::P2N($ipString);
return $ip;
return IPUtils::stringToBinaryIP($this->getIpString());
}
public function setForceIp($ip)
public function getForcedUserId()
{
if (!empty($ip)) {
$this->enforcedIp = $ip;
$userId = $this->getParam('uid');
if (strlen($userId) > 0) {
return $userId;
}
}
public function setForceDateTime($dateTime)
{
if (!is_numeric($dateTime)) {
$dateTime = strtotime($dateTime);
}
if (!empty($dateTime)) {
$this->timestamp = $dateTime;
}
}
public function setForcedVisitorId($visitorId)
{
if (!empty($visitorId)) {
$this->forcedVisitorId = $visitorId;
}
return false;
}
public function getForcedVisitorId()
{
return $this->forcedVisitorId;
}
public function overrideLocation(&$visitorInfo)
{
if (!$this->isAuthenticated()) {
return;
}
// check for location override query parameters (ie, lat, long, country, region, city)
static $locationOverrideParams = array(
'country' => array('string', 'location_country'),
'region' => array('string', 'location_region'),
'city' => array('string', 'location_city'),
'lat' => array('float', 'location_latitude'),
'long' => array('float', 'location_longitude'),
);
foreach ($locationOverrideParams as $queryParamName => $info) {
list($type, $visitorInfoKey) = $info;
$value = Common::getRequestVar($queryParamName, false, $type, $this->params);
if (!empty($value)) {
$visitorInfo[$visitorInfoKey] = $value;
}
}
return;
return $this->getParam('cid');
}
public function getPlugins()
@ -553,9 +741,9 @@ class Request
return $plugins;
}
public function getParamsCount()
public function isEmptyRequest()
{
return count($this->params);
return $this->isEmptyRequest;
}
const GENERATION_TIME_MS_MAXIMUM = 3600000; // 1 hour
@ -568,6 +756,71 @@ class Request
) {
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;
}
}

View 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
}
}

View file

@ -0,0 +1,255 @@
<?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 reponse? 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'];
}
private function getCurrentEnvironment()
{
return array(
'server' => $_SERVER
);
}
}

View file

@ -0,0 +1,182 @@
<?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}', 'Piwik &rsaquo; Error', $headerPage);
echo $headerPage . '<p>' . $this->getMessageFromException($e) . '</p>' . $trailer . $footerPage;
} else {
$this->outputApiResponse($tracker);
}
}
public function outputResponse(Tracker $tracker)
{
if (!$tracker->shouldRecordStatistics()) {
$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 => Piwik page");
echo "<a href='/'>Piwik</a> is a free/libre web <a href='http://piwik.org'>analytics</a> that lets you keep control of your data.";
} 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 (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 Piwik 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(Exception $e)
{
error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e))));
}
}

View 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');
}
}

View 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);
}
}

View file

@ -0,0 +1,58 @@
<?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\Settings\Storage;
use Piwik\Tracker;
use Piwik\Cache as PiwikCache;
/**
* Loads settings from tracker cache instead of database. If not yet present in tracker cache will cache it.
*/
class SettingsStorage extends Storage
{
protected function loadSettings()
{
$cacheId = $this->getOptionKey();
$cache = $this->getCache();
if ($cache->contains($cacheId)) {
$settings = $cache->fetch($cacheId);
} else {
$settings = parent::loadSettings();
$cache->save($cacheId, $settings);
}
return $settings;
}
public function save()
{
parent::save();
self::clearCache();
}
private function getCache()
{
return self::buildCache($this->getOptionKey());
}
public static function clearCache()
{
Cache::deleteTrackerCache();
self::buildCache()->flushAll();
}
private static function buildCache()
{
return PiwikCache::getEagerCache();
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -10,9 +10,9 @@
namespace Piwik\Tracker;
use Piwik\Common;
use Piwik\SegmentExpression;
use Piwik\Tracker;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Segment\SegmentExpression;
/**
* This class is used to query Action IDs from the log_action table.
@ -36,35 +36,22 @@ class TableLogAction
public static function loadIdsAction($actionsNameAndType)
{
// Add url prefix if not set
foreach($actionsNameAndType as &$action) {
if(count($action) == 2) {
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;
$queriedIds = $queriedIds + $insertedIds;
return $queriedIds;
}
/**
* @param $name
* @param $type
* @return string
*/
private static function getIdActionMatchingNameAndType($name, $type)
{
$sql = TableLogAction::getSqlSelectActionId();
$bind = array($name, $name, $type);
$idAction = \Piwik\Db::fetchOne($sql, $bind);
return $idAction;
}
/**
* @param $matchType
* @param $actionType
@ -76,73 +63,66 @@ class TableLogAction
// 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 '=@':
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 '!@':
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 getSqlSelectActionId()
{
$sql = "SELECT idaction, type, name
FROM " . Common::prefixTable('log_action')
. " WHERE "
. " ( hash = CRC32(?) AND name = ? AND type = ? ) ";
$sql = sprintf($sql, $where);
return $sql;
}
private static function insertNewIdsAction($actionsNameAndType, $fieldNamesToInsert)
{
$sql = "INSERT INTO " . Common::prefixTable('log_action') .
"( name, hash, type, url_prefix ) VALUES (?,CRC32(?),?,?)";
// Then, we insert all new actions in the lookup table
$inserted = array();
foreach ($fieldNamesToInsert as $fieldName) {
list($name, $type, $urlPrefix) = $actionsNameAndType[$fieldName];
Tracker::getDatabase()->query($sql, array($name, $name, $type, $urlPrefix));
$actionId = Tracker::getDatabase()->lastInsertId();
$inserted[$fieldName] = $actionId;
$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)
{
$sql = TableLogAction::getSqlSelectActionId();
$bind = array();
$i = 0;
$toQuery = array();
foreach ($actionsNameAndType as &$actionNameType) {
list($name, $type, $urlPrefix) = $actionNameType;
if (empty($name)) {
continue;
}
if ($i > 0) {
$sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) ";
}
$bind[] = $name;
$bind[] = $name;
$bind[] = $type;
$i++;
$toQuery[] = array('name' => $name, 'type' => $type);
}
// Case URL & Title are empty
if (empty($bind)) {
return false;
}
$actionIds = Tracker::getDatabase()->fetchAll($sql, $bind);
$actionIds = self::getModel()->getIdsAction($toQuery);
return $actionIds;
}
@ -151,6 +131,7 @@ class TableLogAction
// 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)) {
@ -173,10 +154,10 @@ class TableLogAction
$fieldNamesToInsert[] = $fieldName;
}
}
return array($fieldNameToActionId, $fieldNamesToInsert);
}
/**
* Convert segment expression to an action ID or an SQL expression.
*
@ -193,34 +174,37 @@ class TableLogAction
*/
public static function getIdActionFromSegment($valueToMatch, $sqlField, $matchType, $segmentName)
{
$actionType = self::guessActionTypeFromSegment($segmentName);
if ($actionType == Action::TYPE_PAGE_URL) {
// for urls trim protocol and www because it is not recorded in the db
$valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch);
}
$valueToMatch = Common::sanitizeInputValue(Common::unsanitizeInputValue($valueToMatch));
if ($matchType == SegmentExpression::MATCH_EQUAL
|| $matchType == SegmentExpression::MATCH_NOT_EQUAL
) {
$idAction = self::getIdActionMatchingNameAndType($valueToMatch, $actionType);
// if the action is not found, we hack -100 to ensure it tries to match against an integer
// otherwise binding idaction_name to "false" returns some rows for some reasons (in case &segment=pageTitle==Větrnásssssss)
if (empty($idAction)) {
$idAction = -100;
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) {
// for urls trim protocol and www because it is not recorded in the db
$valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch);
}
return $idAction;
$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);
}
// "name contains $string" match can match several idaction so we cannot return yet an idaction
// special case
$sql = TableLogAction::getSelectQueryWhereNameContains($matchType, $actionType);
return array(
// mark that the returned value is an sql-expression instead of a literal value
'SQL' => $sql,
'bind' => $valueToMatch,
);
$cache = StaticContainer::get('Piwik\Tracker\TableLogAction\Cache');
return $cache->getIdActionFromSegment($valueToMatch, $sql);
}
/**
@ -231,11 +215,18 @@ class TableLogAction
private static function guessActionTypeFromSegment($segmentName)
{
$exactMatch = array(
'eventAction' => Action::TYPE_EVENT_ACTION,
'eventCategory' => Action::TYPE_EVENT_CATEGORY,
'eventName' => Action::TYPE_EVENT_NAME,
'outlinkUrl' => Action::TYPE_OUTLINK,
'downloadUrl' => Action::TYPE_DOWNLOAD,
'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])) {
if (!empty($exactMatch[$segmentName])) {
return $exactMatch[$segmentName];
}
@ -253,5 +244,40 @@ class TableLogAction
}
}
}
/**
* 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(
$actionType == Action::TYPE_DOWNLOAD,
$actionType == Action::TYPE_OUTLINK,
$actionType == Action::TYPE_PAGE_URL,
$actionType == Action::TYPE_CONTENT,
);
return in_array($actionType, $actionsTypesStoredUnsanitized);
}
}

View 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;
}
}

View file

@ -0,0 +1,199 @@
<?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\CustomVariables\CustomVariables;
use Piwik\Plugins\SitesManager\API as APISitesManager;
/**
* Generates the Javascript code to be inserted on every page of the website to track.
*/
class TrackerCodeGenerator
{
/**
* @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
* @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
) {
// changes made to this code should be mirrored in plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js var generateJsCode
$jsCode = file_get_contents(PIWIK_INCLUDE_PATH . "/plugins/Morpheus/templates/javascriptCode.tpl");
$jsCode = htmlentities($jsCode);
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 ($mergeSubdomains || $mergeAliasUrls) {
$options .= $this->getJavascriptTagOptions($idSite, $mergeSubdomains, $mergeAliasUrls);
}
$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' => '//'
);
$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.
*
* 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)) + $codeImpl;
foreach ($codeImpl as $keyToReplace => $replaceWith) {
$jsCode = str_replace('{$' . $keyToReplace . '}', $replaceWith, $jsCode);
}
return $jsCode;
}
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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load diff

View 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;
}
}

View file

@ -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 = unserialize($list);
} else {
// Fallback to reading the bundled list
$file = PIWIK_VENDOR_PATH . '/piwik/referrer-spam-blacklist/spammers.txt';
$this->spammerList = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
}
return $this->spammerList;
}
}

View 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;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,15 +8,24 @@
*/
namespace Piwik\Tracker;
use Piwik\Cache as PiwikCache;
use Piwik\Common;
use Piwik\IP;
use Piwik\DeviceDetectorFactory;
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;
/**
* @param Request $request
* @param bool|string $ip
@ -24,16 +33,18 @@ class VisitExcluded
*/
public function __construct(Request $request, $ip = false, $userAgent = false)
{
if ($ip === false) {
$this->spamFilter = new ReferrerSpamFilter();
if (false === $ip) {
$ip = $request->getIp();
}
if ($userAgent === false) {
if (false === $userAgent) {
$userAgent = $request->getUserAgent();
}
$this->request = $request;
$this->idSite = $request->getIdSite();
$this->request = $request;
$this->idSite = $request->getIdSite();
$this->userAgent = $userAgent;
$this->ip = $ip;
}
@ -72,9 +83,9 @@ class VisitExcluded
/**
* 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.
@ -110,6 +121,22 @@ class VisitExcluded
}
}
// 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;
@ -138,37 +165,53 @@ class VisitExcluded
* 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');
return !$allowBots
// Seen in the wild
&& (strpos($this->userAgent, 'Googlebot') !== false // Googlebot
|| strpos($this->userAgent, 'Google Web Preview') !== false // Google Instant
|| strpos($this->userAgent, 'AdsBot-Google') !== false // Google Adwords landing pages
|| strpos($this->userAgent, 'Google Page Speed Insights') !== false // #4049
|| strpos($this->userAgent, 'Google (+https://developers.google.com') !== false // Google Snippet https://developers.google.com/+/web/snippet/
|| strpos($this->userAgent, 'facebookexternalhit') !== false // http://www.facebook.com/externalhit_uatext.php
|| strpos($this->userAgent, 'baidu') !== false // Baidu
|| strpos($this->userAgent, 'bingbot') !== false // Bingbot
|| strpos($this->userAgent, 'YottaaMonitor') !== false // Yottaa
|| strpos($this->userAgent, 'CloudFlare') !== false // CloudFlare-AlwaysOnline
$deviceDetector = DeviceDetectorFactory::getInstance($this->userAgent);
// Added as they are popular bots
|| strpos($this->userAgent, 'pingdom') !== false // pingdom
|| strpos($this->userAgent, 'yandex') !== false // yandex
|| strpos($this->userAgent, 'exabot') !== false // Exabot
|| strpos($this->userAgent, 'sogou') !== false // Sogou
|| strpos($this->userAgent, 'soso') !== false // Soso
|| IP::isIpInRange($this->ip, $this->getBotIpRanges()));
return !$allowBots
&& ($deviceDetector->isBot() || $this->isIpInRange());
}
protected function getBotIpRanges()
private function isIpInRange()
{
return array(
$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',
@ -180,12 +223,29 @@ class VisitExcluded
'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'
);
}
@ -199,6 +259,7 @@ class VisitExcluded
Common::printDebug('Piwik ignore cookie was found, visit not tracked.');
return true;
}
return false;
}
@ -210,12 +271,39 @@ class VisitExcluded
protected function isVisitorIpExcluded()
{
$websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite);
if (!empty($websiteAttributes['excluded_ips'])) {
if (IP::isIpInRange($this->ip, $websiteAttributes['excluded_ips'])) {
Common::printDebug('Visitor IP ' . IP::N2P($this->ip) . ' is excluded from being tracked');
$ip = IP::fromBinaryIP($this->ip);
if ($ip->isInRanges($websiteAttributes['excluded_ips'])) {
Common::printDebug('Visitor IP ' . $ip->toString() . ' is excluded from being tracked');
return true;
}
}
return false;
}
/**
* Checks if request URL is excluded
* @return bool
*/
protected function isUrlExcluded()
{
$site = Cache::getCacheWebsiteAttributes($this->idSite);
if (!empty($site['exclude_unknown_urls']) && !empty($site['urls'])) {
$url = $this->request->getParam('url');
$parsedUrl = parse_url($url);
$trackingUrl = new SiteUrls();
$urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $site['urls']));
$idSites = $trackingUrl->getIdSitesMatchingUrl($parsedUrl, $urls);
$isUrlExcluded = !isset($idSites) || !in_array($this->idSite, $idSites);
return $isUrlExcluded;
}
return false;
}
@ -231,6 +319,7 @@ class VisitExcluded
protected function isUserAgentExcluded()
{
$websiteAttributes = Cache::getCacheWebsiteAttributes($this->idSite);
if (!empty($websiteAttributes['excluded_user_agents'])) {
foreach ($websiteAttributes['excluded_user_agents'] as $excludedUserAgent) {
// if the excluded user agent string part is in this visit's user agent, this visit should be excluded
@ -239,6 +328,17 @@ class VisitExcluded
}
}
}
return false;
}
/**
* Returns true if the Referrer is a known spammer.
*
* @return bool
*/
protected function isReferrerSpamExcluded()
{
return $this->spamFilter->isSpam($this->request);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,13 +11,13 @@ 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
*/
@ -25,7 +25,7 @@ interface VisitInterface
/**
* Tracks a visit.
*
*
* @return void
*/
public function handle();

View 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;
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -14,4 +14,3 @@ namespace Piwik\Tracker;
class VisitorNotFoundInDb extends \Exception
{
}

View file

@ -0,0 +1,269 @@
<?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;
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 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->getVisitFieldsPersist();
$shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, $request);
list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit($request);
$visitRow = $this->model->findVisitor($idSite, $configId, $idVisitor, $persistedVisitAttributes, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead);
$isNewVisitForced = $request->getParam('new_visit');
$isNewVisitForced = !empty($isNewVisitForced);
$enforceNewVisit = $isNewVisitForced || $this->trackerAlwaysNewVisitor;
if (!$enforceNewVisit
&& $visitRow
&& count($visitRow) > 0
) {
// These values will be used throughout the request
foreach ($persistedVisitAttributes as $field) {
$visitProperties->setProperty($field, $visitRow[$field]);
}
$visitProperties->setProperty('visit_last_action_time', strtotime($visitRow['visit_last_action_time']));
$visitProperties->setProperty('visit_first_action_time', strtotime($visitRow['visit_first_action_time']));
// Custom Variables copied from Visit in potential later conversion
if (!empty($numCustomVarsToRead)) {
for ($i = 1; $i <= $numCustomVarsToRead; $i++) {
if (isset($visitRow['custom_var_k' . $i])
&& strlen($visitRow['custom_var_k' . $i])
) {
$visitProperties->setProperty('custom_var_k' . $i, $visitRow['custom_var_k' . $i]);
}
if (isset($visitRow['custom_var_v' . $i])
&& strlen($visitRow['custom_var_v' . $i])
) {
$visitProperties->setProperty('custom_var_v' . $i, $visitRow['custom_var_v' . $i]);
}
}
}
Common::printDebug("The visitor is known (idvisitor = " . bin2hex($visitProperties->getProperty('idvisitor')) . ",
config_id = " . bin2hex($configId) . ",
idvisit = {$visitProperties->getProperty('idvisit')},
last action = " . date("r", $visitProperties->getProperty('visit_last_action_time')) . ",
first action = " . date("r", $visitProperties->getProperty('visit_first_action_time')) . ",
visit_goal_buyer' = " . $visitProperties->getProperty('visit_goal_buyer') . ")");
return true;
} else {
Common::printDebug("The visitor was not matched with an existing visitor...");
return false;
}
}
protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup, Request $request)
{
$isForcedUserIdMustMatch = (false !== $request->getForcedUserId());
if ($isForcedUserIdMustMatch) {
// if &iud was set, we must try and match both idvisitor and config_id
return false;
}
// 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 && $this->trustCookiesOnly) {
return true;
}
// 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 getVisitFieldsPersist()
{
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')) {
$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;
}
}