2665 lines
79 KiB
PHP
2665 lines
79 KiB
PHP
<?php
|
||
/**
|
||
* Matomo - free/libre analytics platform
|
||
*
|
||
* For more information, see README.md
|
||
*
|
||
* @license released under BSD License http://www.opensource.org/licenses/bsd-license.php
|
||
* @link https://matomo.org/docs/tracking-api/
|
||
*
|
||
* @category Matomo
|
||
* @package MatomoTracker
|
||
*/
|
||
|
||
namespace WP_Piwik;
|
||
|
||
/**
|
||
* MatomoTracker implements the Matomo Tracking Web API.
|
||
*
|
||
* For more information, see: https://github.com/matomo-org/matomo-php-tracker/
|
||
*
|
||
* @package MatomoTracker
|
||
* @api
|
||
*/
|
||
#[AllowDynamicProperties]
|
||
class MatomoTracker
|
||
{
|
||
/**
|
||
* Matomo base URL, for example http://example.org/matomo/
|
||
* Must be set before using the class by calling
|
||
* MatomoTracker::$URL = 'http://yourwebsite.org/matomo/';
|
||
*
|
||
* @var string
|
||
*/
|
||
static public $URL = '';
|
||
|
||
public const AI_BOT_USER_AGENT_SUBSTRINGS = [
|
||
'GPTBot',
|
||
'ChatGPT-User',
|
||
'MistralAI-User',
|
||
'Gemini-Deep-Research',
|
||
'Claude-User',
|
||
'Perplexity-User',
|
||
'GoogleAgent',
|
||
'Devin',
|
||
'NovaAct',
|
||
'Google-Extended',
|
||
'Google-CloudVertexBot',
|
||
];
|
||
|
||
/**
|
||
* API Version
|
||
*
|
||
* @ignore
|
||
* @var int
|
||
*/
|
||
public const VERSION = 1;
|
||
|
||
/**
|
||
* @ignore
|
||
*/
|
||
public $DEBUG_APPEND_URL = '';
|
||
|
||
/**
|
||
* Visitor ID length
|
||
*
|
||
* @ignore
|
||
*/
|
||
public const LENGTH_VISITOR_ID = 16;
|
||
|
||
/**
|
||
* Charset
|
||
* @see setPageCharset
|
||
* @ignore
|
||
*/
|
||
public const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8';
|
||
|
||
/**
|
||
* See matomo.js
|
||
*/
|
||
public const FIRST_PARTY_COOKIES_PREFIX = '_pk_';
|
||
|
||
/**
|
||
* Defines how many categories can be used max when calling addEcommerceItem().
|
||
* @var int
|
||
*/
|
||
public const MAX_NUM_ECOMMERCE_ITEM_CATEGORIES = 5;
|
||
|
||
public const DEFAULT_COOKIE_PATH = '/';
|
||
|
||
public $ecommerceItems = [];
|
||
|
||
public $attributionInfo = false;
|
||
|
||
public $eventCustomVar = [];
|
||
|
||
public $forcedDatetime = false;
|
||
|
||
public $forcedNewVisit = false;
|
||
|
||
public $networkTime = false;
|
||
|
||
public $serverTime = false;
|
||
|
||
public $transferTime = false;
|
||
|
||
public $domProcessingTime = false;
|
||
|
||
public $domCompletionTime = false;
|
||
|
||
public $onLoadTime = false;
|
||
|
||
public $pageCustomVar = [];
|
||
|
||
public $ecommerceView = [];
|
||
|
||
public $customParameters = [];
|
||
|
||
public $customDimensions = [];
|
||
|
||
public $customData = false;
|
||
|
||
public $hasCookies = false;
|
||
|
||
public $token_auth = false;
|
||
|
||
public $userAgent = false;
|
||
|
||
public $country = false;
|
||
|
||
public $region = false;
|
||
|
||
public $city = false;
|
||
|
||
public $lat = false;
|
||
|
||
public $long = false;
|
||
|
||
public $width = false;
|
||
|
||
public $height = false;
|
||
|
||
public $plugins = false;
|
||
|
||
public $localHour = false;
|
||
|
||
public $localMinute = false;
|
||
|
||
public $localSecond = false;
|
||
|
||
public $idPageview = false;
|
||
|
||
public $idPageviewSetManually = false;
|
||
|
||
public $idSite;
|
||
|
||
public $urlReferrer;
|
||
|
||
public $pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES;
|
||
|
||
public $pageUrl;
|
||
|
||
public $ip;
|
||
|
||
public $acceptLanguage;
|
||
|
||
public $clientHints = [];
|
||
|
||
// Life of the visitor cookie (in sec)
|
||
public $configVisitorCookieTimeout = 33955200; // 13 months (365 + 28 days)
|
||
|
||
// Life of the session cookie (in sec)
|
||
public $configSessionCookieTimeout = 1800; // 30 minutes
|
||
|
||
// Life of the session cookie (in sec)
|
||
public $configReferralCookieTimeout = 15768000; // 6 months
|
||
|
||
// Visitor Ids in order
|
||
public $userId = false;
|
||
|
||
public $forcedVisitorId = false;
|
||
|
||
public $cookieVisitorId = false;
|
||
|
||
public $randomVisitorId = false;
|
||
|
||
public $configCookiesDisabled = false;
|
||
|
||
public $configCookiePath = self::DEFAULT_COOKIE_PATH;
|
||
|
||
public $configCookieDomain = '';
|
||
|
||
public $configCookieSameSite = '';
|
||
|
||
public $configCookieSecure = false;
|
||
|
||
public $configCookieHTTPOnly = false;
|
||
|
||
public $currentTs;
|
||
|
||
public $createTs;
|
||
|
||
// Allow debug while blocking the request
|
||
public $requestTimeout = 600;
|
||
|
||
public $requestConnectTimeout = 300;
|
||
|
||
public $doBulkRequests = false;
|
||
|
||
public $storedTrackingActions = [];
|
||
|
||
public $sendImageResponse = true;
|
||
|
||
public $outgoingTrackerCookies = [];
|
||
|
||
public $incomingTrackerCookies = [];
|
||
|
||
public $visitorCustomVar;
|
||
|
||
private $requestMethod = null;
|
||
|
||
/**
|
||
* Builds a MatomoTracker object, used to track visits, pages and Goal conversions
|
||
* for a specific website, by using the Matomo Tracking API.
|
||
*
|
||
* @param int $idSite Id site to be tracked
|
||
* @param string $apiUrl "http://example.org/matomo/" or "http://matomo.example.org/"
|
||
* If set, will overwrite MatomoTracker::$URL
|
||
*/
|
||
public function __construct(int $idSite, string $apiUrl = '')
|
||
{
|
||
$this->idSite = $idSite;
|
||
$this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false;
|
||
$this->pageUrl = self::getCurrentUrl();
|
||
$this->ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false;
|
||
$this->acceptLanguage = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : false;
|
||
$this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : false;
|
||
$this->setClientHints(
|
||
!empty($_SERVER['HTTP_SEC_CH_UA_MODEL']) ? $_SERVER['HTTP_SEC_CH_UA_MODEL'] : '',
|
||
!empty($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) ? $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] : '',
|
||
!empty($_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION']) ? $_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] : '',
|
||
!empty($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST']) ? $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST'] : '',
|
||
!empty($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION']) ? $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION'] : ''
|
||
);
|
||
if (!empty($apiUrl)) {
|
||
self::$URL = $apiUrl;
|
||
}
|
||
|
||
$this->setNewVisitorId();
|
||
|
||
$this->currentTs = time();
|
||
$this->createTs = $this->currentTs;
|
||
|
||
$this->visitorCustomVar = $this->getCustomVariablesFromCookie();
|
||
}
|
||
|
||
public function setApiUrl(string $url): void
|
||
{
|
||
self::$URL = $url;
|
||
}
|
||
|
||
/**
|
||
* By default, Matomo expects utf-8 encoded values, for example
|
||
* for the page URL parameter values, Page Title, etc.
|
||
* It is recommended to only send UTF-8 data to Matomo.
|
||
* If required though, you can also specify another charset using this function.
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setPageCharset(string $charset = '')
|
||
{
|
||
$this->pageCharset = $charset;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the current URL being tracked
|
||
*
|
||
* @param string $url Raw URL (not URL encoded)
|
||
* @return $this
|
||
*/
|
||
public function setUrl(string $url)
|
||
{
|
||
$this->pageUrl = $url;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the URL referrer used to track Referrers details for new visits.
|
||
*
|
||
* @param string $url Raw URL (not URL encoded)
|
||
* @return $this
|
||
*/
|
||
public function setUrlReferrer(string $url)
|
||
{
|
||
$this->urlReferrer = $url;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* This method is deprecated and does nothing. It used to set the time that it took to generate the document on the server side.
|
||
*
|
||
* @param int $timeMs Generation time in ms
|
||
* @return $this
|
||
*
|
||
* @deprecated this metric is deprecated please use performance timings instead
|
||
* @see setPerformanceTimings
|
||
*/
|
||
public function setGenerationTime(int $timeMs)
|
||
{
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets timings for various browser performance metrics.
|
||
* @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
|
||
*
|
||
* @param null|int $network Network time in ms (connectEnd – fetchStart)
|
||
* @param null|int $server Server time in ms (responseStart – requestStart)
|
||
* @param null|int $transfer Transfer time in ms (responseEnd – responseStart)
|
||
* @param null|int $domProcessing DOM Processing to Interactive time in ms (domInteractive – domLoading)
|
||
* @param null|int $domCompletion DOM Interactive to Complete time in ms (domComplete – domInteractive)
|
||
* @param null|int $onload Onload time in ms (loadEventEnd – loadEventStart)
|
||
* @return $this
|
||
*/
|
||
public function setPerformanceTimings(
|
||
?int $network = null,
|
||
?int $server = null,
|
||
?int $transfer = null,
|
||
?int $domProcessing = null,
|
||
?int $domCompletion = null,
|
||
?int $onload = null
|
||
) {
|
||
$this->networkTime = $network;
|
||
$this->serverTime = $server;
|
||
$this->transferTime = $transfer;
|
||
$this->domProcessingTime = $domProcessing;
|
||
$this->domCompletionTime = $domCompletion;
|
||
$this->onLoadTime = $onload;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Clear / reset all previously set performance metrics.
|
||
*/
|
||
public function clearPerformanceTimings(): void
|
||
{
|
||
$this->networkTime = false;
|
||
$this->serverTime = false;
|
||
$this->transferTime = false;
|
||
$this->domProcessingTime = false;
|
||
$this->domCompletionTime = false;
|
||
$this->onLoadTime = false;
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
* @ignore
|
||
*/
|
||
public function setUrlReferer(string $url)
|
||
{
|
||
$this->setUrlReferrer($url);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the attribution information to the visit, so that subsequent Goal conversions are
|
||
* properly attributed to the right Referrer URL, timestamp, Campaign Name & Keyword.
|
||
*
|
||
* This must be a JSON encoded string that would typically be fetched from the JS API:
|
||
* matomoTracker.getAttributionInfo() and that you have JSON encoded via JSON2.stringify()
|
||
*
|
||
* If you call enableCookies() then these referral attribution values will be set
|
||
* to the 'ref' first party cookie storing referral information.
|
||
*
|
||
* @param string $jsonEncoded JSON encoded array containing Attribution info
|
||
* @return $this
|
||
* @throws Exception
|
||
* @see function getAttributionInfo() in https://github.com/matomo-org/matomo/blob/master/js/matomo.js
|
||
*/
|
||
public function setAttributionInfo(string $jsonEncoded)
|
||
{
|
||
$decoded = json_decode($jsonEncoded, $assoc = true);
|
||
if (!is_array($decoded)) {
|
||
throw new Exception("setAttributionInfo() is expecting a JSON encoded string, $jsonEncoded given");
|
||
}
|
||
$this->attributionInfo = $decoded;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets Visit Custom Variable.
|
||
* See https://matomo.org/docs/custom-variables/
|
||
*
|
||
* @param int $id Custom variable slot ID from 1-5
|
||
* @param string $name Custom variable name
|
||
* @param string $value Custom variable value
|
||
* @param string $scope Custom variable scope. Possible values: visit, page, event
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setCustomVariable(
|
||
int $id,
|
||
string $name,
|
||
string $value,
|
||
string $scope = 'visit'
|
||
) {
|
||
if ($scope === 'page') {
|
||
$this->pageCustomVar[$id] = array($name, $value);
|
||
} elseif ($scope === 'event') {
|
||
$this->eventCustomVar[$id] = array($name, $value);
|
||
} elseif ($scope === 'visit') {
|
||
$this->visitorCustomVar[$id] = array($name, $value);
|
||
} else {
|
||
throw new Exception("Invalid 'scope' parameter value");
|
||
}
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Returns the currently assigned Custom Variable.
|
||
*
|
||
* If scope is 'visit', it will attempt to read the value set in the first party cookie created by Matomo Tracker
|
||
* ($_COOKIE array).
|
||
*
|
||
* @param int $id Custom Variable integer index to fetch from cookie. Should be a value from 1 to 5
|
||
* @param string $scope Custom variable scope. Possible values: visit, page, event
|
||
*
|
||
* @throws Exception
|
||
* @return mixed An array with this format: array( 0 => CustomVariableName, 1 => CustomVariableValue ) or false
|
||
* @see matomo.js getCustomVariable()
|
||
*/
|
||
public function getCustomVariable(int $id, string $scope = 'visit')
|
||
{
|
||
if ($scope === 'page') {
|
||
return $this->pageCustomVar[$id] ?? false;
|
||
}
|
||
|
||
if ($scope === 'event') {
|
||
return $this->eventCustomVar[$id] ?? false;
|
||
}
|
||
|
||
if ($scope !== 'visit') {
|
||
throw new Exception("Invalid 'scope' parameter value");
|
||
}
|
||
|
||
if (!empty($this->visitorCustomVar[$id])) {
|
||
return $this->visitorCustomVar[$id];
|
||
}
|
||
$cookieDecoded = $this->getCustomVariablesFromCookie();
|
||
|
||
if (!is_array($cookieDecoded)
|
||
|| !isset($cookieDecoded[$id])
|
||
|| !is_array($cookieDecoded[$id])
|
||
|| count($cookieDecoded[$id]) !== 2
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
return $cookieDecoded[$id];
|
||
}
|
||
|
||
/**
|
||
* Clears any Custom Variable that may be have been set.
|
||
*
|
||
* This can be useful when you have enabled bulk requests,
|
||
* and you wish to clear Custom Variables of 'visit' scope.
|
||
*/
|
||
public function clearCustomVariables(): void
|
||
{
|
||
$this->visitorCustomVar = [];
|
||
$this->pageCustomVar = [];
|
||
$this->eventCustomVar = [];
|
||
}
|
||
|
||
/**
|
||
* Sets a specific custom dimension
|
||
*
|
||
* @param int $id id of custom dimension
|
||
* @param string $value value for custom dimension
|
||
* @return $this
|
||
*/
|
||
public function setCustomDimension(int $id, string $value)
|
||
{
|
||
$this->customDimensions['dimension'.$id] = $value;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Clears all previously set custom dimensions
|
||
*/
|
||
public function clearCustomDimensions(): void
|
||
{
|
||
$this->customDimensions = [];
|
||
}
|
||
|
||
/**
|
||
* Returns the value of the custom dimension with the given id
|
||
*
|
||
* @param int $id id of custom dimension
|
||
* @return string|null
|
||
*/
|
||
public function getCustomDimension(int $id): ?string
|
||
{
|
||
return $this->customDimensions['dimension'.$id] ?? null;
|
||
}
|
||
|
||
/**
|
||
* Sets a custom tracking parameter. This is useful if you need to send any tracking parameters for a 3rd party
|
||
* plugin that is not shipped with Matomo itself. Please note that custom parameters are cleared after each
|
||
* tracking request.
|
||
*
|
||
* @param string $trackingApiParameter The name of the tracking API parameter, eg 'bw_bytes'
|
||
* @param string $value Tracking parameter value that shall be sent for this tracking parameter.
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setCustomTrackingParameter(string $trackingApiParameter, string $value)
|
||
{
|
||
$matches = [];
|
||
|
||
if (preg_match('/^dimension([0-9]+)$/', $trackingApiParameter, $matches)) {
|
||
$this->setCustomDimension($matches[1], $value);
|
||
|
||
return $this;
|
||
}
|
||
|
||
$this->customParameters[$trackingApiParameter] = $value;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Clear / reset all previously set custom tracking parameters.
|
||
*/
|
||
public function clearCustomTrackingParameters(): void
|
||
{
|
||
$this->customParameters = [];
|
||
}
|
||
|
||
/**
|
||
* Sets the current visitor ID to a random new one.
|
||
* @return $this
|
||
*/
|
||
public function setNewVisitorId()
|
||
{
|
||
$this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID);
|
||
$this->forcedVisitorId = false;
|
||
$this->cookieVisitorId = false;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the current site ID.
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setIdSite(int $idSite)
|
||
{
|
||
$this->idSite = $idSite;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the Browser language. Used to guess visitor countries when GeoIP is not enabled
|
||
*
|
||
* @param string $acceptLanguage For example "fr-fr"
|
||
* @return $this
|
||
*/
|
||
public function setBrowserLanguage(string $acceptLanguage)
|
||
{
|
||
$this->acceptLanguage = $acceptLanguage;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the user agent, used to detect OS and browser.
|
||
* If this function is not called, the User Agent will default to the current user agent.
|
||
*
|
||
* @param string $userAgent
|
||
* @return $this
|
||
*/
|
||
public function setUserAgent(string $userAgent)
|
||
{
|
||
$this->userAgent = $userAgent;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the client hints, used to detect OS and browser.
|
||
* If this function is not called, the client hints sent with the current request will be used.
|
||
*
|
||
* Supported as of Matomo 4.12.0
|
||
*
|
||
* @param string $model Value of the header 'HTTP_SEC_CH_UA_MODEL'
|
||
* @param string $platform Value of the header 'HTTP_SEC_CH_UA_PLATFORM'
|
||
* @param string $platformVersion Value of the header 'HTTP_SEC_CH_UA_PLATFORM_VERSION'
|
||
* @param string|array<string, mixed> $fullVersionList Value of header 'HTTP_SEC_CH_UA_FULL_VERSION_LIST'
|
||
* or an array containing all brands with the structure
|
||
* [['brand' => 'Chrome', 'version' => '10.0.2'], ['brand' => '...]
|
||
* @param string $uaFullVersion Value of the header 'HTTP_SEC_CH_UA_FULL_VERSION'
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setClientHints(
|
||
string $model = '',
|
||
string $platform = '',
|
||
string $platformVersion = '',
|
||
$fullVersionList = '',
|
||
string $uaFullVersion = ''
|
||
) {
|
||
if (is_string($fullVersionList)) {
|
||
$reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/';
|
||
$list = [];
|
||
|
||
while (\preg_match($reg, $fullVersionList, $matches)) {
|
||
$list[] = ['brand' => $matches[1], 'version' => $matches[2]];
|
||
$fullVersionList = \substr($fullVersionList, \strlen($matches[0]));
|
||
}
|
||
|
||
$fullVersionList = $list;
|
||
} elseif (!is_array($fullVersionList)) {
|
||
$fullVersionList = [];
|
||
}
|
||
|
||
$this->clientHints = array_filter([
|
||
'model' => $model,
|
||
'platform' => $platform,
|
||
'platformVersion' => $platformVersion,
|
||
'uaFullVersion' => $uaFullVersion,
|
||
'fullVersionList' => $fullVersionList,
|
||
]);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the country of the visitor. If not used, Matomo will try to find the country
|
||
* using either the visitor's IP address or language.
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth().
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setCountry(string $country)
|
||
{
|
||
$this->country = $country;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the region of the visitor. If not used, Matomo may try to find the region
|
||
* using the visitor's IP address (if configured to do so).
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth().
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setRegion(string $region)
|
||
{
|
||
$this->region = $region;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the city of the visitor. If not used, Matomo may try to find the city
|
||
* using the visitor's IP address (if configured to do so).
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth().
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setCity(string $city)
|
||
{
|
||
$this->city = $city;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the latitude of the visitor. If not used, Matomo may try to find the visitor's
|
||
* latitude using the visitor's IP address (if configured to do so).
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth().
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setLatitude(float $lat)
|
||
{
|
||
$this->lat = $lat;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the longitude of the visitor. If not used, Matomo may try to find the visitor's
|
||
* longitude using the visitor's IP address (if configured to do so).
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth().
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setLongitude(float $long)
|
||
{
|
||
$this->long = $long;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Enables the bulk request feature. When used, each tracking action is stored until the
|
||
* doBulkTrack method is called. This method will send all tracking data at once.
|
||
*/
|
||
public function enableBulkTracking(): void
|
||
{
|
||
$this->doBulkRequests = true;
|
||
}
|
||
|
||
/**
|
||
* Disables the bulk request feature. Make sure to call `doBulkTrack()` before disabling it if you have stored
|
||
* tracking actions previously as this method won't be sending any previously stored actions before disabling it.
|
||
*/
|
||
public function disableBulkTracking(): void
|
||
{
|
||
$this->doBulkRequests = false;
|
||
}
|
||
|
||
/**
|
||
* Enable Cookie Creation - this will cause a first party VisitorId cookie to be set when the VisitorId is set or reset
|
||
*
|
||
* @param string $domain (optional) Set first-party cookie domain.
|
||
* Accepted values: example.com, *.example.com (same as .example.com) or subdomain.example.com
|
||
* @param string $path (optional) Set first-party cookie path
|
||
* @param bool $secure (optional) Set secure flag for cookies
|
||
* @param bool $httpOnly (optional) Set HTTPOnly flag for cookies
|
||
* @param string $sameSite (optional) Set SameSite flag for cookies
|
||
*/
|
||
public function enableCookies(
|
||
string $domain = '',
|
||
string $path = '/',
|
||
bool $secure = false,
|
||
bool $httpOnly = false,
|
||
string $sameSite = ''
|
||
): void {
|
||
$this->configCookiesDisabled = false;
|
||
$this->configCookieDomain = self::domainFixup($domain);
|
||
$this->configCookiePath = $path;
|
||
$this->configCookieSecure = $secure;
|
||
$this->configCookieHTTPOnly = $httpOnly;
|
||
$this->configCookieSameSite = $sameSite;
|
||
}
|
||
|
||
/**
|
||
* If image response is disabled Matomo will respond with a HTTP 204 header instead of responding with a gif.
|
||
*/
|
||
public function disableSendImageResponse(): void
|
||
{
|
||
$this->sendImageResponse = false;
|
||
}
|
||
|
||
/**
|
||
* Fix-up domain
|
||
*/
|
||
protected static function domainFixup($domain)
|
||
{
|
||
if (strlen($domain) > 0) {
|
||
$dl = strlen($domain) - 1;
|
||
// remove trailing '.'
|
||
if ($domain[$dl] === '.') {
|
||
$domain = substr($domain, 0, $dl);
|
||
}
|
||
// remove leading '*'
|
||
if (substr($domain, 0, 2) === '*.') {
|
||
$domain = substr($domain, 1);
|
||
}
|
||
}
|
||
|
||
return $domain;
|
||
}
|
||
|
||
/**
|
||
* Get cookie name with prefix and domain hash
|
||
*/
|
||
protected function getCookieName(string $cookieName): string
|
||
{
|
||
// NOTE: If the cookie name is changed, we must also update the method in matomo.js with the same name.
|
||
$hash = substr(
|
||
sha1(
|
||
($this->configCookieDomain === ''
|
||
? self::getCurrentHost()
|
||
: $this->configCookieDomain
|
||
) . $this->configCookiePath
|
||
),
|
||
0,
|
||
4
|
||
);
|
||
|
||
return self::FIRST_PARTY_COOKIES_PREFIX . $cookieName . '.' . $this->idSite . '.' . $hash;
|
||
}
|
||
|
||
/**
|
||
* Tracks a page view
|
||
*
|
||
* @param string $documentTitle Page title as it will appear in the Actions > Page titles report
|
||
* @return mixed Response string or true if using bulk requests.
|
||
*/
|
||
public function doTrackPageView(string $documentTitle)
|
||
{
|
||
if (!$this->idPageviewSetManually) {
|
||
$this->generateNewPageviewId();
|
||
}
|
||
|
||
$url = $this->getUrlTrackPageView($documentTitle);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* If the current user agent belongs to an AI agent bot, tracks a pageview action.
|
||
*
|
||
* This method should be used server side to track AI bots that do not execute
|
||
* JavaScript.
|
||
*
|
||
* @return mixed Response string or true if using bulk requests.
|
||
*/
|
||
public function doTrackPageViewIfAIBot(?int $httpStatus = null, ?int $responseSizeBytes = null, ?int $serverTimeMs = null, ?string $source = null)
|
||
{
|
||
if (!self::isUserAgentAIBot($this->userAgent)) {
|
||
return null;
|
||
}
|
||
|
||
$url = $this->getUrlTrackAIBot($httpStatus, $responseSizeBytes, $serverTimeMs, $source);
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Override PageView id for every use of `doTrackPageView()`. Do not use this if you call `doTrackPageView()`
|
||
* multiple times during tracking (if, for example, you are tracking a single page application).
|
||
*/
|
||
public function setPageviewId(string $idPageview): void
|
||
{
|
||
$this->idPageview = $idPageview;
|
||
$this->idPageviewSetManually = true;
|
||
}
|
||
|
||
/**
|
||
* Returns the PageView id. If the id was manually set using `setPageViewId()`, that id will be returned.
|
||
* If the id was not set manually, the id that was automatically generated in last `doTrackPageView()` will
|
||
* be returned. If there was no last page view, this will be false.
|
||
*
|
||
* @return string|false The PageView id as string or false if there is none yet.
|
||
*/
|
||
public function getPageviewId()
|
||
{
|
||
return $this->idPageview;
|
||
}
|
||
|
||
private function generateNewPageviewId(): void
|
||
{
|
||
$this->idPageview = substr(md5(uniqid(rand(), true)), 0, 6);
|
||
}
|
||
|
||
/**
|
||
* Tracks an event
|
||
*
|
||
* @param string $category The Event Category (Videos, Music, Games...)
|
||
* @param string $action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
|
||
* @param string|bool $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...)
|
||
* @param float|bool $value (optional) The Event's value
|
||
* @return mixed Response string or true if using bulk requests.
|
||
*/
|
||
public function doTrackEvent(
|
||
string $category,
|
||
string $action,
|
||
$name = false,
|
||
$value = false
|
||
) {
|
||
$url = $this->getUrlTrackEvent($category, $action, $name, $value);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Tracks a content impression
|
||
*
|
||
* @param string $contentName The name of the content. For instance 'Ad Foo Bar'
|
||
* @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
|
||
* @param string|bool $contentTarget (optional) The target of the content. For instance the URL of a landing page.
|
||
* @return mixed Response string or true if using bulk requests.
|
||
*/
|
||
public function doTrackContentImpression(
|
||
string $contentName,
|
||
string $contentPiece = 'Unknown',
|
||
$contentTarget = false
|
||
) {
|
||
$url = $this->getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Tracks a content interaction. Make sure you have tracked a content impression using the same content name and
|
||
* content piece, otherwise it will not count. To do so you should call the method doTrackContentImpression();
|
||
*
|
||
* @param string $interaction The name of the interaction with the content. For instance a 'click'
|
||
* @param string $contentName The name of the content. For instance 'Ad Foo Bar'
|
||
* @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
|
||
* @param string|bool $contentTarget (optional) The target the content leading to when an interaction occurs. For instance the URL of a landing page.
|
||
* @return mixed Response string or true if using bulk requests.
|
||
*/
|
||
public function doTrackContentInteraction(
|
||
string $interaction,
|
||
string $contentName,
|
||
string $contentPiece = 'Unknown',
|
||
$contentTarget = false
|
||
) {
|
||
$url = $this->getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Tracks an internal Site Search query, and optionally tracks the Search Category, and Search results Count.
|
||
* These are used to populate reports in Actions > Site Search.
|
||
*
|
||
* @param string $keyword Searched query on the site
|
||
* @param string $category (optional) Search engine category if applicable
|
||
* @param bool|int $countResults (optional) results displayed on the search result page. Used to track "zero result" keywords.
|
||
*
|
||
* @return mixed Response or true if using bulk requests.
|
||
*/
|
||
public function doTrackSiteSearch(
|
||
string $keyword,
|
||
string $category = '',
|
||
$countResults = false
|
||
) {
|
||
$url = $this->getUrlTrackSiteSearch($keyword, $category, $countResults);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Records a Goal conversion
|
||
*
|
||
* @param int $idGoal Id Goal to record a conversion
|
||
* @param float $revenue Revenue for this conversion
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackGoal(int $idGoal, float $revenue = 0.0)
|
||
{
|
||
$url = $this->getUrlTrackGoal($idGoal, $revenue);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Tracks a download or outlink
|
||
*
|
||
* @param string $actionUrl URL of the download or outlink
|
||
* @param string $actionType Type of the action: 'download' or 'link'
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackAction(string $actionUrl, string $actionType)
|
||
{
|
||
// Referrer could be udpated to be the current URL temporarily (to mimic JS behavior)
|
||
$url = $this->getUrlTrackAction($actionUrl, $actionType);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Adds an item in the Ecommerce order.
|
||
*
|
||
* This should be called before doTrackEcommerceOrder(), or before doTrackEcommerceCartUpdate().
|
||
* This function can be called for all individual products in the cart (or order).
|
||
* SKU parameter is mandatory. Other parameters are optional (set to false if value not known).
|
||
* Ecommerce items added via this function are automatically cleared when doTrackEcommerceOrder() or getUrlTrackEcommerceOrder() is called.
|
||
*
|
||
* @param string $sku (required) SKU, Product identifier
|
||
* @param string $name (optional) Product name
|
||
* @param string|array $category (optional) Product category, or array of product categories (up to 5 categories can be specified for a given product)
|
||
* @param float|int $price (optional) Individual product price (supports integer and decimal prices)
|
||
* @param int $quantity (optional) Product quantity. If not specified, will default to 1 in the Reports
|
||
* @throws Exception
|
||
* @return $this
|
||
*/
|
||
public function addEcommerceItem(
|
||
string $sku,
|
||
string $name = '',
|
||
$category = '',
|
||
$price = 0.0,
|
||
int $quantity = 1
|
||
) {
|
||
if (empty($sku)) {
|
||
throw new Exception("You must specify a SKU for the Ecommerce item");
|
||
}
|
||
|
||
$price = $this->forceDotAsSeparatorForDecimalPoint($price);
|
||
|
||
$this->ecommerceItems[] = array($sku, $name, $category, $price, $quantity);
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Tracks a Cart Update (add item, remove item, update item).
|
||
*
|
||
* On every Cart update, you must call addEcommerceItem() for each item (product) in the cart,
|
||
* including the items that haven't been updated since the last cart update.
|
||
* Items which were in the previous cart and are not sent in later Cart updates will be deleted from the cart (in the database).
|
||
*
|
||
* @param float $grandTotal Cart grandTotal (typically the sum of all items' prices)
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackEcommerceCartUpdate(float $grandTotal)
|
||
{
|
||
$url = $this->getUrlTrackEcommerceCartUpdate($grandTotal);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Sends all stored tracking actions at once. Only has an effect if bulk tracking is enabled.
|
||
*
|
||
* To enable bulk tracking, call enableBulkTracking().
|
||
*
|
||
* @throws Exception
|
||
* @return string Response
|
||
*/
|
||
public function doBulkTrack()
|
||
{
|
||
if (empty($this->storedTrackingActions)) {
|
||
throw new Exception(
|
||
"Error: you must call the function doTrackPageView or doTrackGoal from this class,
|
||
before calling this method doBulkTrack()"
|
||
);
|
||
}
|
||
|
||
$data = ['requests' => $this->storedTrackingActions];
|
||
|
||
// token_auth is not required by default, except if bulk_requests_require_authentication=1
|
||
if (!empty($this->token_auth)) {
|
||
$data['token_auth'] = $this->token_auth;
|
||
}
|
||
|
||
$postData = json_encode($data);
|
||
$response = $this->sendRequest($this->getBaseUrl(), 'POST', $postData, $force = true);
|
||
|
||
$this->storedTrackingActions = [];
|
||
|
||
return $response;
|
||
}
|
||
|
||
/**
|
||
* Tracks an Ecommerce order.
|
||
*
|
||
* If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order.
|
||
* All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Matomo reports.
|
||
* Only the parameters $orderId and $grandTotal are required.
|
||
*
|
||
* @param string|int $orderId (required) Unique Order ID.
|
||
* This will be used to count this order only once in the event the order page is reloaded several times.
|
||
* orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Matomo.
|
||
* @param float $grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.)
|
||
* @param float $subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied)
|
||
* @param float $tax (optional) Tax amount for this order
|
||
* @param float $shipping (optional) Shipping amount for this order
|
||
* @param float $discount (optional) Discounted amount in this order
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackEcommerceOrder(
|
||
$orderId,
|
||
float $grandTotal,
|
||
float $subTotal = 0.0,
|
||
float $tax = 0.0,
|
||
float $shipping = 0.0,
|
||
float $discount = 0.0
|
||
) {
|
||
$url = $this->getUrlTrackEcommerceOrder($orderId, $grandTotal, $subTotal, $tax, $shipping, $discount);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Tracks a PHP Throwable a crash (requires CrashAnalytics to be enabled in the target Matomo)
|
||
*
|
||
* @param Throwable $throwable (required) the throwable to track. The message, stack trace, file location and line number
|
||
* of the crash are deduced from this parameter. The crash type is set to the class name of
|
||
* the Throwable.
|
||
* @param string|null $category (optional) a category value for this crash. This can be any information you want
|
||
* to attach to the crash.
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackPhpThrowable(Throwable $throwable, ?string $category = null)
|
||
{
|
||
$message = $throwable->getMessage();
|
||
$stack = $throwable->getTraceAsString();
|
||
$type = get_class($throwable);
|
||
$location = $throwable->getFile();
|
||
$line = $throwable->getLine();
|
||
|
||
return $this->doTrackCrash($message, $type, $category, $stack, $location, $line);
|
||
}
|
||
|
||
/**
|
||
* Track a crash (requires CrashAnalytics to be enabled in the target Matomo)
|
||
*
|
||
* @param string $message (required) the error message.
|
||
* @param string|null $type (optional) the error type, such as the class name of an Exception.
|
||
* @param string|null $category (optional) a category value for this crash. This can be any information you want
|
||
* to attach to the crash.
|
||
* @param string|null $stack (optional) the stack trace of the crash.
|
||
* @param string|null $location (optional) the source file URI where the crash originated.
|
||
* @param int|null $line (optional) the source file line where the crash originated.
|
||
* @param int|null $column (optional) the source file column where the crash originated.
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doTrackCrash(
|
||
string $message,
|
||
?string $type = null,
|
||
?string $category = null,
|
||
?string $stack = null,
|
||
?string $location = null,
|
||
?int $line = null,
|
||
?int $column = null
|
||
) {
|
||
$url = $this->getUrlTrackCrash($message, $type, $category, $stack, $location, $line, $column);
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Sends a ping request.
|
||
*
|
||
* Ping requests do not track new actions. If they are sent within the standard visit length (see global.ini.php),
|
||
* they will extend the existing visit and the current last action for the visit. If after the standard visit length,
|
||
* ping requests will create a new visit using the last action in the last known visit.
|
||
*
|
||
* @return mixed Response or true if using bulk request
|
||
*/
|
||
public function doPing()
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&ping=1';
|
||
|
||
return $this->sendRequest($url);
|
||
}
|
||
|
||
/**
|
||
* Sets the current page view as an item (product) page view, or an Ecommerce Category page view.
|
||
*
|
||
* This must be called before doTrackPageView() on this product/category page.
|
||
*
|
||
* On a category page, you may set the parameter $category only and set the other parameters to false.
|
||
*
|
||
* Tracking Product/Category page views will allow Matomo to report on Product & Categories
|
||
* conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category)
|
||
*
|
||
* @param string $sku Product SKU being viewed
|
||
* @param string $name Product Name being viewed
|
||
* @param string|array $category Category being viewed. On a Product page, this is the product's category.
|
||
* You can also specify an array of up to 5 categories for a given page view.
|
||
* @param float $price Specify the price at which the item was displayed
|
||
* @return $this
|
||
*/
|
||
public function setEcommerceView(
|
||
string $sku = '',
|
||
string $name = '',
|
||
$category = '',
|
||
float $price = 0.0
|
||
) {
|
||
$this->ecommerceView = [];
|
||
|
||
if (!empty($category)) {
|
||
if (is_array($category)) {
|
||
$category = json_encode($category);
|
||
}
|
||
} else {
|
||
$category = "";
|
||
}
|
||
$this->ecommerceView['_pkc'] = $category;
|
||
|
||
if (!empty($price)) {
|
||
$price = $this->forceDotAsSeparatorForDecimalPoint($price);
|
||
$this->ecommerceView['_pkp'] = $price;
|
||
}
|
||
|
||
// On a category page, do not record "Product name not defined"
|
||
if (empty($sku) && empty($name)) {
|
||
return $this;
|
||
}
|
||
if (!empty($sku)) {
|
||
$this->ecommerceView['_pks'] = $sku;
|
||
}
|
||
if (empty($name)) {
|
||
$name = '';
|
||
}
|
||
$this->ecommerceView['_pkn'] = $name;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Force the separator for decimal point to be a dot. See https://github.com/matomo-org/matomo/issues/6435
|
||
* If for instance a German locale is used it would be a comma otherwise.
|
||
*
|
||
* @param float|string $value
|
||
*/
|
||
private function forceDotAsSeparatorForDecimalPoint($value): string
|
||
{
|
||
if (null === $value || false === $value) {
|
||
return $value;
|
||
}
|
||
|
||
return str_replace(',', '.', $value);
|
||
}
|
||
|
||
/**
|
||
* TODO: tests
|
||
* TODO: docs
|
||
*
|
||
* @return string
|
||
*/
|
||
public function getUrlTrackAIBot(?int $httpStatus = null, ?int $responseSizeBytes = null, ?int $serverTimeMs = null, ?string $source = null): string
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
|
||
$url .= '&recMode=1';
|
||
|
||
if (!empty($httpStatus)) {
|
||
$url .= '&http_status=' . $httpStatus;
|
||
}
|
||
|
||
if (!empty($responseSizeBytes)) {
|
||
$url .= '&bw_bytes=' . $responseSizeBytes;
|
||
}
|
||
|
||
if (!empty($serverTimeMs)) {
|
||
$url .= '&pf_srv=' . $serverTimeMs;
|
||
}
|
||
|
||
if (!empty($source)) {
|
||
$url .= '&source=' . rawurlencode($source);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Returns URL used to track Ecommerce Cart updates
|
||
* Calling this function will reinitializes the property ecommerceItems to empty array
|
||
* so items will have to be added again via addEcommerceItem()
|
||
* @ignore
|
||
*/
|
||
public function getUrlTrackEcommerceCartUpdate($grandTotal)
|
||
{
|
||
return $this->getUrlTrackEcommerce($grandTotal);
|
||
}
|
||
|
||
/**
|
||
* Returns URL used to track Ecommerce Orders
|
||
* Calling this function will reinitializes the property ecommerceItems to empty array
|
||
* so items will have to be added again via addEcommerceItem()
|
||
* @ignore
|
||
*/
|
||
public function getUrlTrackEcommerceOrder(
|
||
$orderId,
|
||
$grandTotal,
|
||
$subTotal = 0.0,
|
||
$tax = 0.0,
|
||
$shipping = 0.0,
|
||
$discount = 0.0
|
||
) {
|
||
if (empty($orderId)) {
|
||
throw new Exception("You must specifiy an orderId for the Ecommerce order");
|
||
}
|
||
$url = $this->getUrlTrackEcommerce($grandTotal, $subTotal, $tax, $shipping, $discount);
|
||
$url .= '&ec_id=' . urlencode($orderId);
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Returns URL used to track Ecommerce orders
|
||
*
|
||
* Calling this function will reinitializes the property ecommerceItems to empty array
|
||
* so items will have to be added again via addEcommerceItem()
|
||
*
|
||
* @ignore
|
||
*/
|
||
protected function getUrlTrackEcommerce($grandTotal, $subTotal = 0.0, $tax = 0.0, $shipping = 0.0, $discount = 0.0)
|
||
{
|
||
if (!is_numeric($grandTotal)) {
|
||
throw new Exception("You must specifiy a grandTotal for the Ecommerce order (or Cart update)");
|
||
}
|
||
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&idgoal=0';
|
||
if (!empty($grandTotal)) {
|
||
$grandTotal = $this->forceDotAsSeparatorForDecimalPoint($grandTotal);
|
||
$url .= '&revenue=' . $grandTotal;
|
||
}
|
||
if (!empty($subTotal)) {
|
||
$subTotal = $this->forceDotAsSeparatorForDecimalPoint($subTotal);
|
||
$url .= '&ec_st=' . $subTotal;
|
||
}
|
||
if (!empty($tax)) {
|
||
$tax = $this->forceDotAsSeparatorForDecimalPoint($tax);
|
||
$url .= '&ec_tx=' . $tax;
|
||
}
|
||
if (!empty($shipping)) {
|
||
$shipping = $this->forceDotAsSeparatorForDecimalPoint($shipping);
|
||
$url .= '&ec_sh=' . $shipping;
|
||
}
|
||
if (!empty($discount)) {
|
||
$discount = $this->forceDotAsSeparatorForDecimalPoint($discount);
|
||
$url .= '&ec_dt=' . $discount;
|
||
}
|
||
if (!empty($this->ecommerceItems)) {
|
||
$url .= '&ec_items=' . urlencode(json_encode($this->ecommerceItems));
|
||
}
|
||
$this->ecommerceItems = array();
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a page view.
|
||
*
|
||
* @see doTrackPageView()
|
||
* @param string $documentTitle Page view name as it will appear in Matomo reports
|
||
* @return string URL to matomo.php with all parameters set to track the pageview
|
||
*/
|
||
public function getUrlTrackPageView(string $documentTitle = ''): string
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
if (strlen($documentTitle) > 0) {
|
||
$url .= '&action_name=' . urlencode($documentTitle);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a custom event.
|
||
*
|
||
* @see doTrackEvent()
|
||
* @param string $category The Event Category (Videos, Music, Games...)
|
||
* @param string $action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...)
|
||
* @param string|bool $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...)
|
||
* @param float|bool $value (optional) The Event's value
|
||
* @return string URL to matomo.php with all parameters set to track the pageview
|
||
* @throws
|
||
*/
|
||
public function getUrlTrackEvent(
|
||
string $category,
|
||
string $action,
|
||
$name = false,
|
||
$value = false
|
||
): string {
|
||
$url = $this->getRequest($this->idSite);
|
||
if (strlen($category) === 0) {
|
||
throw new Exception("You must specify an Event Category name (Music, Videos, Games...).");
|
||
}
|
||
if (strlen($action) === 0) {
|
||
throw new Exception("You must specify an Event action (click, view, add...).");
|
||
}
|
||
|
||
$url .= '&e_c=' . urlencode($category);
|
||
$url .= '&e_a=' . urlencode($action);
|
||
|
||
if (strlen($name) > 0) {
|
||
$url .= '&e_n=' . urlencode($name);
|
||
}
|
||
if (strlen($value) > 0) {
|
||
$value = $this->forceDotAsSeparatorForDecimalPoint($value);
|
||
$url .= '&e_v=' . $value;
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a content impression.
|
||
*
|
||
* @see doTrackContentImpression()
|
||
* @param string $contentName The name of the content. For instance 'Ad Foo Bar'
|
||
* @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
|
||
* @param string|false $contentTarget (optional) The target of the content. For instance the URL of a landing page.
|
||
* @throws Exception In case $contentName is empty
|
||
* @return string URL to matomo.php with all parameters set to track the pageview
|
||
*/
|
||
public function getUrlTrackContentImpression(
|
||
string $contentName,
|
||
string $contentPiece,
|
||
$contentTarget
|
||
): string {
|
||
$url = $this->getRequest($this->idSite);
|
||
|
||
if (strlen($contentName) === 0) {
|
||
throw new Exception("You must specify a content name");
|
||
}
|
||
|
||
$url .= '&c_n=' . urlencode($contentName);
|
||
|
||
if (!empty($contentPiece) && strlen($contentPiece) > 0) {
|
||
$url .= '&c_p=' . urlencode($contentPiece);
|
||
}
|
||
if (!empty($contentTarget) && strlen($contentTarget) > 0) {
|
||
$url .= '&c_t=' . urlencode($contentTarget);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a content interaction.
|
||
*
|
||
* @see doTrackContentInteraction()
|
||
* @param string $interaction The name of the interaction with the content. For instance a 'click'
|
||
* @param string $contentName The name of the content. For instance 'Ad Foo Bar'
|
||
* @param string $contentPiece The actual content. For instance the path to an image, video, audio, any text
|
||
* @param string|false $contentTarget (optional) The target the content leading to when an interaction occurs. For instance the URL of a landing page.
|
||
* @throws Exception In case $interaction or $contentName is empty
|
||
* @return string URL to matomo.php with all parameters set to track the pageview
|
||
*/
|
||
public function getUrlTrackContentInteraction(
|
||
string $interaction,
|
||
string $contentName,
|
||
string $contentPiece,
|
||
$contentTarget
|
||
): string {
|
||
$url = $this->getRequest($this->idSite);
|
||
|
||
if (strlen($interaction) === 0) {
|
||
throw new Exception("You must specify a name for the interaction");
|
||
}
|
||
|
||
if (strlen($contentName) === 0) {
|
||
throw new Exception("You must specify a content name");
|
||
}
|
||
|
||
$url .= '&c_i=' . urlencode($interaction);
|
||
$url .= '&c_n=' . urlencode($contentName);
|
||
|
||
if (!empty($contentPiece) && strlen($contentPiece) > 0) {
|
||
$url .= '&c_p=' . urlencode($contentPiece);
|
||
}
|
||
if (!empty($contentTarget) && strlen($contentTarget) > 0) {
|
||
$url .= '&c_t=' . urlencode($contentTarget);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a site search.
|
||
*
|
||
* @see doTrackSiteSearch()
|
||
*/
|
||
public function getUrlTrackSiteSearch(string $keyword, string $category, int $countResults): string
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&search=' . urlencode($keyword);
|
||
if (strlen($category) > 0) {
|
||
$url .= '&search_cat=' . urlencode($category);
|
||
}
|
||
if (!empty($countResults) || $countResults === 0) {
|
||
$url .= '&search_count=' . (int)$countResults;
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a goal with idGoal and revenue.
|
||
*
|
||
* @see doTrackGoal()
|
||
* @param int $idGoal Id Goal to record a conversion
|
||
* @param float $revenue Revenue for this conversion
|
||
* @return string URL to matomo.php with all parameters set to track the goal conversion
|
||
*/
|
||
public function getUrlTrackGoal(int $idGoal, float $revenue = 0.0): string
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&idgoal=' . $idGoal;
|
||
if (!empty($revenue)) {
|
||
$revenue = $this->forceDotAsSeparatorForDecimalPoint($revenue);
|
||
$url .= '&revenue=' . $revenue;
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a new action.
|
||
*
|
||
* @see doTrackAction()
|
||
* @param string $actionUrl URL of the download or outlink
|
||
* @param string $actionType Type of the action: 'download' or 'link'
|
||
* @return string URL to matomo.php with all parameters set to track an action
|
||
*/
|
||
public function getUrlTrackAction(string $actionUrl, string $actionType): string
|
||
{
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&' . $actionType . '=' . urlencode($actionUrl);
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Builds URL to track a crash.
|
||
*
|
||
* @see doTrackCrash()
|
||
* @param string $message (required) the error message.
|
||
* @param string|null $type (optional) the error type, such as the class name of an Exception.
|
||
* @param string|null $category (optional) a category value for this crash. This can be any information you want
|
||
* to attach to the crash.
|
||
* @param string|null $stack (optional) the stack trace of the crash.
|
||
* @param string|null $location (optional) the source file URI where the crash originated.
|
||
* @param int|null $line (optional) the source file line where the crash originated.
|
||
* @param int|null $column (optional) the source file column where the crash originated.
|
||
* @return string URL to matomo.php with all parameters set to track an action
|
||
*/
|
||
public function getUrlTrackCrash(
|
||
string $message,
|
||
?string $type = null,
|
||
?string $category = null,
|
||
?string $stack = null,
|
||
?string $location = null,
|
||
?int $line = null,
|
||
?int $column = null
|
||
): string {
|
||
$url = $this->getRequest($this->idSite);
|
||
$url .= '&ca=1&cra=' . urlencode($message);
|
||
if ($type) {
|
||
$url .= '&cra_tp=' . urlencode($type);
|
||
}
|
||
if ($category) {
|
||
$url .= '&cra_ct=' . urlencode($category);
|
||
}
|
||
if ($stack) {
|
||
$url .= '&cra_st=' . urlencode($stack);
|
||
}
|
||
if ($location) {
|
||
$url .= '&cra_ru=' . urlencode($location);
|
||
}
|
||
if ($line) {
|
||
$url .= '&cra_rl=' . urlencode($line);
|
||
}
|
||
if ($column) {
|
||
$url .= '&cra_rc=' . urlencode($column);
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Overrides server date and time for the tracking requests.
|
||
* By default Matomo will track requests for the "current datetime" but this function allows you
|
||
* to track visits in the past. All times are in UTC.
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth()
|
||
* @see setTokenAuth()
|
||
* @param string $dateTime Date with the format 'Y-m-d H:i:s', or a UNIX timestamp.
|
||
* If the datetime is older than one day (default value for tracking_requests_require_authentication_when_custom_timestamp_newer_than), then you must call setTokenAuth() with a valid Admin/Super user token.
|
||
* @return $this
|
||
*/
|
||
public function setForceVisitDateTime(string $dateTime)
|
||
{
|
||
$this->forcedDatetime = $dateTime;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Forces Matomo to create a new visit for the tracking request.
|
||
*
|
||
* By default, Matomo will create a new visit if the last request by this user was more than 30 minutes ago.
|
||
* If you call setForceNewVisit() before calling doTrack*, then a new visit will be created for this request.
|
||
* @return $this
|
||
*/
|
||
public function setForceNewVisit()
|
||
{
|
||
$this->forcedNewVisit = true;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Overrides IP address
|
||
*
|
||
* Allowed only for Admin/Super User, must be used along with setTokenAuth()
|
||
* @see setTokenAuth()
|
||
* @param string $ip IP string, eg. 130.54.2.1
|
||
* @return $this
|
||
*/
|
||
public function setIp(string $ip)
|
||
{
|
||
$this->ip = $ip;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Force the action to be recorded for a specific User. The User ID is a string representing a given user in your system.
|
||
*
|
||
* A User ID can be a username, UUID or an email address, or any number or string that uniquely identifies a user or client.
|
||
*
|
||
* @param string $userId Any user ID string (eg. email address, ID, username). Must be non empty. Set to false to de-assign a user id previously set.
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setUserId(string $userId)
|
||
{
|
||
if ($userId === '') {
|
||
throw new Exception("User ID cannot be empty.");
|
||
}
|
||
$this->userId = $userId;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Hash function used internally by Matomo to hash a User ID into the Visitor ID.
|
||
*
|
||
* Note: matches implementation of Tracker\Request->getUserIdHashed()
|
||
*
|
||
* @return string
|
||
*/
|
||
public static function getUserIdHashed($id): string
|
||
{
|
||
return substr(sha1($id), 0, 16);
|
||
}
|
||
|
||
/**
|
||
* Forces the requests to be recorded for the specified Visitor ID.
|
||
*
|
||
* Rather than letting Matomo attribute the user with a heuristic based on IP and other user fingeprinting attributes,
|
||
* force the action to be recorded for a particular visitor.
|
||
*
|
||
* If not set, the visitor ID will be fetched from the 1st party cookie, or will be set to a random UUID.
|
||
*
|
||
* @param string $visitorId 16 hexadecimal characters visitor ID, eg. "33c31e01394bdc63"
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setVisitorId(string $visitorId)
|
||
{
|
||
$hexChars = '01234567890abcdefABCDEF';
|
||
if (strlen($visitorId) !== self::LENGTH_VISITOR_ID
|
||
|| strspn($visitorId, $hexChars) !== strlen($visitorId)
|
||
) {
|
||
throw new Exception(
|
||
"setVisitorId() expects a "
|
||
. self::LENGTH_VISITOR_ID
|
||
. " characters hexadecimal string (containing only the following: "
|
||
. $hexChars
|
||
. ")"
|
||
);
|
||
}
|
||
$this->forcedVisitorId = $visitorId;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* If the user initiating the request has the Matomo first party cookie,
|
||
* this function will try and return the ID parsed from this first party cookie (found in $_COOKIE).
|
||
*
|
||
* If you call this function from a server, where the call is triggered by a cron or script
|
||
* not initiated by the actual visitor being tracked, then it will return
|
||
* the random Visitor ID that was assigned to this visit object.
|
||
*
|
||
* This can be used if you wish to record more visits, actions or goals for this visitor ID later on.
|
||
*
|
||
* @return string 16 hex chars visitor ID string
|
||
*/
|
||
public function getVisitorId()
|
||
{
|
||
if (!empty($this->forcedVisitorId)) {
|
||
return $this->forcedVisitorId;
|
||
}
|
||
if ($this->loadVisitorIdCookie()) {
|
||
return $this->cookieVisitorId;
|
||
}
|
||
|
||
return $this->randomVisitorId;
|
||
}
|
||
|
||
/**
|
||
* Returns the currently set user agent.
|
||
* @return string
|
||
*/
|
||
public function getUserAgent()
|
||
{
|
||
return $this->userAgent;
|
||
}
|
||
|
||
/**
|
||
* Returns the currently set IP address.
|
||
* @return string
|
||
*/
|
||
public function getIp()
|
||
{
|
||
return $this->ip;
|
||
}
|
||
|
||
/**
|
||
* Returns the User ID string, which may have been set via:
|
||
* $v->setUserId('username@example.org');
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function getUserId()
|
||
{
|
||
return $this->userId;
|
||
}
|
||
|
||
/**
|
||
* Loads values from the VisitorId Cookie
|
||
*
|
||
* @return bool True if cookie exists and is valid, False otherwise
|
||
*/
|
||
protected function loadVisitorIdCookie(): bool
|
||
{
|
||
$idCookie = $this->getCookieMatchingName('id');
|
||
if ($idCookie === false) {
|
||
return false;
|
||
}
|
||
$parts = explode('.', $idCookie);
|
||
if (strlen($parts[0]) !== self::LENGTH_VISITOR_ID) {
|
||
return false;
|
||
}
|
||
|
||
/* $this->cookieVisitorId provides backward compatibility since getVisitorId()
|
||
didn't change any existing VisitorId value */
|
||
$this->cookieVisitorId = $parts[0];
|
||
$this->createTs = $parts[1];
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Deletes all first party cookies from the client
|
||
*/
|
||
public function deleteCookies(): void
|
||
{
|
||
$cookies = array('id', 'ses', 'cvar', 'ref');
|
||
foreach ($cookies as $cookie) {
|
||
$this->setCookie($cookie, '', -86400);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the currently assigned Attribution Information stored in a first party cookie.
|
||
*
|
||
* This function will only work if the user is initiating the current request, and his cookies
|
||
* can be read by PHP from the $_COOKIE array.
|
||
*
|
||
* @return string JSON Encoded string containing the Referrer information for Goal conversion attribution.
|
||
* Will return false if the cookie could not be found
|
||
* @see matomo.js getAttributionInfo()
|
||
*/
|
||
public function getAttributionInfo()
|
||
{
|
||
if (!empty($this->attributionInfo)) {
|
||
return json_encode($this->attributionInfo);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Some Tracking API functionality requires express authentication, using either the
|
||
* Super User token_auth, or a user with 'admin' access to the website.
|
||
*
|
||
* The following features require access:
|
||
* - force the visitor IP
|
||
* - force the date & time of the tracking requests rather than track for the current datetime
|
||
*
|
||
* @param string $token_auth token_auth 32 chars token_auth string
|
||
* @return $this
|
||
*/
|
||
public function setTokenAuth(string $token_auth)
|
||
{
|
||
$this->token_auth = $token_auth;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets local visitor time
|
||
*
|
||
* @param string $time HH:MM:SS format
|
||
* @return $this
|
||
*/
|
||
public function setLocalTime(string $time)
|
||
{
|
||
[$hour, $minute, $second] = explode(':', $time);
|
||
$this->localHour = (int)$hour;
|
||
$this->localMinute = (int)$minute;
|
||
$this->localSecond = (int)$second;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets user resolution width and height.
|
||
*
|
||
* @param int $width
|
||
* @param int $height
|
||
* @return $this
|
||
*/
|
||
public function setResolution(int $width, int $height)
|
||
{
|
||
$this->width = $width;
|
||
$this->height = $height;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets if the browser supports cookies
|
||
* This is reported in "List of plugins" report in Matomo.
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setBrowserHasCookies(bool $hasCookies)
|
||
{
|
||
$this->hasCookies = $hasCookies;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Will append a custom string at the end of the Tracking request.
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setDebugStringAppend(string $debugString)
|
||
{
|
||
$this->DEBUG_APPEND_URL = '&' . $debugString;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets visitor browser supported plugins
|
||
*
|
||
* @return $this
|
||
*/
|
||
public function setPlugins(
|
||
bool $flash = false,
|
||
bool $java = false,
|
||
bool $quickTime = false,
|
||
bool $realPlayer = false,
|
||
bool $pdf = false,
|
||
bool $windowsMedia = false,
|
||
bool $silverlight = false
|
||
) {
|
||
$this->plugins =
|
||
'&fla=' . (int)$flash .
|
||
'&java=' . (int)$java .
|
||
'&qt=' . (int)$quickTime .
|
||
'&realp=' . (int)$realPlayer .
|
||
'&pdf=' . (int)$pdf .
|
||
'&wma=' . (int)$windowsMedia .
|
||
'&ag=' . (int)$silverlight;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* By default, MatomoTracker will read first party cookies
|
||
* from the request and write updated cookies in the response (using setrawcookie).
|
||
* This can be disabled by calling this function.
|
||
*/
|
||
public function disableCookieSupport(): void
|
||
{
|
||
$this->configCookiesDisabled = true;
|
||
}
|
||
|
||
/**
|
||
* Returns the maximum number of seconds the tracker will spend waiting for a response
|
||
* from Matomo. Defaults to 600 seconds.
|
||
*/
|
||
public function getRequestTimeout(): int
|
||
{
|
||
return $this->requestTimeout;
|
||
}
|
||
|
||
/**
|
||
* Sets the maximum number of seconds that the tracker will spend waiting for a response
|
||
* from Matomo.
|
||
*
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setRequestTimeout(int $timeout)
|
||
{
|
||
if ($timeout < 0) {
|
||
throw new Exception("Invalid value supplied for request timeout: $timeout");
|
||
}
|
||
|
||
$this->requestTimeout = $timeout;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Returns the maximum number of seconds the tracker will spend trying to connect to Matomo.
|
||
* Defaults to 300 seconds.
|
||
*/
|
||
public function getRequestConnectTimeout(): int
|
||
{
|
||
return $this->requestConnectTimeout;
|
||
}
|
||
|
||
/**
|
||
* Sets the maximum number of seconds that the tracker will spend tryint to connect to Matomo.
|
||
*
|
||
* @param int $timeout
|
||
* @return $this
|
||
* @throws Exception
|
||
*/
|
||
public function setRequestConnectTimeout(int $timeout)
|
||
{
|
||
if ($timeout < 0) {
|
||
throw new Exception("Invalid value supplied for request connect timeout: $timeout");
|
||
}
|
||
|
||
$this->requestConnectTimeout = $timeout;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets the request method to POST, which is recommended when using setTokenAuth()
|
||
* to prevent the token from being recorded in server logs. Avoid using redirects
|
||
* when using POST to prevent the loss of POST values. When using Log Analytics,
|
||
* be aware that POST requests are not parseable/replayable.
|
||
*
|
||
* @param string $method Either 'POST' or 'GET'
|
||
* @return $this
|
||
*/
|
||
public function setRequestMethodNonBulk(string $method)
|
||
{
|
||
$this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET';
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* If a proxy is needed to look up the address of the Matomo site, set it with this
|
||
* @param string $proxy IP as string, for example "173.234.92.107"
|
||
*/
|
||
public function setProxy(string $proxy, int $proxyPort = 80): void
|
||
{
|
||
$this->proxy = $proxy;
|
||
$this->proxyPort = $proxyPort;
|
||
}
|
||
|
||
/**
|
||
* If the proxy IP and the proxy port have been set, with the setProxy() function
|
||
* returns a string, like "173.234.92.107:80"
|
||
*/
|
||
private function getProxy(): ?string
|
||
{
|
||
if (isset($this->proxy) && isset($this->proxyPort)) {
|
||
return $this->proxy.":".$this->proxyPort;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Used in tests to output useful error messages.
|
||
*
|
||
* @ignore
|
||
*/
|
||
static public $DEBUG_LAST_REQUESTED_URL = false;
|
||
|
||
/**
|
||
* Returns array of curl options for request
|
||
*
|
||
* @return array<int, mixed>
|
||
*/
|
||
protected function prepareCurlOptions(
|
||
string $url,
|
||
string $method,
|
||
$data,
|
||
bool $forcePostUrlEncoded
|
||
): array {
|
||
$options = [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_USERAGENT => $this->userAgent,
|
||
CURLOPT_HEADER => true,
|
||
CURLOPT_TIMEOUT => $this->requestTimeout,
|
||
CURLOPT_CONNECTTIMEOUT => $this->requestConnectTimeout,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Accept-Language: ' . $this->acceptLanguage,
|
||
],
|
||
];
|
||
|
||
if ($method === 'GET') {
|
||
$options[CURLOPT_FOLLOWLOCATION] = true;
|
||
}
|
||
|
||
if (defined('PATH_TO_CERTIFICATES_FILE')) {
|
||
$options[CURLOPT_CAINFO] = PATH_TO_CERTIFICATES_FILE;
|
||
}
|
||
|
||
$proxy = $this->getProxy();
|
||
if (isset($proxy)) {
|
||
$options[CURLOPT_PROXY] = $proxy;
|
||
}
|
||
|
||
switch ($method) {
|
||
case 'POST':
|
||
$options[CURLOPT_POST] = true;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// only supports JSON data
|
||
if (!empty($data) && $forcePostUrlEncoded) {
|
||
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
|
||
$options[CURLOPT_POSTFIELDS] = $data;
|
||
$options[CURLOPT_POST] = true;
|
||
if (defined('CURL_REDIR_POST_ALL')) {
|
||
$options[CURLOPT_POSTREDIR] = CURL_REDIR_POST_ALL;
|
||
$options[CURLOPT_FOLLOWLOCATION] = true;
|
||
}
|
||
} elseif (!empty($data)) {
|
||
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
|
||
$options[CURLOPT_HTTPHEADER][] = 'Expect:';
|
||
$options[CURLOPT_POSTFIELDS] = $data;
|
||
}
|
||
|
||
if (!empty($this->outgoingTrackerCookies)) {
|
||
$options[CURLOPT_COOKIE] = http_build_query($this->outgoingTrackerCookies);
|
||
$this->outgoingTrackerCookies = array();
|
||
}
|
||
|
||
return $options;
|
||
}
|
||
|
||
/**
|
||
* Returns array of stream options for request
|
||
*
|
||
* @return array{http: array<string, mixed>}
|
||
*/
|
||
protected function prepareStreamOptions(string $method, $data, bool $forcePostUrlEncoded): array
|
||
{
|
||
$stream_options = [
|
||
'http' => [
|
||
'method' => $method,
|
||
'user_agent' => $this->userAgent,
|
||
'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n",
|
||
'timeout' => $this->requestTimeout,
|
||
],
|
||
];
|
||
|
||
$proxy = $this->getProxy();
|
||
if (isset($proxy)) {
|
||
$stream_options['http']['proxy'] = $proxy;
|
||
}
|
||
|
||
// only supports JSON data
|
||
if (!empty($data) && $forcePostUrlEncoded) {
|
||
$stream_options['http']['header'] .= "Content-Type: application/x-www-form-urlencoded \r\n";
|
||
$stream_options['http']['content'] = $data;
|
||
} elseif (!empty($data)) {
|
||
$stream_options['http']['header'] .= "Content-Type: application/json \r\n";
|
||
$stream_options['http']['content'] = $data;
|
||
}
|
||
|
||
if (!empty($this->outgoingTrackerCookies)) {
|
||
$stream_options['http']['header'] .= 'Cookie: ' . http_build_query($this->outgoingTrackerCookies) . "\r\n";
|
||
$this->outgoingTrackerCookies = array();
|
||
}
|
||
|
||
return $stream_options;
|
||
}
|
||
|
||
/**
|
||
* @ignore
|
||
*/
|
||
protected function sendRequest(string $url, string $method = 'GET', $data = null, bool $force = false): string
|
||
{
|
||
self::$DEBUG_LAST_REQUESTED_URL = $url;
|
||
|
||
// if doing a bulk request, store the url
|
||
if ($this->doBulkRequests && !$force) {
|
||
$this->storedTrackingActions[]
|
||
= $url
|
||
. (!empty($this->userAgent) ? ('&ua=' . urlencode($this->userAgent)) : '')
|
||
. (!empty($this->acceptLanguage) ? ('&lang=' . urlencode($this->acceptLanguage)) : '');
|
||
|
||
// Clear custom variables & dimensions so they don't get copied over to other users in the bulk request
|
||
$this->clearCustomVariables();
|
||
$this->clearCustomDimensions();
|
||
$this->clearCustomTrackingParameters();
|
||
$this->userAgent = false;
|
||
$this->clientHints = false;
|
||
$this->acceptLanguage = false;
|
||
|
||
return true;
|
||
}
|
||
|
||
$forcePostUrlEncoded = false;
|
||
if (!$this->doBulkRequests) {
|
||
if (!empty($this->requestMethod) && strtoupper($this->requestMethod) === 'POST') {
|
||
// POST ALL parameters and have no GET parameters
|
||
$urlParts = explode('?', $url);
|
||
|
||
$url = $urlParts[0];
|
||
$data = $urlParts[1];
|
||
$forcePostUrlEncoded = true;
|
||
|
||
$method = 'POST';
|
||
}
|
||
|
||
if (!empty($this->token_auth)) {
|
||
$appendTokenString = '&token_auth=' . urlencode($this->token_auth);
|
||
|
||
if (empty($this->requestMethod) || $method === 'POST') {
|
||
// Only post token_auth but use GET URL parameters for everything else
|
||
$forcePostUrlEncoded = true;
|
||
if (empty($data)) {
|
||
$data = '';
|
||
}
|
||
$data .= $appendTokenString;
|
||
$data = ltrim($data, '&'); // when no request method set we don't want it to start with '&'
|
||
} elseif (!empty($this->token_auth)) {
|
||
// Use GET for all URL parameters
|
||
$url .= $appendTokenString;
|
||
}
|
||
}
|
||
}
|
||
|
||
$content = '';
|
||
|
||
if (function_exists('curl_init') && function_exists('curl_exec')) {
|
||
$options = $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded);
|
||
|
||
$ch = curl_init();
|
||
curl_setopt_array($ch, $options);
|
||
ob_start();
|
||
$response = @curl_exec($ch);
|
||
|
||
try {
|
||
$header = '';
|
||
|
||
if ($response === false) {
|
||
$curlError = curl_error($ch);
|
||
if (!empty($curlError)) {
|
||
throw new \RuntimeException($curlError);
|
||
}
|
||
}
|
||
|
||
if (!empty($response)) {
|
||
// extract header
|
||
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||
$header = substr($response, 0, $headerSize);
|
||
|
||
// extract content
|
||
$content = substr($response, $headerSize);
|
||
}
|
||
|
||
$this->parseIncomingCookies(explode("\r\n", $header));
|
||
} finally {
|
||
curl_close($ch);
|
||
ob_end_clean();
|
||
}
|
||
} elseif (function_exists('stream_context_create')) {
|
||
$stream_options = $this->prepareStreamOptions($method, $data, $forcePostUrlEncoded);
|
||
|
||
$ctx = stream_context_create($stream_options);
|
||
$response = file_get_contents($url, 0, $ctx);
|
||
$content = $response;
|
||
|
||
$this->parseIncomingCookies($http_response_header);
|
||
}
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* Returns current timestamp, or forced timestamp/datetime if it was set
|
||
* @return string|int
|
||
*/
|
||
protected function getTimestamp()
|
||
{
|
||
return !empty($this->forcedDatetime)
|
||
? strtotime($this->forcedDatetime)
|
||
: time();
|
||
}
|
||
|
||
/**
|
||
* Returns the base URL for the Matomo server.
|
||
*/
|
||
protected function getBaseUrl(): string
|
||
{
|
||
if (empty(self::$URL)) {
|
||
throw new Exception(
|
||
'You must first set the Matomo Tracker URL by calling
|
||
MatomoTracker::$URL = \'http://your-website.org/matomo/\';'
|
||
);
|
||
}
|
||
if (strpos(self::$URL, '/matomo.php') === false
|
||
&& strpos(self::$URL, '/proxy-matomo.php') === false
|
||
) {
|
||
self::$URL = rtrim(self::$URL, '/');
|
||
self::$URL .= '/matomo.php';
|
||
}
|
||
|
||
return self::$URL;
|
||
}
|
||
|
||
/**
|
||
* @ignore
|
||
*/
|
||
protected function getRequest(int $idSite): string
|
||
{
|
||
$this->setFirstPartyCookies();
|
||
|
||
$customFields = '';
|
||
if (!empty($this->customParameters)) {
|
||
$customFields = '&' . http_build_query($this->customParameters, '', '&');
|
||
}
|
||
|
||
$customDimensions = '';
|
||
if (!empty($this->customDimensions)) {
|
||
$customDimensions = '&' . http_build_query($this->customDimensions, '', '&');
|
||
}
|
||
|
||
$baseUrl = $this->getBaseUrl();
|
||
$start = '?';
|
||
if (strpos($baseUrl, '?') !== false) {
|
||
$start = '&';
|
||
}
|
||
|
||
$url = $baseUrl . $start .
|
||
'idsite=' . $idSite .
|
||
'&rec=1' .
|
||
'&apiv=' . self::VERSION .
|
||
'&r=' . substr(strval(mt_rand()), 2, 6) .
|
||
|
||
// XDEBUG_SESSIONS_START and KEY are related to the PHP Debugger, this can be ignored in other languages
|
||
(!empty($_GET['XDEBUG_SESSION_START']) ?
|
||
'&XDEBUG_SESSION_START=' . @urlencode($_GET['XDEBUG_SESSION_START']) : '') .
|
||
(!empty($_GET['KEY']) ? '&KEY=' . @urlencode($_GET['KEY']) : '') .
|
||
|
||
// Only allowed for Admin/Super User, token_auth required,
|
||
((!empty($this->ip) && !empty($this->token_auth)) ? '&cip=' . $this->ip : '') .
|
||
(!empty($this->userId) ? '&uid=' . urlencode($this->userId) : '') .
|
||
(!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') .
|
||
(!empty($this->forcedNewVisit) ? '&new_visit=1' : '') .
|
||
|
||
// Values collected from cookie
|
||
'&_idts=' . $this->createTs .
|
||
|
||
// These parameters are set by the JS, but optional when using API
|
||
(!empty($this->plugins) ? $this->plugins : '') .
|
||
(($this->localHour !== false && $this->localMinute !== false && $this->localSecond !== false) ?
|
||
'&h=' . $this->localHour . '&m=' . $this->localMinute . '&s=' . $this->localSecond : '') .
|
||
(!empty($this->width) && !empty($this->height) ? '&res=' . $this->width . 'x' . $this->height : '') .
|
||
(!empty($this->hasCookies) ? '&cookie=' . $this->hasCookies : '') .
|
||
|
||
// Various important attributes
|
||
(!empty($this->customData) ? '&data=' . $this->customData : '') .
|
||
(!empty($this->visitorCustomVar) ? '&_cvar=' . urlencode(json_encode($this->visitorCustomVar)) : '') .
|
||
(!empty($this->pageCustomVar) ? '&cvar=' . urlencode(json_encode($this->pageCustomVar)) : '') .
|
||
(!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode(json_encode($this->eventCustomVar)) : '') .
|
||
(!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) .
|
||
|
||
// URL parameters
|
||
'&url=' . urlencode($this->pageUrl ?? '') .
|
||
'&urlref=' . urlencode($this->urlReferrer ?? '') .
|
||
((!empty($this->pageCharset) && $this->pageCharset != self::DEFAULT_CHARSET_PARAMETER_VALUES) ?
|
||
'&cs=' . $this->pageCharset : '') .
|
||
|
||
// unique pageview id
|
||
(!empty($this->idPageview) ? '&pv_id=' . urlencode($this->idPageview) : '') .
|
||
|
||
// Attribution information, so that Goal conversions are attributed to the right referrer or campaign
|
||
// Campaign name
|
||
(!empty($this->attributionInfo[0]) ? '&_rcn=' . urlencode($this->attributionInfo[0]) : '') .
|
||
// Campaign keyword
|
||
(!empty($this->attributionInfo[1]) ? '&_rck=' . urlencode($this->attributionInfo[1]) : '') .
|
||
// Timestamp at which the referrer was set
|
||
(!empty($this->attributionInfo[2]) ? '&_refts=' . $this->attributionInfo[2] : '') .
|
||
// Referrer URL
|
||
(!empty($this->attributionInfo[3]) ? '&_ref=' . urlencode($this->attributionInfo[3]) : '') .
|
||
|
||
// custom location info
|
||
(!empty($this->country) ? '&country=' . urlencode($this->country) : '') .
|
||
(!empty($this->region) ? '®ion=' . urlencode($this->region) : '') .
|
||
(!empty($this->city) ? '&city=' . urlencode($this->city) : '') .
|
||
(!empty($this->lat) ? '&lat=' . urlencode($this->lat) : '') .
|
||
(!empty($this->long) ? '&long=' . urlencode($this->long) : '') .
|
||
$customFields . $customDimensions .
|
||
(!$this->sendImageResponse ? '&send_image=0' : '') .
|
||
|
||
// client hints
|
||
(!empty($this->clientHints) ? ('&uadata=' . urlencode(json_encode($this->clientHints))) : '') .
|
||
|
||
// DEBUG
|
||
$this->DEBUG_APPEND_URL;
|
||
|
||
if (!empty($this->idPageview)) {
|
||
$url .=
|
||
($this->networkTime !== false ? '&pf_net=' . ((int)$this->networkTime) : '') .
|
||
($this->serverTime !== false ? '&pf_srv=' . ((int)$this->serverTime) : '') .
|
||
($this->transferTime !== false ? '&pf_tfr=' . ((int)$this->transferTime) : '') .
|
||
($this->domProcessingTime !== false ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') .
|
||
($this->domCompletionTime !== false ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') .
|
||
($this->onLoadTime !== false ? '&pf_onl=' . ((int)$this->onLoadTime) : '');
|
||
$this->clearPerformanceTimings();
|
||
}
|
||
|
||
foreach ($this->ecommerceView as $param => $value) {
|
||
$url .= '&' . $param . '=' . urlencode($value);
|
||
}
|
||
|
||
// Reset page level custom variables after this page view
|
||
$this->ecommerceView = [];
|
||
$this->pageCustomVar = [];
|
||
$this->eventCustomVar = [];
|
||
$this->clearCustomDimensions();
|
||
$this->clearCustomTrackingParameters();
|
||
|
||
// force new visit only once, user must call again setForceNewVisit()
|
||
$this->forcedNewVisit = false;
|
||
|
||
return $url;
|
||
}
|
||
|
||
|
||
/**
|
||
* Returns a first party cookie which name contains $name
|
||
*
|
||
* @return string String value of cookie, or false if not found
|
||
* @ignore
|
||
*/
|
||
protected function getCookieMatchingName(string $name)
|
||
{
|
||
if ($this->configCookiesDisabled) {
|
||
return false;
|
||
}
|
||
if (!is_array($_COOKIE)) {
|
||
return false;
|
||
}
|
||
$name = $this->getCookieName($name);
|
||
|
||
// Matomo cookie names use dots separators in matomo.js,
|
||
// but PHP Replaces . with _ http://www.php.net/manual/en/language.variables.predefined.php#72571
|
||
$name = str_replace('.', '_', $name);
|
||
foreach ($_COOKIE as $cookieName => $cookieValue) {
|
||
if (strpos($cookieName, $name) !== false) {
|
||
return $cookieValue;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
|
||
* will return "/dir1/dir2/index.php"
|
||
*
|
||
* @ignore
|
||
*/
|
||
protected static function getCurrentScriptName(): string
|
||
{
|
||
$url = '';
|
||
if (!empty($_SERVER['PATH_INFO'])) {
|
||
$url = $_SERVER['PATH_INFO'];
|
||
} else {
|
||
if (!empty($_SERVER['REQUEST_URI'])) {
|
||
if (($pos = strpos($_SERVER['REQUEST_URI'], '?')) !== false) {
|
||
$url = substr($_SERVER['REQUEST_URI'], 0, $pos);
|
||
} else {
|
||
$url = $_SERVER['REQUEST_URI'];
|
||
}
|
||
}
|
||
}
|
||
if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) {
|
||
$url = $_SERVER['SCRIPT_NAME'];
|
||
} elseif (empty($url)) {
|
||
$url = '/';
|
||
}
|
||
|
||
if (!empty($url) && $url[0] !== '/') {
|
||
$url = '/' . $url;
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* If the current URL is 'http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
|
||
* will return 'http'
|
||
*
|
||
* @return string 'https' or 'http'
|
||
* @ignore
|
||
*/
|
||
protected static function getCurrentScheme(): string
|
||
{
|
||
if (isset($_SERVER['HTTPS'])
|
||
&& ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === true)
|
||
) {
|
||
return 'https';
|
||
}
|
||
|
||
return 'http';
|
||
}
|
||
|
||
/**
|
||
* If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
|
||
* will return "http://example.org"
|
||
*
|
||
* @ignore
|
||
*/
|
||
protected static function getCurrentHost(): string
|
||
{
|
||
if (isset($_SERVER['HTTP_HOST'])) {
|
||
return $_SERVER['HTTP_HOST'];
|
||
}
|
||
|
||
return 'unknown';
|
||
}
|
||
|
||
/**
|
||
* If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"
|
||
* will return "?param1=value1¶m2=value2"
|
||
*
|
||
* @ignore
|
||
*/
|
||
protected static function getCurrentQueryString(): string
|
||
{
|
||
$url = '';
|
||
if (isset($_SERVER['QUERY_STRING'])
|
||
&& !empty($_SERVER['QUERY_STRING'])
|
||
) {
|
||
$url .= '?' . $_SERVER['QUERY_STRING'];
|
||
}
|
||
|
||
return $url;
|
||
}
|
||
|
||
/**
|
||
* Returns the current full URL (scheme, host, path and query string.
|
||
*
|
||
* @ignore
|
||
*/
|
||
protected static function getCurrentUrl(): string
|
||
{
|
||
return self::getCurrentScheme() . '://'
|
||
. self::getCurrentHost()
|
||
. self::getCurrentScriptName()
|
||
. self::getCurrentQueryString();
|
||
}
|
||
|
||
/**
|
||
* Sets the first party cookies as would the matomo.js
|
||
* All cookies are supported: 'id' and 'ses' and 'ref' and 'cvar' cookies.
|
||
* @return $this
|
||
*/
|
||
protected function setFirstPartyCookies()
|
||
{
|
||
if ($this->configCookiesDisabled) {
|
||
return $this;
|
||
}
|
||
|
||
if (empty($this->cookieVisitorId)) {
|
||
$this->loadVisitorIdCookie();
|
||
}
|
||
|
||
// Set the 'ref' cookie
|
||
$attributionInfo = $this->getAttributionInfo();
|
||
if (!empty($attributionInfo)) {
|
||
$this->setCookie('ref', $attributionInfo, $this->configReferralCookieTimeout);
|
||
}
|
||
|
||
// Set the 'ses' cookie
|
||
$this->setCookie('ses', '*', $this->configSessionCookieTimeout);
|
||
|
||
// Set the 'id' cookie
|
||
$cookieValue = $this->getVisitorId() . '.' . $this->createTs;
|
||
$this->setCookie('id', $cookieValue, $this->configVisitorCookieTimeout);
|
||
|
||
// Set the 'cvar' cookie
|
||
$this->setCookie('cvar', json_encode($this->visitorCustomVar), $this->configSessionCookieTimeout);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Sets a first party cookie to the client to improve dual JS-PHP tracking.
|
||
*
|
||
* This replicates the matomo.js tracker algorithms for consistency and better accuracy.
|
||
*
|
||
* @return $this
|
||
*/
|
||
protected function setCookie(string $cookieName, $cookieValue, int $cookieTTL)
|
||
{
|
||
$cookieExpire = $this->currentTs + $cookieTTL;
|
||
if (!headers_sent()) {
|
||
$header = 'Set-Cookie: ' . rawurlencode($this->getCookieName($cookieName)) . '=' . rawurlencode($cookieValue)
|
||
. (empty($cookieExpire) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $cookieExpire) . ' GMT')
|
||
. (empty($this->configCookiePath) ? '' : '; path=' . $this->configCookiePath)
|
||
. (empty($this->configCookieDomain) ? '' : '; domain=' . rawurlencode($this->configCookieDomain))
|
||
. (!$this->configCookieSecure ? '' : '; secure')
|
||
. (!$this->configCookieHTTPOnly ? '' : '; HttpOnly')
|
||
. (!$this->configCookieSameSite ? '' : '; SameSite=' . rawurlencode($this->configCookieSameSite));
|
||
|
||
header($header, false);
|
||
}
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* @return array
|
||
*/
|
||
protected function getCustomVariablesFromCookie()
|
||
{
|
||
$cookie = $this->getCookieMatchingName('cvar');
|
||
if (!$cookie) {
|
||
return [];
|
||
}
|
||
|
||
return json_decode($cookie, true);
|
||
}
|
||
|
||
/**
|
||
* Sets a cookie to be sent to the tracking server.
|
||
*
|
||
* @param $name
|
||
* @param $value
|
||
*/
|
||
public function setOutgoingTrackerCookie($name, $value)
|
||
{
|
||
if ($value === null) {
|
||
unset($this->outgoingTrackerCookies[$name]);
|
||
}
|
||
else {
|
||
$this->outgoingTrackerCookies[$name] = $value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets a cookie which was set by the tracking server.
|
||
*
|
||
* @param $name
|
||
*
|
||
* @return bool|string
|
||
*/
|
||
public function getIncomingTrackerCookie($name)
|
||
{
|
||
if (isset($this->incomingTrackerCookies[$name])) {
|
||
return $this->incomingTrackerCookies[$name];
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Reads incoming tracking server cookies.
|
||
*
|
||
* @param array $headers Array with HTTP response headers as values
|
||
*/
|
||
protected function parseIncomingCookies(array $headers): void
|
||
{
|
||
$this->incomingTrackerCookies = [];
|
||
|
||
if (!empty($headers)) {
|
||
$headerName = 'set-cookie:';
|
||
$headerNameLength = strlen($headerName);
|
||
|
||
foreach($headers as $header) {
|
||
if (strpos(strtolower($header), $headerName) !== 0) {
|
||
continue;
|
||
}
|
||
$cookies = trim(substr($header, $headerNameLength));
|
||
$posEnd = strpos($cookies, ';');
|
||
if ($posEnd !== false) {
|
||
$cookies = substr($cookies, 0, $posEnd);
|
||
}
|
||
parse_str($cookies, $this->incomingTrackerCookies);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns true if the given user agent belongs to a known AI bot.
|
||
*
|
||
* @param string $userAgent
|
||
* @return bool
|
||
*/
|
||
public static function isUserAgentAIBot(string $userAgent): bool
|
||
{
|
||
if (empty($userAgent)) {
|
||
return false;
|
||
}
|
||
|
||
foreach (self::AI_BOT_USER_AGENT_SUBSTRINGS as $substring) {
|
||
if (stripos($userAgent, $substring) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper function to quickly generate the URL to track a page view.
|
||
*
|
||
* @param $idSite
|
||
* @param string $documentTitle
|
||
* @return string
|
||
*/
|
||
function Matomo_getUrlTrackPageView($idSite, $documentTitle = '')
|
||
{
|
||
$tracker = new MatomoTracker($idSite);
|
||
|
||
return $tracker->getUrlTrackPageView($documentTitle);
|
||
}
|
||
|
||
/**
|
||
* Helper function to quickly generate the URL to track a goal.
|
||
*
|
||
* @param $idSite
|
||
* @param $idGoal
|
||
* @param float $revenue
|
||
* @return string
|
||
*/
|
||
function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0)
|
||
{
|
||
$tracker = new MatomoTracker($idSite);
|
||
|
||
return $tracker->getUrlTrackGoal($idGoal, $revenue);
|
||
}
|
||
|
||
/**
|
||
* Ensure PiwikTracker class is available as well
|
||
*
|
||
* @deprecated
|
||
*/
|
||
if (!class_exists('\WP_Piwik\PiwikTracker')) {
|
||
include_once('PiwikTracker.php');
|
||
}
|