From 83671a25db597315b6f21ab8d9891295182e9838 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Wed, 19 Apr 2017 16:16:35 +1200 Subject: [PATCH 001/115] Allow arrays to be used in custom parameters I was setting an array as value in a custom parameter but then the tracker fails because urlencode expects a string. I think http_build_query will be also better in general and handle pretty much all cases. --- PiwikTracker.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 16c82cd..23343fe 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1652,9 +1652,7 @@ protected function getRequest($idSite) $customFields = ''; if (!empty($this->customParameters)) { - foreach ($this->customParameters as $parameter => $value) { - $customFields .= '&' . urlencode($parameter) . '=' . urlencode($value); - } + $customFields = '&' . http_build_query($this->customParameters, '', '&'); } $url = $this->getBaseUrl() . From 7c0f9fdbde7fcba836f23d067ebc589a6a010e9f Mon Sep 17 00:00:00 2001 From: Michael Heerklotz Date: Wed, 8 Nov 2017 22:13:29 +0100 Subject: [PATCH 002/115] Added support for sending and reading Tracker Cookies --- PiwikTracker.php | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/PiwikTracker.php b/PiwikTracker.php index 23343fe..9e40975 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -158,6 +158,9 @@ public function __construct($idSite, $apiUrl = '') $this->sendImageResponse = true; $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); + + $this->outgoingTrackerCookies = array(); + $this->incomingTrackerCookies = array(); } /** @@ -1574,15 +1577,23 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $options[CURLOPT_POSTFIELDS] = $data; } + if (!empty($this->outgoingTrackerCookies)) { + $options[CURLOPT_COOKIE] = http_build_query($this->outgoingTrackerCookies); + $this->outgoingTrackerCookies = array(); + } + $ch = curl_init(); curl_setopt_array($ch, $options); ob_start(); $response = @curl_exec($ch); ob_end_clean(); + $header = ''; $content = ''; if (!empty($response)) { list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2); } + + $this->parseIncomingCookies(explode("\r\n", $header)); } elseif (function_exists('stream_context_create')) { $stream_options = array( @@ -1604,9 +1615,16 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $stream_options['http']['content'] = $data; } + if (!empty($this->outgoingTrackerCookies)) { + $stream_options['http']['header'] .= 'Cookie: ' . http_build_query($this->outgoingTrackerCookies) . "\r\n"; + $this->outgoingTrackerCookies = array(); + } + $ctx = stream_context_create($stream_options); $response = file_get_contents($url, 0, $ctx); $content = $response; + + $this->parseIncomingCookies($http_response_header); } return $content; @@ -1940,6 +1958,65 @@ protected function getCustomVariablesFromCookie() return json_decode($cookie, $assoc = 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 $headers Array with HTTP response headers as values + */ + protected function parseIncomingCookies($headers) + { + $this->incomingTrackerCookies = array(); + + 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); + } + } + } } /** From c635dc3b5ed656838d433ad5e373cb3487807185 Mon Sep 17 00:00:00 2001 From: Matthieu Aubry Date: Mon, 1 Oct 2018 22:18:11 +1300 Subject: [PATCH 003/115] Piwik -> Matomo --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e8447f0..d458bc4 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -# PHP Client for Piwik Analytics Tracking API +# PHP Client for Matomo Analytics Tracking API -The PHP Tracker Client provides all features of the [Piwik Javascript Tracker](http://developer.piwik.org/api-reference/tracking-javascript), -such as Ecommerce Tracking, Custom Variable, Event tracking and more. +The PHP Tracker Client provides all features of the [Matomo Javascript Tracker](http://developer.matomo.org/api-reference/tracking-javascript), such as Ecommerce Tracking, Custom Variable, Event tracking and more. ## Documentation and examples -Check out our [Piwik-PHP-Tracker developer documentation](http://developer.piwik.org/api-reference/PHP-Piwik-Tracker) and -[Piwik Tracking API guide](http://piwik.org/docs/tracking-api/). +Check out our [Matomo-PHP-Tracker developer documentation](http://developer.matomo.org/api-reference/PHP-Piwik-Tracker) and [Matomo Tracking API guide](http://matomo.org/docs/tracking-api/). ## Requirements: * json extension (json_decode, json_encode) -* CURL or STREAM extensions (to issue the HTTP request to Piwik) +* CURL or STREAM extensions (to issue the HTTPS request to Matomo) ## License -Released under the [BSD License](http://www.opensource.org/licenses/bsd-license.php) \ No newline at end of file +Released under the [BSD License](http://www.opensource.org/licenses/bsd-license.php) From 4a464bf8ea21d8aaa05472162543c2b1e675f93f Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Fri, 12 Oct 2018 13:32:39 +1300 Subject: [PATCH 004/115] Do not append piwik.php if url contains matomo.php --- PiwikTracker.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PiwikTracker.php b/PiwikTracker.php index 9e40975..f60a35b 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1654,6 +1654,8 @@ protected function getBaseUrl() } if (strpos(self::$URL, '/piwik.php') === false && strpos(self::$URL, '/proxy-piwik.php') === false + && strpos(self::$URL, '/matomo.php') === false + && strpos(self::$URL, '/proxy-matomo.php') === false ) { self::$URL .= '/piwik.php'; } From 72069c00796d7613d3affbb7ea98f657517f934b Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Fri, 14 Dec 2018 08:48:05 +1300 Subject: [PATCH 005/115] Custom IP should be only sent when token is specified, otherwise the request fails The CIP parameter requires a token with at least write access. See https://github.com/matomo-org/matomo/pull/13675 Didn't know the PHP tracker would set this parameter by default... --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index f60a35b..6eaa652 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1687,7 +1687,7 @@ protected function getRequest($idSite) (!empty($_GET['KEY']) ? '&KEY=' . @urlencode($_GET['KEY']) : '') . // Only allowed for Admin/Super User, token_auth required, - (!empty($this->ip) ? '&cip=' . $this->ip : '') . + ((!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' : '') . From 6f8e4ecaedbb780dc437ae644e80c1cfc6cf8cda Mon Sep 17 00:00:00 2001 From: diosmosis Date: Thu, 13 Dec 2018 17:09:42 -0800 Subject: [PATCH 006/115] Revert "Custom IP should be only sent when token is specified, otherwise the request fails" --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 6eaa652..f60a35b 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1687,7 +1687,7 @@ protected function getRequest($idSite) (!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->ip) ? '&cip=' . $this->ip : '') . (!empty($this->userId) ? '&uid=' . urlencode($this->userId) : '') . (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') . (!empty($this->forcedNewVisit) ? '&new_visit=1' : '') . From 0021ee186b00f9c81952f29ae328ac479d616528 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Thu, 13 Dec 2018 17:32:54 -0800 Subject: [PATCH 007/115] Revert "Revert "Custom IP should be only sent when token is specified, otherwise the request fails"" --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index f60a35b..6eaa652 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1687,7 +1687,7 @@ protected function getRequest($idSite) (!empty($_GET['KEY']) ? '&KEY=' . @urlencode($_GET['KEY']) : '') . // Only allowed for Admin/Super User, token_auth required, - (!empty($this->ip) ? '&cip=' . $this->ip : '') . + ((!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' : '') . From bbae9dc67d50eb8988c8f1da4ceebe794bdb764d Mon Sep 17 00:00:00 2001 From: Matthieu Aubry Date: Thu, 28 Mar 2019 13:53:11 +1300 Subject: [PATCH 008/115] add link so it shows up on https://developer.matomo.org/api-reference/PHP-Piwik-Tracker --- PiwikTracker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 6eaa652..c0c824b 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -12,9 +12,9 @@ */ /** - * PiwikTracker implements the Piwik Tracking Web API. + * PiwikTracker implements the Matomo Tracking Web API. * - * For more information, see README.md + * For more information, see: https://github.com/matomo-org/matomo-php-tracker/ * * @package PiwikTracker * @api From 86f027fd987d65ea64fe3f794911692bf8280bc5 Mon Sep 17 00:00:00 2001 From: Olle Haerstedt Date: Wed, 3 Jul 2019 13:52:37 +0200 Subject: [PATCH 009/115] Set return transfer to true for GET requests --- PiwikTracker.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index c0c824b..897d3fa 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1548,12 +1548,15 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal CURLOPT_USERAGENT => $this->userAgent, CURLOPT_HEADER => true, CURLOPT_TIMEOUT => $this->requestTimeout, - CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array( 'Accept-Language: ' . $this->acceptLanguage, ), ); + if ($method === 'GET') { + $options[CURLOPT_RETURNTRANSFER] = true; + } + if (defined('PATH_TO_CERTIFICATES_FILE')) { $options[CURLOPT_CAINFO] = PATH_TO_CERTIFICATES_FILE; } From ee74196552900750f69ab85be056d3cb70eecc58 Mon Sep 17 00:00:00 2001 From: "michael.heerklotz" Date: Fri, 23 Aug 2019 18:49:11 +0200 Subject: [PATCH 010/115] Unlink userId and visitorId logic --- PiwikTracker.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index c0c824b..6c54f71 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -363,7 +363,6 @@ public function clearCustomTrackingParameters() public function setNewVisitorId() { $this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID); - $this->userId = false; $this->forcedVisitorId = false; $this->cookieVisitorId = false; return $this; @@ -1169,10 +1168,6 @@ public function setIp($ip) */ public function setUserId($userId) { - if ($userId === false) { - $this->setNewVisitorId(); - return $this; - } if ($userId === '') { throw new Exception("User ID cannot be empty."); } @@ -1195,15 +1190,12 @@ public static function getUserIdHashed($id) /** * Forces the requests to be recorded for the specified Visitor ID. - * Note: it is recommended to use ->setUserId($userId); instead. * * Rather than letting Piwik 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 you use both setVisitorId and setUserId, setUserId will take precedence. * If not set, the visitor ID will be fetched from the 1st party cookie, or will be set to a random UUID. * - * @deprecated We recommend to use ->setUserId($userId). * @param string $visitorId 16 hexadecimal characters visitor ID, eg. "33c31e01394bdc63" * @return $this * @throws Exception @@ -1240,9 +1232,6 @@ public function setVisitorId($visitorId) */ public function getVisitorId() { - if (!empty($this->userId)) { - return $this->getUserIdHashed($this->userId); - } if (!empty($this->forcedVisitorId)) { return $this->forcedVisitorId; } From 60796a7bebe5e3847ac96a1040a3c25d50e98f85 Mon Sep 17 00:00:00 2001 From: Lukas Winkler Date: Wed, 28 Aug 2019 18:11:15 +0200 Subject: [PATCH 011/115] remove deprecated array syntax for PHP 7.4 compatibility --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 897d3fa..30ba6d4 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -516,7 +516,7 @@ protected static function domainFixup($domain) if (strlen($domain) > 0) { $dl = strlen($domain) - 1; // remove trailing '.' - if ($domain{$dl} === '.') { + if ($domain[$dl] === '.') { $domain = substr($domain, 0, $dl); } // remove leading '*' From e2c9dd5f8cd08bfb5b944daa1857ca044073758d Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 24 Sep 2019 15:32:03 +1200 Subject: [PATCH 012/115] Fix wrong tracking url generated if tracker API endpoint already contains query search --- PiwikTracker.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 30ba6d4..6592f0f 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1678,7 +1678,13 @@ protected function getRequest($idSite) $customFields = '&' . http_build_query($this->customParameters, '', '&'); } - $url = $this->getBaseUrl() . + $baseUrl = $this->getBaseUrl(); + $start = '?'; + if (strpos($baseUrl, '?') !== false) { + $start = '&'; + } + + $url = $baseUrl . $start . '?idsite=' . $idSite . '&rec=1' . '&apiv=' . self::VERSION . From 7bef8157722c2c4dbf6a21f3832a2fbdfca2a9ec Mon Sep 17 00:00:00 2001 From: Tobias Etzold Date: Tue, 22 Oct 2019 10:30:57 +0200 Subject: [PATCH 013/115] Method addEcommerceItem() returns object instance --- PiwikTracker.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PiwikTracker.php b/PiwikTracker.php index 30ba6d4..74bd19f 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -680,6 +680,7 @@ public function doTrackAction($actionUrl, $actionType) * @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($sku, $name = '', $category = '', $price = 0.0, $quantity = 1) { @@ -690,6 +691,7 @@ public function addEcommerceItem($sku, $name = '', $category = '', $price = 0.0, $price = $this->forceDotAsSeparatorForDecimalPoint($price); $this->ecommerceItems[] = array($sku, $name, $category, $price, $quantity); + return $this; } /** From c3f760005047e925d92e2fb0d91ee2e1969f99f5 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Thu, 31 Oct 2019 16:52:32 +1300 Subject: [PATCH 014/115] Update PiwikTracker.php --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 6592f0f..574fc55 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1685,7 +1685,7 @@ protected function getRequest($idSite) } $url = $baseUrl . $start . - '?idsite=' . $idSite . + 'idsite=' . $idSite . '&rec=1' . '&apiv=' . self::VERSION . '&r=' . substr(strval(mt_rand()), 2, 6) . From 00c43be22fa4436009ca9b9dc0e445496328a17e Mon Sep 17 00:00:00 2001 From: diosmosis Date: Tue, 5 Nov 2019 22:55:41 -0800 Subject: [PATCH 015/115] Wrong header is enabled only for GET. --- PiwikTracker.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 808dfc8..ddd93f8 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1539,13 +1539,14 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal CURLOPT_USERAGENT => $this->userAgent, CURLOPT_HEADER => true, CURLOPT_TIMEOUT => $this->requestTimeout, + CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array( 'Accept-Language: ' . $this->acceptLanguage, ), ); if ($method === 'GET') { - $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_FOLLOWLOCATION] = true; } if (defined('PATH_TO_CERTIFICATES_FILE')) { From 8d61bc07e40603f0ad2dc8c718efd0a3663b19f5 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Wed, 18 Dec 2019 16:25:49 +1300 Subject: [PATCH 016/115] Prevent notice script_name not defined > ( ! ) Notice: Undefined index: SCRIPT_NAME in endor/piwik/piwik-php-tracker/PiwikTracker.php on line 1815 --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index ddd93f8..9124351 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1811,7 +1811,7 @@ protected static function getCurrentScriptName() } } } - if (empty($url)) { + if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) { $url = $_SERVER['SCRIPT_NAME']; } From 10828556f7ffbbe2abe1f71cd341eaf65702f74e Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Wed, 18 Dec 2019 16:27:25 +1300 Subject: [PATCH 017/115] Update PiwikTracker.php --- PiwikTracker.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PiwikTracker.php b/PiwikTracker.php index 9124351..a0ff7d3 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1813,6 +1813,8 @@ protected static function getCurrentScriptName() } if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) { $url = $_SERVER['SCRIPT_NAME']; + } elseif (empty($url)) { + $url = '/'; } if ($url[0] !== '/') { From cabcdc4bb28fda9bc75f71c12e279d963c7a768a Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Sun, 22 Dec 2019 09:15:27 +1300 Subject: [PATCH 018/115] Update PiwikTracker.php --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index a0ff7d3..a4ab871 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1817,7 +1817,7 @@ protected static function getCurrentScriptName() $url = '/'; } - if ($url[0] !== '/') { + if (!empty($url) && $url[0] !== '/') { $url = '/' . $url; } From 64497dc5c6bc11be8cdd57ed5b6a11cf249bbb5e Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 9 Dec 2019 18:01:07 +0100 Subject: [PATCH 019/115] Rebrand from Piwik to Matomo --- LICENSE | 2 +- PiwikTracker.php => MatomoTracker.php | 130 +++++++++++++------------- README.md | 4 +- composer.json | 20 ++-- 4 files changed, 77 insertions(+), 79 deletions(-) rename PiwikTracker.php => MatomoTracker.php (93%) diff --git a/LICENSE b/LICENSE index f7cc808..6efca76 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014, Piwik Open Source Analytics +Copyright (c) 2014, Matomo Open Source Analytics All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/PiwikTracker.php b/MatomoTracker.php similarity index 93% rename from PiwikTracker.php rename to MatomoTracker.php index a4ab871..f6903b8 100644 --- a/PiwikTracker.php +++ b/MatomoTracker.php @@ -1,30 +1,30 @@ CustomVariableName, 1 => CustomVariableValue ) or false - * @see Piwik.js getCustomVariable() + * @see matomo.js getCustomVariable() */ public function getCustomVariable($id, $scope = 'visit') { @@ -334,7 +334,7 @@ public function clearCustomVariables() /** * 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 Piwik itself. Please note that custom parameters are cleared after each + * 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 'dimension1' @@ -406,7 +406,7 @@ public function setUserAgent($userAgent) } /** - * Sets the country of the visitor. If not used, Piwik will try to find the country + * 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(). @@ -420,7 +420,7 @@ public function setCountry($country) } /** - * Sets the region of the visitor. If not used, Piwik may try to find the region + * 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(). @@ -434,7 +434,7 @@ public function setRegion($region) } /** - * Sets the city of the visitor. If not used, Piwik may try to find the city + * 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(). @@ -448,7 +448,7 @@ public function setCity($city) } /** - * Sets the latitude of the visitor. If not used, Piwik may try to find the visitor's + * 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(). @@ -462,7 +462,7 @@ public function setLatitude($lat) } /** - * Sets the longitude of the visitor. If not used, Piwik may try to find the visitor's + * 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(). @@ -500,7 +500,7 @@ public function enableCookies($domain = '', $path = '/') } /** - * If image response is disabled Piwik will respond with a HTTP 204 header instead of responding with a gif. + * If image response is disabled Matomo will respond with a HTTP 204 header instead of responding with a gif. */ public function disableSendImageResponse() { @@ -534,7 +534,7 @@ protected static function domainFixup($domain) */ protected function getCookieName($cookieName) { - // NOTE: If the cookie name is changed, we must also update the method in piwik.js with the same name. + // 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 @@ -746,12 +746,12 @@ public function doBulkTrack() * 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 Piwik reports. + * 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 Piwik. + * 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 @@ -799,7 +799,7 @@ public function doPing() * * 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 Piwik to report on Product & Categories + * 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 @@ -841,7 +841,7 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = } /** - * Force the separator for decimal point to be a dot. See https://github.com/piwik/piwik/issues/6435 + * 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 @@ -942,8 +942,8 @@ protected function getUrlTrackEcommerce($grandTotal, $subTotal = 0.0, $tax = 0.0 * Builds URL to track a page view. * * @see doTrackPageView() - * @param string $documentTitle Page view name as it will appear in Piwik reports - * @return string URL to piwik.php with all parameters set to track the pageview + * @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($documentTitle = '') { @@ -963,7 +963,7 @@ public function getUrlTrackPageView($documentTitle = '') * @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 piwik.php with all parameters set to track the pageview + * @return string URL to matomo.php with all parameters set to track the pageview * @throws */ public function getUrlTrackEvent($category, $action, $name = false, $value = false) @@ -998,7 +998,7 @@ public function getUrlTrackEvent($category, $action, $name = false, $value = fal * @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 piwik.php with all parameters set to track the pageview + * @return string URL to matomo.php with all parameters set to track the pageview */ public function getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget) { @@ -1029,7 +1029,7 @@ public function getUrlTrackContentImpression($contentName, $contentPiece, $conte * @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 piwik.php with all parameters set to track the pageview + * @return string URL to matomo.php with all parameters set to track the pageview */ public function getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget) { @@ -1085,7 +1085,7 @@ public function getUrlTrackSiteSearch($keyword, $category, $countResults) * @see doTrackGoal() * @param int $idGoal Id Goal to record a conversion * @param float $revenue Revenue for this conversion - * @return string URL to piwik.php with all parameters set to track the goal conversion + * @return string URL to matomo.php with all parameters set to track the goal conversion */ public function getUrlTrackGoal($idGoal, $revenue = 0.0) { @@ -1105,7 +1105,7 @@ public function getUrlTrackGoal($idGoal, $revenue = 0.0) * @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 piwik.php with all parameters set to track an action + * @return string URL to matomo.php with all parameters set to track an action */ public function getUrlTrackAction($actionUrl, $actionType) { @@ -1117,7 +1117,7 @@ public function getUrlTrackAction($actionUrl, $actionType) /** * Overrides server date and time for the tracking requests. - * By default Piwik will track requests for the "current datetime" but this function allows you + * 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() @@ -1133,9 +1133,9 @@ public function setForceVisitDateTime($dateTime) } /** - * Forces Piwik to create a new visit for the tracking request. + * Forces Matomo to create a new visit for the tracking request. * - * By default, Piwik will create a new visit if the last request by this user was more than 30 minutes ago. + * 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 */ @@ -1178,7 +1178,7 @@ public function setUserId($userId) } /** - * Hash function used internally by Piwik to hash a User ID into the Visitor ID. + * Hash function used internally by Matomo to hash a User ID into the Visitor ID. * * Note: matches implementation of Tracker\Request->getUserIdHashed() * @@ -1193,7 +1193,7 @@ public static function getUserIdHashed($id) /** * Forces the requests to be recorded for the specified Visitor ID. * - * Rather than letting Piwik attribute the user with a heuristic based on IP and other user fingeprinting attributes, + * 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. @@ -1221,7 +1221,7 @@ public function setVisitorId($visitorId) } /** - * If the user initiating the request has the Piwik first party cookie, + * 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 @@ -1321,7 +1321,7 @@ public function deleteCookies() * * @return string JSON Encoded string containing the Referrer information for Goal conversion attribution. * Will return false if the cookie could not be found - * @see Piwik.js getAttributionInfo() + * @see matomo.js getAttributionInfo() */ public function getAttributionInfo() { @@ -1380,7 +1380,7 @@ public function setResolution($width, $height) /** * Sets if the browser supports cookies - * This is reported in "List of plugins" report in Piwik. + * This is reported in "List of plugins" report in Matomo. * * @param bool $bool * @return $this @@ -1442,7 +1442,7 @@ public function setPlugins( } /** - * By default, PiwikTracker will read first party cookies + * 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. */ @@ -1453,7 +1453,7 @@ public function disableCookieSupport() /** * Returns the maximum number of seconds the tracker will spend waiting for a response - * from Piwik. Defaults to 600 seconds. + * from Matomo. Defaults to 600 seconds. */ public function getRequestTimeout() { @@ -1462,7 +1462,7 @@ public function getRequestTimeout() /** * Sets the maximum number of seconds that the tracker will spend waiting for a response - * from Piwik. + * from Matomo. * * @param int $timeout * @return $this @@ -1479,7 +1479,7 @@ public function setRequestTimeout($timeout) } /** - * If a proxy is needed to look up the address of the Piwik site, set it with 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" * @param int $proxyPort */ @@ -1637,22 +1637,20 @@ protected function getTimestamp() } /** - * Returns the base URL for the piwik server. + * Returns the base URL for the Matomo server. */ protected function getBaseUrl() { if (empty(self::$URL)) { throw new Exception( - 'You must first set the Piwik Tracker URL by calling - PiwikTracker::$URL = \'http://your-website.org/piwik/\';' + 'You must first set the Matomo Tracker URL by calling + MatomoTracker::$URL = \'http://your-website.org/matomo/\';' ); } - if (strpos(self::$URL, '/piwik.php') === false - && strpos(self::$URL, '/proxy-piwik.php') === false - && strpos(self::$URL, '/matomo.php') === false + if (strpos(self::$URL, '/matomo.php') === false && strpos(self::$URL, '/proxy-matomo.php') === false ) { - self::$URL .= '/piwik.php'; + self::$URL .= '/matomo.php'; } return self::$URL; @@ -1778,7 +1776,7 @@ protected function getCookieMatchingName($name) } $name = $this->getCookieName($name); - // Piwik cookie names use dots separators in piwik.js, + // 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) { @@ -1892,7 +1890,7 @@ protected static function getCurrentUrl() } /** - * Sets the first party cookies as would the piwik.js + * Sets the first party cookies as would the matomo.js * All cookies are supported: 'id' and 'ses' and 'ref' and 'cvar' cookies. * @return $this */ @@ -1929,7 +1927,7 @@ protected function setFirstPartyCookies() /** * Sets a first party cookie to the client to improve dual JS-PHP tracking. * - * This replicates the piwik.js tracker algorithms for consistency and better accuracy. + * This replicates the matomo.js tracker algorithms for consistency and better accuracy. * * @param $cookieName * @param $cookieValue @@ -2031,9 +2029,9 @@ protected function parseIncomingCookies($headers) * @param string $documentTitle * @return string */ -function Piwik_getUrlTrackPageView($idSite, $documentTitle = '') +function Matomo_getUrlTrackPageView($idSite, $documentTitle = '') { - $tracker = new PiwikTracker($idSite); + $tracker = new MatomoTracker($idSite); return $tracker->getUrlTrackPageView($documentTitle); } @@ -2046,9 +2044,9 @@ function Piwik_getUrlTrackPageView($idSite, $documentTitle = '') * @param float $revenue * @return string */ -function Piwik_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) +function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) { - $tracker = new PiwikTracker($idSite); + $tracker = new MatomoTracker($idSite); return $tracker->getUrlTrackGoal($idGoal, $revenue); } diff --git a/README.md b/README.md index d458bc4..659a0f9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # PHP Client for Matomo Analytics Tracking API -The PHP Tracker Client provides all features of the [Matomo Javascript Tracker](http://developer.matomo.org/api-reference/tracking-javascript), such as Ecommerce Tracking, Custom Variable, Event tracking and more. +The PHP Tracker Client provides all features of the [Matomo Javascript Tracker](https://developer.matomo.org/api-reference/tracking-javascript), such as Ecommerce Tracking, Custom Variable, Event tracking and more. ## Documentation and examples -Check out our [Matomo-PHP-Tracker developer documentation](http://developer.matomo.org/api-reference/PHP-Piwik-Tracker) and [Matomo Tracking API guide](http://matomo.org/docs/tracking-api/). +Check out our [Matomo-PHP-Tracker developer documentation](https://developer.matomo.org/api-reference/PHP-Piwik-Tracker) and [Matomo Tracking API guide](https://matomo.org/docs/tracking-api/). ## Requirements: * json extension (json_decode, json_encode) diff --git a/composer.json b/composer.json index 4b7babd..7700418 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,20 @@ { - "name": "piwik/piwik-php-tracker", - "description": "PHP Client for Piwik Analytics Tracking API", - "keywords": ["piwik","tracker","analytics"], - "homepage": "http://piwik.org", + "name": "matomo/matomo-php-tracker", + "description": "PHP Client for Matomo Analytics Tracking API", + "keywords": ["matomo","piwik","tracker","analytics"], + "homepage": "https://matomo.org", "license": "BSD-2-Clause", "authors": [ { - "name": "The Piwik Team", - "email": "hello@piwik.org", - "homepage": "http://piwik.org/the-piwik-team/" + "name": "The Matomo Team", + "email": "hello@matomo.org", + "homepage": "https://matomo.org/team/" } ], "support": { - "forum": "http://forum.piwik.org/", - "issues": "https://github.com/piwik/piwik-php-tracker/issues", - "source": "https://github.com/piwik/piwik-php-tracker" + "forum": "https://forum.matomo.org/", + "issues": "https://github.com/matomo-org/matomo-php-tracker/issues", + "source": "https://github.com/matomo-org/matomo-php-tracker" }, "autoload": { "classmap": ["."] From 7484c538a737bf9e6c18a64dc1300befdc2ade20 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 19 Dec 2019 15:42:57 +0100 Subject: [PATCH 020/115] keep bc --- MatomoTracker.php | 7 +++++++ PiwikTracker.php | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 PiwikTracker.php diff --git a/MatomoTracker.php b/MatomoTracker.php index f6903b8..976da22 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2050,3 +2050,10 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) return $tracker->getUrlTrackGoal($idGoal, $revenue); } + +/** + * For BC only + * + * @deprecated use MatomoTracker instead + */ +class PiwikTracker extends MatomoTracker {} \ No newline at end of file diff --git a/PiwikTracker.php b/PiwikTracker.php new file mode 100644 index 0000000..c6f2df3 --- /dev/null +++ b/PiwikTracker.php @@ -0,0 +1,48 @@ + Date: Thu, 2 Jan 2020 11:05:36 +0100 Subject: [PATCH 021/115] Remove duplicate class PiwikTracker --- MatomoTracker.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 976da22..f6903b8 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2050,10 +2050,3 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) return $tracker->getUrlTrackGoal($idGoal, $revenue); } - -/** - * For BC only - * - * @deprecated use MatomoTracker instead - */ -class PiwikTracker extends MatomoTracker {} \ No newline at end of file From 6ee26306a94340a5017d9421b05354c737088b3a Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Thu, 2 Jan 2020 11:09:45 +0100 Subject: [PATCH 022/115] remove duplicate class declaration fixes #54 --- MatomoTracker.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 976da22..f6903b8 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2050,10 +2050,3 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) return $tracker->getUrlTrackGoal($idGoal, $revenue); } - -/** - * For BC only - * - * @deprecated use MatomoTracker instead - */ -class PiwikTracker extends MatomoTracker {} \ No newline at end of file From 772490e99d39f4c77ddda399c56538cf211b68a1 Mon Sep 17 00:00:00 2001 From: peter279k Date: Mon, 13 Jan 2020 11:23:32 +0800 Subject: [PATCH 023/115] Add require block to define required extensions --- MatomoTracker.php | 24 ++++++++++++------------ PiwikTracker.php | 2 +- composer.json | 7 +++++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index f6903b8..f0a9906 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -158,7 +158,7 @@ public function __construct($idSite, $apiUrl = '') $this->sendImageResponse = true; $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); - + $this->outgoingTrackerCookies = array(); $this->incomingTrackerCookies = array(); } @@ -1122,7 +1122,7 @@ public function getUrlTrackAction($actionUrl, $actionType) * * 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. + * @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 */ @@ -1576,7 +1576,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $options[CURLOPT_COOKIE] = http_build_query($this->outgoingTrackerCookies); $this->outgoingTrackerCookies = array(); } - + $ch = curl_init(); curl_setopt_array($ch, $options); ob_start(); @@ -1587,7 +1587,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal if (!empty($response)) { list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2); } - + $this->parseIncomingCookies(explode("\r\n", $header)); } elseif (function_exists('stream_context_create')) { @@ -1596,7 +1596,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal 'method' => $method, 'user_agent' => $this->userAgent, 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n", - 'timeout' => $this->requestTimeout, // PHP 5.2.1 + 'timeout' => $this->requestTimeout, ), ); @@ -1614,11 +1614,11 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $stream_options['http']['header'] .= 'Cookie: ' . http_build_query($this->outgoingTrackerCookies) . "\r\n"; $this->outgoingTrackerCookies = array(); } - + $ctx = stream_context_create($stream_options); $response = file_get_contents($url, 0, $ctx); $content = $response; - + $this->parseIncomingCookies($http_response_header); } @@ -1977,7 +1977,7 @@ public function setOutgoingTrackerCookie($name, $value) $this->outgoingTrackerCookies[$name] = $value; } } - + /** * Gets a cookie which was set by the tracking server. * @@ -1990,7 +1990,7 @@ public function getIncomingTrackerCookie($name) if (isset($this->incomingTrackerCookies[$name])) { return $this->incomingTrackerCookies[$name]; } - + return false; } @@ -2002,11 +2002,11 @@ public function getIncomingTrackerCookie($name) protected function parseIncomingCookies($headers) { $this->incomingTrackerCookies = array(); - + if (!empty($headers)) { $headerName = 'set-cookie:'; $headerNameLength = strlen($headerName); - + foreach($headers as $header) { if (strpos(strtolower($header), $headerName) !== 0) { continue; @@ -2018,7 +2018,7 @@ protected function parseIncomingCookies($headers) } parse_str($cookies, $this->incomingTrackerCookies); } - } + } } } diff --git a/PiwikTracker.php b/PiwikTracker.php index c6f2df3..4f7b5c8 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -45,4 +45,4 @@ function Piwik_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) * * @deprecated use MatomoTracker instead */ -class PiwikTracker extends MatomoTracker {} \ No newline at end of file +class PiwikTracker extends MatomoTracker {} diff --git a/composer.json b/composer.json index 7700418..0ab400b 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,13 @@ "issues": "https://github.com/matomo-org/matomo-php-tracker/issues", "source": "https://github.com/matomo-org/matomo-php-tracker" }, + "require": { + "php": "^5.3", + "ext-json": "*" + }, + "suggest": { + "ext-curl": "Using this extension to issue the HTTPS request to Matomo" + }, "autoload": { "classmap": ["."] } From 0c73a8061da299cb258b96210af27d5efea1becc Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 13 Jan 2020 14:46:19 +0100 Subject: [PATCH 024/115] Ensure PiwikTracker and MatomoTracker are both available --- MatomoTracker.php | 9 +++++++++ PiwikTracker.php | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index f0a9906..ad6b4c3 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2050,3 +2050,12 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) return $tracker->getUrlTrackGoal($idGoal, $revenue); } + +/** + * Ensure PiwikTracker class is available as well + * + * @deprecated + */ +if (!class_exists('\PiwikTracker')) { + include_once('./PiwikTracker.php'); +} \ No newline at end of file diff --git a/PiwikTracker.php b/PiwikTracker.php index 4f7b5c8..16b50d9 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -11,7 +11,9 @@ * @package MatomoTracker */ -include_once('./MatomoTracker.php'); +if (!class_exists('\MatomoTracker')) { + include_once('./MatomoTracker.php'); +} /** * Helper function to quickly generate the URL to track a page view. From 9a4d993cb6b3bc75fdc3465cd96eb02823ef98f7 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Tue, 14 Jan 2020 20:04:53 +0100 Subject: [PATCH 025/115] Mark package as compatible with PHP 7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0ab400b..b985ae5 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^5.3", + "php": ">=5.3", "ext-json": "*" }, "suggest": { From a76fbb23db490843c52ca1c83da7717191970cec Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Thu, 20 Feb 2020 16:45:05 +1300 Subject: [PATCH 026/115] Add documentation fix https://github.com/matomo-org/matomo-php-tracker/issues/59 --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 659a0f9..45d7858 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,49 @@ The PHP Tracker Client provides all features of the [Matomo Javascript Tracker]( ## Documentation and examples Check out our [Matomo-PHP-Tracker developer documentation](https://developer.matomo.org/api-reference/PHP-Piwik-Tracker) and [Matomo Tracking API guide](https://matomo.org/docs/tracking-api/). + +```php +// Required variables +$matomoSiteId = 6; // Site ID +$matomoUrl = "https://example.tld"; // Your matomo URL +$matomoToken = ""; // Your authentication token + +// Optional variable +$matomoPageTitle = ""; // The title of the page + +// Load object +require_once("MatomoTracker.php"); + +// Matomo object +$matomoTracker = new MatomoTracker((int)$matomoSiteId, $matomoUrl); + +// Set authentication token +$matomoTracker->setTokenAuth($matomoToken); + +// Track page view +$matomoTracker->doTrackPageView($matomoPageTitle); +``` + ## Requirements: * json extension (json_decode, json_encode) * CURL or STREAM extensions (to issue the HTTPS request to Matomo) +## Installation + +### Composer + +``` +composer require matomo/matomo-php-tracker +``` + +### Manually + +Alternatively, you can download the files and require the Matomo tracker manually: + +``` +require_once("MatomoTracker.php"); +``` + ## License Released under the [BSD License](http://www.opensource.org/licenses/bsd-license.php) From 3a5440cc660b2ee6932a8aabe22d7c87521ff9f0 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Mon, 16 Mar 2020 13:16:06 +1300 Subject: [PATCH 027/115] Prevent double slashes Otherwise might generate a URL like https://example.com//matomo.php --- MatomoTracker.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ad6b4c3..c4d0d84 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1650,6 +1650,7 @@ protected function getBaseUrl() if (strpos(self::$URL, '/matomo.php') === false && strpos(self::$URL, '/proxy-matomo.php') === false ) { + self::$URL = rtrim(self::$URL, '/'); self::$URL .= '/matomo.php'; } @@ -2058,4 +2059,4 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) */ if (!class_exists('\PiwikTracker')) { include_once('./PiwikTracker.php'); -} \ No newline at end of file +} From fed1832947d2cc5dbfacdf6feafc51a4626a2bb5 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Fri, 27 Mar 2020 08:52:39 +0100 Subject: [PATCH 028/115] Adds method to set page performance metrics --- MatomoTracker.php | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index c4d0d84..0b05e44 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -94,6 +94,11 @@ public function __construct($idSite, $apiUrl = '') $this->forcedDatetime = false; $this->forcedNewVisit = false; $this->generationTime = false; + $this->transferTime = false; + $this->onLoadTime = false; + $this->latencyTime = false; + $this->domProcessingTime = false; + $this->domCompletionTime = false; $this->pageCustomVar = false; $this->customParameters = array(); $this->customData = false; @@ -214,6 +219,38 @@ public function setGenerationTime($timeMs) return $this; } + /** + * Sets timings for various performance metrics. + * + * @param null|int $latency + * @param null|int $transfer + * @param null|int $domProcessing + * @param null|int $domCompletion + * @param null|int $onload + * @return $this + */ + public function setPerformanceTimings($latency = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null) + { + $this->latencyTime = $latency; + $this->transferTime = $transfer; + $this->domProcessingTime = $domProcessing; + $this->domCompletionTime = $domCompletion; + $this->onLoadTime = $onload; + return $this; + } + + /** + * Clear / reset all previously set performance metrics. + */ + public function clearPerformanceTimings() + { + $this->latencyTime = false; + $this->transferTime = false; + $this->domProcessingTime = false; + $this->domCompletionTime = false; + $this->onLoadTime = false; + } + /** * @deprecated * @ignore @@ -1525,6 +1562,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal // Clear custom variables so they don't get copied over to other users in the bulk request $this->clearCustomVariables(); $this->clearCustomTrackingParameters(); + $this->clearPerformanceTimings(); $this->userAgent = false; $this->acceptLanguage = false; @@ -1714,6 +1752,11 @@ protected function getRequest($idSite) (!empty($this->pageCustomVar) ? '&cvar=' . urlencode(json_encode($this->pageCustomVar)) : '') . (!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode(json_encode($this->eventCustomVar)) : '') . (!empty($this->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . + (!empty($this->latencyTime) ? '&pf_lat=' . ((int)$this->latencyTime) : '') . + (!empty($this->transferTime) ? '&pf_tfr=' . ((int)$this->transferTime) : '') . + (!empty($this->domProcessingTime) ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') . + (!empty($this->domCompletionTime) ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') . + (!empty($this->onLoadTime) ? '&pf_onl=' . ((int)$this->onLoadTime) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters From c1365619e5eb4800b4f81a76774684f4672fda41 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 2 Apr 2020 08:28:00 +0200 Subject: [PATCH 029/115] improve docs --- MatomoTracker.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 0b05e44..201fceb 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -220,13 +220,14 @@ public function setGenerationTime($timeMs) } /** - * Sets timings for various performance metrics. - * - * @param null|int $latency - * @param null|int $transfer - * @param null|int $domProcessing - * @param null|int $domCompletion - * @param null|int $onload + * Sets timings for various browser performance metrics. + * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming + * + * @param null|int $latency Latency time in ms (responseStart – fetchStart) + * @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($latency = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null) From 9794438b0c1754060cc15048a030c9d39222b615 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 6 Apr 2020 09:37:16 +0200 Subject: [PATCH 030/115] send performance timings once a pageview was tracked --- MatomoTracker.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 201fceb..da9f2e5 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -212,6 +212,9 @@ public function setUrlReferrer($url) * * @param int $timeMs Generation time in ms * @return $this + * + * @deprecated this metric is deprecated please use performance timings instead + * @see setPerformanceTimings */ public function setGenerationTime($timeMs) { @@ -1563,7 +1566,6 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal // Clear custom variables so they don't get copied over to other users in the bulk request $this->clearCustomVariables(); $this->clearCustomTrackingParameters(); - $this->clearPerformanceTimings(); $this->userAgent = false; $this->acceptLanguage = false; @@ -1753,11 +1755,6 @@ protected function getRequest($idSite) (!empty($this->pageCustomVar) ? '&cvar=' . urlencode(json_encode($this->pageCustomVar)) : '') . (!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode(json_encode($this->eventCustomVar)) : '') . (!empty($this->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . - (!empty($this->latencyTime) ? '&pf_lat=' . ((int)$this->latencyTime) : '') . - (!empty($this->transferTime) ? '&pf_tfr=' . ((int)$this->transferTime) : '') . - (!empty($this->domProcessingTime) ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') . - (!empty($this->domCompletionTime) ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') . - (!empty($this->onLoadTime) ? '&pf_onl=' . ((int)$this->onLoadTime) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters @@ -1791,6 +1788,15 @@ protected function getRequest($idSite) // DEBUG $this->DEBUG_APPEND_URL; + if (!empty($this->idPageview)) { + $url .= + (!empty($this->latencyTime) ? '&pf_lat=' . ((int)$this->latencyTime) : '') . + (!empty($this->transferTime) ? '&pf_tfr=' . ((int)$this->transferTime) : '') . + (!empty($this->domProcessingTime) ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') . + (!empty($this->domCompletionTime) ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') . + (!empty($this->onLoadTime) ? '&pf_onl=' . ((int)$this->onLoadTime) : ''); + $this->clearPerformanceTimings(); + } // Reset page level custom variables after this page view $this->pageCustomVar = array(); From 6013814c09e16c0370cac65fa62765f3eafa2399 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 9 Apr 2020 09:27:36 +0200 Subject: [PATCH 031/115] split latency into network and server time --- MatomoTracker.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index da9f2e5..14e8130 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -94,11 +94,12 @@ public function __construct($idSite, $apiUrl = '') $this->forcedDatetime = false; $this->forcedNewVisit = false; $this->generationTime = false; + $this->networkTime = false; + $this->serverTime = false; $this->transferTime = false; - $this->onLoadTime = false; - $this->latencyTime = false; $this->domProcessingTime = false; $this->domCompletionTime = false; + $this->onLoadTime = false; $this->pageCustomVar = false; $this->customParameters = array(); $this->customData = false; @@ -226,16 +227,18 @@ public function setGenerationTime($timeMs) * Sets timings for various browser performance metrics. * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming * - * @param null|int $latency Latency time in ms (responseStart – fetchStart) + * @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($latency = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null) + public function setPerformanceTimings($network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null) { - $this->latencyTime = $latency; + $this->networkTime = $network; + $this->serverTime = $server; $this->transferTime = $transfer; $this->domProcessingTime = $domProcessing; $this->domCompletionTime = $domCompletion; @@ -248,7 +251,8 @@ public function setPerformanceTimings($latency = null, $transfer = null, $domPro */ public function clearPerformanceTimings() { - $this->latencyTime = false; + $this->networkTime = false; + $this->serverTime = false; $this->transferTime = false; $this->domProcessingTime = false; $this->domCompletionTime = false; @@ -1790,7 +1794,8 @@ protected function getRequest($idSite) if (!empty($this->idPageview)) { $url .= - (!empty($this->latencyTime) ? '&pf_lat=' . ((int)$this->latencyTime) : '') . + (!empty($this->networkTime) ? '&pf_net=' . ((int)$this->networkTime) : '') . + (!empty($this->serverTime) ? '&pf_srv=' . ((int)$this->serverTime) : '') . (!empty($this->transferTime) ? '&pf_tfr=' . ((int)$this->transferTime) : '') . (!empty($this->domProcessingTime) ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') . (!empty($this->domCompletionTime) ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') . From 88b2135f94a6cb044d6eb136f22d62f7ad46f243 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 9 Apr 2020 09:49:35 +0200 Subject: [PATCH 032/115] Ensure also zero values are sent --- MatomoTracker.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 14e8130..250b04b 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1794,12 +1794,12 @@ protected function getRequest($idSite) if (!empty($this->idPageview)) { $url .= - (!empty($this->networkTime) ? '&pf_net=' . ((int)$this->networkTime) : '') . - (!empty($this->serverTime) ? '&pf_srv=' . ((int)$this->serverTime) : '') . - (!empty($this->transferTime) ? '&pf_tfr=' . ((int)$this->transferTime) : '') . - (!empty($this->domProcessingTime) ? '&pf_dm1=' . ((int)$this->domProcessingTime) : '') . - (!empty($this->domCompletionTime) ? '&pf_dm2=' . ((int)$this->domCompletionTime) : '') . - (!empty($this->onLoadTime) ? '&pf_onl=' . ((int)$this->onLoadTime) : ''); + ($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(); } From 9f736cc84d6ae366b86e77ac530dad88ea8ed5f9 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Wed, 15 Apr 2020 14:01:51 +0200 Subject: [PATCH 033/115] remove possibility to set generation time --- MatomoTracker.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 250b04b..d91306a 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -93,7 +93,6 @@ public function __construct($idSite, $apiUrl = '') $this->eventCustomVar = false; $this->forcedDatetime = false; $this->forcedNewVisit = false; - $this->generationTime = false; $this->networkTime = false; $this->serverTime = false; $this->transferTime = false; @@ -219,7 +218,6 @@ public function setUrlReferrer($url) */ public function setGenerationTime($timeMs) { - $this->generationTime = $timeMs; return $this; } @@ -1758,7 +1756,6 @@ protected function getRequest($idSite) (!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->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters From 9efefbfd7c75c15e5edaec4fecfd32078881546d Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Fri, 17 Apr 2020 09:14:27 +0200 Subject: [PATCH 034/115] Revert "Adds method to set page performance metrics" --- MatomoTracker.php | 58 +++-------------------------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index d91306a..c4d0d84 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -93,12 +93,7 @@ public function __construct($idSite, $apiUrl = '') $this->eventCustomVar = false; $this->forcedDatetime = false; $this->forcedNewVisit = false; - $this->networkTime = false; - $this->serverTime = false; - $this->transferTime = false; - $this->domProcessingTime = false; - $this->domCompletionTime = false; - $this->onLoadTime = false; + $this->generationTime = false; $this->pageCustomVar = false; $this->customParameters = array(); $this->customData = false; @@ -212,51 +207,13 @@ public function setUrlReferrer($url) * * @param int $timeMs Generation time in ms * @return $this - * - * @deprecated this metric is deprecated please use performance timings instead - * @see setPerformanceTimings */ public function setGenerationTime($timeMs) { + $this->generationTime = $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($network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $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() - { - $this->networkTime = false; - $this->serverTime = false; - $this->transferTime = false; - $this->domProcessingTime = false; - $this->domCompletionTime = false; - $this->onLoadTime = false; - } - /** * @deprecated * @ignore @@ -1756,6 +1713,7 @@ protected function getRequest($idSite) (!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->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters @@ -1789,16 +1747,6 @@ protected function getRequest($idSite) // 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(); - } // Reset page level custom variables after this page view $this->pageCustomVar = array(); From fffe05a27a16de5162482f1791f63c487556e9cb Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Fri, 17 Apr 2020 09:17:42 +0200 Subject: [PATCH 035/115] Revert "Revert "Adds method to set page performance metrics"" --- MatomoTracker.php | 58 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index c4d0d84..d91306a 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -93,7 +93,12 @@ public function __construct($idSite, $apiUrl = '') $this->eventCustomVar = false; $this->forcedDatetime = false; $this->forcedNewVisit = false; - $this->generationTime = false; + $this->networkTime = false; + $this->serverTime = false; + $this->transferTime = false; + $this->domProcessingTime = false; + $this->domCompletionTime = false; + $this->onLoadTime = false; $this->pageCustomVar = false; $this->customParameters = array(); $this->customData = false; @@ -207,13 +212,51 @@ public function setUrlReferrer($url) * * @param int $timeMs Generation time in ms * @return $this + * + * @deprecated this metric is deprecated please use performance timings instead + * @see setPerformanceTimings */ public function setGenerationTime($timeMs) { - $this->generationTime = $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($network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $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() + { + $this->networkTime = false; + $this->serverTime = false; + $this->transferTime = false; + $this->domProcessingTime = false; + $this->domCompletionTime = false; + $this->onLoadTime = false; + } + /** * @deprecated * @ignore @@ -1713,7 +1756,6 @@ protected function getRequest($idSite) (!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->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters @@ -1747,6 +1789,16 @@ protected function getRequest($idSite) // 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(); + } // Reset page level custom variables after this page view $this->pageCustomVar = array(); From b4f4bc9636b85c099a50896958f7891d2047af8e Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Wed, 22 Apr 2020 09:45:28 +0200 Subject: [PATCH 036/115] cURL error should throw an exception to ease debug --- MatomoTracker.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index c4d0d84..8bbbb6a 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1584,6 +1584,11 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal ob_end_clean(); $header = ''; $content = ''; + + if ($response === false) { + throw new \RuntimeException(curl_error($ch)); + } + if (!empty($response)) { list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2); } From fa879b7260acf39649b3783848c5c8d24f8b08b4 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 23 Apr 2020 09:37:35 +0200 Subject: [PATCH 037/115] Adds a changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c808fbf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Matomo PHP Tracker Changelog + +This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. + + +## Matomo PHP Tracker 4.0.0 + +- Support for new page performance metrics (added in Matomo 4) has been added. You can use `setPerformanceTimings()` to set them for page views. +- Setting page generation time using `setGenerationTime()` has been discontinued. The method still exists to not break applications still using it, but it does not have any effect. Please use new page performance metrics as replacement. +- Sending requests using cURL will now throw an exception if an error occurs in a request \ No newline at end of file From 2dfb2034555edbd8ff1328fa323e7a0a57a1c6f5 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Fri, 15 May 2020 14:46:36 +0200 Subject: [PATCH 038/115] Remove Gears detection --- CHANGELOG.md | 3 ++- MatomoTracker.php | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c808fbf..1040a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,5 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or - Support for new page performance metrics (added in Matomo 4) has been added. You can use `setPerformanceTimings()` to set them for page views. - Setting page generation time using `setGenerationTime()` has been discontinued. The method still exists to not break applications still using it, but it does not have any effect. Please use new page performance metrics as replacement. -- Sending requests using cURL will now throw an exception if an error occurs in a request \ No newline at end of file +- Sending requests using cURL will now throw an exception if an error occurs in a request +- Matomo does not longer support tracking of these browser plugins: Gears \ No newline at end of file diff --git a/MatomoTracker.php b/MatomoTracker.php index 8b65ce4..0115082 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1455,7 +1455,6 @@ public function setDebugStringAppend($string) * @param bool $realPlayer * @param bool $pdf * @param bool $windowsMedia - * @param bool $gears * @param bool $silverlight * @return $this */ @@ -1467,7 +1466,6 @@ public function setPlugins( $realPlayer = false, $pdf = false, $windowsMedia = false, - $gears = false, $silverlight = false ) { @@ -1479,7 +1477,6 @@ public function setPlugins( '&realp=' . (int)$realPlayer . '&pdf=' . (int)$pdf . '&wma=' . (int)$windowsMedia . - '&gears=' . (int)$gears . '&ag=' . (int)$silverlight; return $this; } From 432631f883e4f313ce5f669f8787c0bd30e81b29 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Wed, 20 May 2020 09:04:05 +0200 Subject: [PATCH 039/115] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1040a0c..947b441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,5 +7,5 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or - Support for new page performance metrics (added in Matomo 4) has been added. You can use `setPerformanceTimings()` to set them for page views. - Setting page generation time using `setGenerationTime()` has been discontinued. The method still exists to not break applications still using it, but it does not have any effect. Please use new page performance metrics as replacement. -- Sending requests using cURL will now throw an exception if an error occurs in a request -- Matomo does not longer support tracking of these browser plugins: Gears \ No newline at end of file +- Sending requests using cURL will now throw an exception if an error occurs in a request. +- Matomo does not longer support tracking of these browser plugins: Gears. Therefor the signature of `setPlugins()` changed. From 4332ed618bd9a442a1a270cd707bb8b4ced0be3f Mon Sep 17 00:00:00 2001 From: sgiehl Date: Tue, 26 May 2020 09:39:32 +0200 Subject: [PATCH 040/115] removes director detection --- CHANGELOG.md | 2 +- MatomoTracker.php | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 947b441..7961754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,4 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or - Support for new page performance metrics (added in Matomo 4) has been added. You can use `setPerformanceTimings()` to set them for page views. - Setting page generation time using `setGenerationTime()` has been discontinued. The method still exists to not break applications still using it, but it does not have any effect. Please use new page performance metrics as replacement. - Sending requests using cURL will now throw an exception if an error occurs in a request. -- Matomo does not longer support tracking of these browser plugins: Gears. Therefor the signature of `setPlugins()` changed. +- Matomo does not longer support tracking of these browser plugins: Gears, Director. Therefor the signature of `setPlugins()` changed. diff --git a/MatomoTracker.php b/MatomoTracker.php index 0115082..e46da0e 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1450,7 +1450,6 @@ public function setDebugStringAppend($string) * * @param bool $flash * @param bool $java - * @param bool $director * @param bool $quickTime * @param bool $realPlayer * @param bool $pdf @@ -1461,7 +1460,6 @@ public function setDebugStringAppend($string) public function setPlugins( $flash = false, $java = false, - $director = false, $quickTime = false, $realPlayer = false, $pdf = false, @@ -1472,7 +1470,6 @@ public function setPlugins( $this->plugins = '&fla=' . (int)$flash . '&java=' . (int)$java . - '&dir=' . (int)$director . '&qt=' . (int)$quickTime . '&realp=' . (int)$realPlayer . '&pdf=' . (int)$pdf . From 036f4974eb1024047805cb34c0b999c44283db62 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Thu, 28 May 2020 11:29:11 +0200 Subject: [PATCH 041/115] Track ecommerce views not as custom variables --- MatomoTracker.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index e46da0e..c36a99b 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -62,14 +62,6 @@ class MatomoTracker */ const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; - /** - * Ecommerce item page view tracking stores item's metadata in these Custom Variables slots. - */ - const CVAR_INDEX_ECOMMERCE_ITEM_PRICE = 2; - const CVAR_INDEX_ECOMMERCE_ITEM_SKU = 3; - const CVAR_INDEX_ECOMMERCE_ITEM_NAME = 4; - const CVAR_INDEX_ECOMMERCE_ITEM_CATEGORY = 5; - /** * Defines how many categories can be used max when calling addEcommerceItem(). * @var int @@ -100,6 +92,7 @@ public function __construct($idSite, $apiUrl = '') $this->domCompletionTime = false; $this->onLoadTime = false; $this->pageCustomVar = false; + $this->ecommerceView = array(); $this->customParameters = array(); $this->customData = false; $this->hasCookies = false; @@ -837,8 +830,6 @@ public function doPing() * 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. - * It will set 3 custom variables of scope "page" with the SKU, Name and Category for this page view. - * Note: Custom Variables of scope "page" slots 3, 4 and 5 will be used. * * On a category page, you may set the parameter $category only and set the other parameters to false. * @@ -854,6 +845,8 @@ public function doPing() */ public function setEcommerceView($sku = '', $name = '', $category = '', $price = 0.0) { + $this->ecommerceView = []; + if (!empty($category)) { if (is_array($category)) { $category = json_encode($category); @@ -861,12 +854,12 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = } else { $category = ""; } - $this->pageCustomVar[self::CVAR_INDEX_ECOMMERCE_ITEM_CATEGORY] = array('_pkc', $category); + $this->ecommerceView['_pkc'] = $category; if (!empty($price)) { $price = (float)$price; $price = $this->forceDotAsSeparatorForDecimalPoint($price); - $this->pageCustomVar[self::CVAR_INDEX_ECOMMERCE_ITEM_PRICE] = array('_pkp', $price); + $this->ecommerceView['_pkp'] = $price; } // On a category page, do not record "Product name not defined" @@ -874,12 +867,12 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = return $this; } if (!empty($sku)) { - $this->pageCustomVar[self::CVAR_INDEX_ECOMMERCE_ITEM_SKU] = array('_pks', $sku); + $this->ecommerceView['_pks'] = $sku; } if (empty($name)) { $name = ""; } - $this->pageCustomVar[self::CVAR_INDEX_ECOMMERCE_ITEM_NAME] = array('_pkn', $name); + $this->ecommerceView['_pkn'] = $name; return $this; } @@ -1799,7 +1792,12 @@ protected function getRequest($idSite) $this->clearPerformanceTimings(); } + foreach ($this->ecommerceView as $param => $value) { + $url .= '&' . $param . '=' . urlencode($value); + } + // Reset page level custom variables after this page view + $this->ecommerceView = array(); $this->pageCustomVar = array(); $this->eventCustomVar = array(); $this->clearCustomTrackingParameters(); From 90b2cd168ca6a162a3f08abec6824aa8cb523902 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Fri, 29 May 2020 23:01:22 +0200 Subject: [PATCH 042/115] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7961754..014d86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or ## Matomo PHP Tracker 4.0.0 +Attention: This version of Matomo PHP Tracker is no longer compatible with Matomo 3.x or earlier + - Support for new page performance metrics (added in Matomo 4) has been added. You can use `setPerformanceTimings()` to set them for page views. - Setting page generation time using `setGenerationTime()` has been discontinued. The method still exists to not break applications still using it, but it does not have any effect. Please use new page performance metrics as replacement. - Sending requests using cURL will now throw an exception if an error occurs in a request. - Matomo does not longer support tracking of these browser plugins: Gears, Director. Therefor the signature of `setPlugins()` changed. +- Implementation of ecommerce views changed from custom variables to raw parameters From d84223580a4de2df3150880359891f6a27c9b5b8 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 16 Jun 2020 14:30:43 +1200 Subject: [PATCH 043/115] Fix include_once warning Failed opening './PiwikTracker.php' for inclusion Have been getting these warnings quite often and a user mentioned it recently as well. I reckon this should fix it > Warning: include_once(): Failed opening './PiwikTracker.php' for inclusion (include_path='/vendor/pear/pear_exception:/vendor/pear/console_getopt:/vendor/pear/pear-core-minimal/src:/vendor/pear/archive_tar:.:/usr/local/Cellar/php/7.4.6_1/share/php/pear') in /vendor/matomo/matomo-php-tracker/MatomoTracker.php on line 2110 --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index c4d0d84..74a77df 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2058,5 +2058,5 @@ function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) * @deprecated */ if (!class_exists('\PiwikTracker')) { - include_once('./PiwikTracker.php'); + include_once('PiwikTracker.php'); } From 68a2178d6d8610cc8a4b580584cca70d2e691f9e Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 16 Jun 2020 14:31:03 +1200 Subject: [PATCH 044/115] Update PiwikTracker.php --- PiwikTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PiwikTracker.php b/PiwikTracker.php index 16b50d9..ea5ce68 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -12,7 +12,7 @@ */ if (!class_exists('\MatomoTracker')) { - include_once('./MatomoTracker.php'); + include_once('MatomoTracker.php'); } /** From 3ee25323aa7079e5cafbc6a0c8d3a090c133a4b4 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Mon, 13 Jul 2020 00:33:52 +0200 Subject: [PATCH 045/115] function setRequestMethod added for POST --- MatomoTracker.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 74a77df..bccb444 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1477,6 +1477,19 @@ public function setRequestTimeout($timeout) $this->requestTimeout = $timeout; return $this; } + + /** + * Sets the request method (either GET or POST). POST is recommended when using + * setTokenAuth() to prevent the token from being recorded in server logs. + * + * @param string $method + * @return $this + */ + public function setRequestMethod($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 @@ -1532,6 +1545,8 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } $proxy = $this->getProxy(); + + $method = isset($this->requestMethod) ? $this->requestMethod : $method; if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( From b76389f58b38e346e9af3092dcfd124d1f40faca Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Mon, 13 Jul 2020 02:02:45 +0200 Subject: [PATCH 046/115] Update MatomoTracker.php added warning about redirects for setRequestMethod() --- MatomoTracker.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index bccb444..5e905ac 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1481,7 +1481,8 @@ public function setRequestTimeout($timeout) /** * Sets the request method (either GET or POST). POST is recommended when using * setTokenAuth() to prevent the token from being recorded in server logs. - * + * When using POST, to prevent loss of the POST values, avoid using redirects. + * * @param string $method * @return $this */ @@ -1546,7 +1547,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $proxy = $this->getProxy(); - $method = isset($this->requestMethod) ? $this->requestMethod : $method; + $method = isset($this->requestMethod) ? $this->requestMethod : $method; if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( From 5891331b932d62c159c2580b04aa64fc3592fb13 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Mon, 13 Jul 2020 02:14:17 +0200 Subject: [PATCH 047/115] indentation --- MatomoTracker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 5e905ac..f2d03dc 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1546,8 +1546,8 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } $proxy = $this->getProxy(); - - $method = isset($this->requestMethod) ? $this->requestMethod : $method; + + $method = isset($this->requestMethod) ? $this->requestMethod : $method; if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( From 81d6605f0f153fca54399ad736e8b6170072f0ee Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Mon, 13 Jul 2020 02:33:54 +0200 Subject: [PATCH 048/115] prevent method change for bulk requests prevent method from being set to GET while doing Bulk Requests --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index f2d03dc..bba73c1 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1547,7 +1547,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $proxy = $this->getProxy(); - $method = isset($this->requestMethod) ? $this->requestMethod : $method; + $method = isset($this->requestMethod) && !$this->doBulkRequests ? $this->requestMethod : $method; if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( From 4eac561711675d2421f2efa2223fcdef29563576 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Mon, 13 Jul 2020 17:05:12 +0200 Subject: [PATCH 049/115] reworked Method is now set to POST as bool, instead of a string, since it should remain defaulted to GET and there is no need to have the option to set it to GET. URL is spliced in URL and parameters which are now fed to the POST payload for both cURL and stream. --- MatomoTracker.php | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index bba73c1..092965d 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -161,6 +161,8 @@ public function __construct($idSite, $apiUrl = '') $this->outgoingTrackerCookies = array(); $this->incomingTrackerCookies = array(); + + $this->requestMethodPost = false; } /** @@ -1479,16 +1481,15 @@ public function setRequestTimeout($timeout) } /** - * Sets the request method (either GET or POST). POST is recommended when using - * setTokenAuth() to prevent the token from being recorded in server logs. - * When using POST, to prevent loss of the POST values, avoid using redirects. + * 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. * - * @param string $method * @return $this */ - public function setRequestMethod($method) + public function setRequestMethodPost() { - $this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET'; + $this->requestMethodPost = true; return $this; } @@ -1547,7 +1548,14 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $proxy = $this->getProxy(); - $method = isset($this->requestMethod) && !$this->doBulkRequests ? $this->requestMethod : $method; + if ($this->requestMethodPost && !$this->doBulkRequests) { + $url_parts = explode('?', $url); + + $url = $url_parts[0]; + $post_data = $url_parts[1]; + + $method = 'POST'; + } if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( @@ -1580,6 +1588,11 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal default: break; } + + if (isset($post_data)) { + $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded'; + $options[CURLOPT_POSTFIELDS] = $post_data; + } // only supports JSON data if (!empty($data)) { @@ -1619,6 +1632,11 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal if (isset($proxy)) { $stream_options['http']['proxy'] = $proxy; } + + if (isset($post_data)) { + $stream_options['http']['header'] .= 'Content-Type: application/x-www-form-urlencoded'; + $stream_options['http']['content'] = $post_data; + } // only supports JSON data if (!empty($data)) { From 597712cd6c2e8f994d10a28914c55545ecffa447 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:16:24 +0200 Subject: [PATCH 050/115] Casing, non-boolean method selection Snake case converted to camel case. Reverted back to the method selection of 'POST' (intended). Added a warning about Log Analytics. --- MatomoTracker.php | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 092965d..be2c801 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -161,8 +161,6 @@ public function __construct($idSite, $apiUrl = '') $this->outgoingTrackerCookies = array(); $this->incomingTrackerCookies = array(); - - $this->requestMethodPost = false; } /** @@ -1483,13 +1481,15 @@ public function setRequestTimeout($timeout) /** * 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 POST to prevent the loss of POST values. When using Log Analytics, + * be aware that POST requests are not parseable/replayable. * + * @param string $method * @return $this */ - public function setRequestMethodPost() + public function setRequestMethod($method) { - $this->requestMethodPost = true; + $this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET'; return $this; } @@ -1548,11 +1548,14 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $proxy = $this->getProxy(); - if ($this->requestMethodPost && !$this->doBulkRequests) { - $url_parts = explode('?', $url); + if (isset($this->requestMethod) + && $this->requestMethod === 'POST' + && !$this->doBulkRequests + ) { + $urlParts = explode('?', $url); - $url = $url_parts[0]; - $post_data = $url_parts[1]; + $url = $urlParts[0]; + $postData = $urlParts[1]; $method = 'POST'; } @@ -1589,9 +1592,9 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal break; } - if (isset($post_data)) { + if (isset($postData)) { $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded'; - $options[CURLOPT_POSTFIELDS] = $post_data; + $options[CURLOPT_POSTFIELDS] = $postData; } // only supports JSON data @@ -1633,9 +1636,9 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $stream_options['http']['proxy'] = $proxy; } - if (isset($post_data)) { + if (isset($postData)) { $stream_options['http']['header'] .= 'Content-Type: application/x-www-form-urlencoded'; - $stream_options['http']['content'] = $post_data; + $stream_options['http']['content'] = $postData; } // only supports JSON data From b7ad0c99db6ea4c953d56e9be8cfe576126c22ec Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Thu, 16 Jul 2020 10:40:43 +1200 Subject: [PATCH 051/115] Allow switching between request methods and post token by default --- MatomoTracker.php | 59 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 74a77df..6e67ff2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -78,6 +78,8 @@ class MatomoTracker const DEFAULT_COOKIE_PATH = '/'; + 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. @@ -1501,6 +1503,21 @@ private function getProxy() return null; } + /** + * 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 + * @return $this + */ + public function setRequestMethodForNonBulkRequests($requestMethod) + { + $this->requestMethod = $requestMethod; + return $this; + } + /** * Used in tests to output useful error messages. * @@ -1531,6 +1548,31 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal return true; } + $forcePostUrlEncoded = false; + if (!$this->doBulkRequests) { + if (strtoupper($this->requestMethod) === 'POST') { + $urlParts = explode('?', $url); + + $url = $urlParts[0]; + $data = $urlParts[1]; + $forcePostUrlEncoded = true; + + $method = 'POST'; + } + + if (!empty($this->token_auth)) { + if (empty($this->requestMethod) || $method === 'POST') { + $forcePostUrlEncoded = true; + if (empty($data)) { + $data = array(); + } + $data['token_auth'] = urlencode($this->token_auth); + } elseif (!empty($this->token_auth)) { + $url .= '&token_auth=' . urlencode($this->token_auth); + } + } + } + $proxy = $this->getProxy(); if (function_exists('curl_init') && function_exists('curl_exec')) { @@ -1566,7 +1608,15 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } // only supports JSON data - if (!empty($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')) { + $curl_op[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; @@ -1605,7 +1655,10 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } // only supports JSON data - if (!empty($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; } @@ -1691,8 +1744,6 @@ protected function getRequest($idSite) (!empty($this->userId) ? '&uid=' . urlencode($this->userId) : '') . (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') . (!empty($this->forcedNewVisit) ? '&new_visit=1' : '') . - ((!empty($this->token_auth) && !$this->doBulkRequests) ? - '&token_auth=' . urlencode($this->token_auth) : '') . // Values collected from cookie '&_idts=' . $this->createTs . From d525d373a4befd264eecc9d449332e23fbb3e0a7 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Thu, 16 Jul 2020 10:48:30 +1200 Subject: [PATCH 052/115] add docs --- MatomoTracker.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 6e67ff2..76ed8f2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1504,10 +1504,18 @@ private function getProxy() } /** - * 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. + * + * Customise the request method behaviour. + * This does not impact bulk requests which always POST data. + * All other requests should behave like this: + * 1. You can now force `POST` of all requests by setting `POST` as request method. When set, all parameters will + * be posted. This can be useful if there's a problem on the webserver re URL length or the requests are very long. + * 2. By default regular requests use HTTP "GET" unless a token auth is specified. + * When a token auth is specified, we use HTTP "POST" but we only POST the token, nothing else. This way log + * replay will still be possible. + * 3. If you are having for example redirect issues then you can set requestMethod to `GET` to force using HTTP "GET" + * requests. The token auth will then be sent also using a GET url parameter and the token will be visible in your + * server logs. * * @param string $method * @return $this From 5423ea28a93dfba339134c163fe769ad819232e2 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Thu, 16 Jul 2020 01:17:55 +0200 Subject: [PATCH 053/115] Update MatomoTracker.php --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index be2c801..4c75fe2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1487,7 +1487,7 @@ public function setRequestTimeout($timeout) * @param string $method * @return $this */ - public function setRequestMethod($method) + public function setRequestMethodNonBulk($method) { $this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET'; return $this; From 167fb7e258bc127ce560e4d1c7a34756990a3191 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Thu, 16 Jul 2020 02:20:11 +0200 Subject: [PATCH 054/115] Update MatomoTracker.php --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 4c75fe2..851fd0e 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1484,7 +1484,7 @@ public function setRequestTimeout($timeout) * 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 + * @param string $method Either 'POST' or 'GET' * @return $this */ public function setRequestMethodNonBulk($method) From f0973fbb28ff4153f3dbd311dfa42791a285da36 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Thu, 16 Jul 2020 13:01:55 +1200 Subject: [PATCH 055/115] merge master --- MatomoTracker.php | 48 +++-------------------------------------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 15525a3..e160ba7 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1518,29 +1518,6 @@ private function getProxy() return null; } - /** - * - * Customise the request method behaviour. - * This does not impact bulk requests which always POST data. - * All other requests should behave like this: - * 1. You can now force `POST` of all requests by setting `POST` as request method. When set, all parameters will - * be posted. This can be useful if there's a problem on the webserver re URL length or the requests are very long. - * 2. By default regular requests use HTTP "GET" unless a token auth is specified. - * When a token auth is specified, we use HTTP "POST" but we only POST the token, nothing else. This way log - * replay will still be possible. - * 3. If you are having for example redirect issues then you can set requestMethod to `GET` to force using HTTP "GET" - * requests. The token auth will then be sent also using a GET url parameter and the token will be visible in your - * server logs. - * - * @param string $method - * @return $this - */ - public function setRequestMethodForNonBulkRequests($requestMethod) - { - $this->requestMethod = $requestMethod; - return $this; - } - /** * Used in tests to output useful error messages. * @@ -1574,6 +1551,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $forcePostUrlEncoded = false; if (!$this->doBulkRequests) { if (strtoupper($this->requestMethod) === 'POST') { + // POST ALL parameters and have no GET parameters $urlParts = explode('?', $url); $url = $urlParts[0]; @@ -1585,12 +1563,14 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal if (!empty($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 = array(); } $data['token_auth'] = urlencode($this->token_auth); } elseif (!empty($this->token_auth)) { + // Use GET for all URL parameters $url .= '&token_auth=' . urlencode($this->token_auth); } } @@ -1598,18 +1578,6 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $proxy = $this->getProxy(); - if (isset($this->requestMethod) - && $this->requestMethod === 'POST' - && !$this->doBulkRequests - ) { - $urlParts = explode('?', $url); - - $url = $urlParts[0]; - $postData = $urlParts[1]; - - $method = 'POST'; - } - if (function_exists('curl_init') && function_exists('curl_exec')) { $options = array( CURLOPT_URL => $url, @@ -1641,11 +1609,6 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal default: break; } - - if (isset($postData)) { - $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded'; - $options[CURLOPT_POSTFIELDS] = $postData; - } // only supports JSON data if (!empty($data) && $forcePostUrlEncoded) { @@ -1693,11 +1656,6 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal if (isset($proxy)) { $stream_options['http']['proxy'] = $proxy; } - - if (isset($postData)) { - $stream_options['http']['header'] .= 'Content-Type: application/x-www-form-urlencoded'; - $stream_options['http']['content'] = $postData; - } // only supports JSON data if (!empty($data) && $forcePostUrlEncoded) { From 03cb3086ecbdf35863bf7e4b3140773b8e4457d4 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Mon, 20 Jul 2020 12:50:44 +1200 Subject: [PATCH 056/115] fix token and post was not set --- MatomoTracker.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index e160ba7..7b7da6e 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1562,16 +1562,19 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } 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 = array(); + $data = ''; } - $data['token_auth'] = urlencode($this->token_auth); + $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 .= '&token_auth=' . urlencode($this->token_auth); + $url .= $appendTokenString; } } } From b7badf2dc516cb6a552ef6fbc3530065ffb02df6 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Wed, 22 Jul 2020 16:23:25 +0200 Subject: [PATCH 057/115] Make it possible to configure cookie options for Secure, HTTPOnly and SameSite --- MatomoTracker.php | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index c4d0d84..3e4fafc 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -142,6 +142,9 @@ public function __construct($idSite, $apiUrl = '') $this->configCookiesDisabled = false; $this->configCookiePath = self::DEFAULT_COOKIE_PATH; $this->configCookieDomain = ''; + $this->configCookieSameSite = ''; + $this->configCookieSecure = false; + $this->configCookieHTTPOnly = false; $this->currentTs = time(); $this->createTs = $this->currentTs; @@ -491,12 +494,18 @@ public function enableBulkTracking() * @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($domain = '', $path = '/') + public function enableCookies($domain = '', $path = '/', $secure = false, $httpOnly = false, $sameSite = '') { $this->configCookiesDisabled = false; $this->configCookieDomain = self::domainFixup($domain); $this->configCookiePath = $path; + $this->configCookieSecure = $secure; + $this->configCookieHTTPOnly = $httpOnly; + $this->configCookieSameSite = $sameSite; } /** @@ -1939,13 +1948,15 @@ protected function setCookie($cookieName, $cookieValue, $cookieTTL) { $cookieExpire = $this->currentTs + $cookieTTL; if (!headers_sent()) { - setcookie( - $this->getCookieName($cookieName), - $cookieValue, - $cookieExpire, - $this->configCookiePath, - $this->configCookieDomain - ); + $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; } @@ -1998,7 +2009,7 @@ public function getIncomingTrackerCookie($name) /** * Reads incoming tracking server cookies. * - * @param $headers Array with HTTP response headers as values + * @param array $headers Array with HTTP response headers as values */ protected function parseIncomingCookies($headers) { From 79fc91978891d8bb2c5fb222d6a85f85b86614b6 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Sun, 26 Jul 2020 11:05:53 +0200 Subject: [PATCH 058/115] proper and consistent capitalisation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 45d7858..0340777 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PHP Client for Matomo Analytics Tracking API -The PHP Tracker Client provides all features of the [Matomo Javascript Tracker](https://developer.matomo.org/api-reference/tracking-javascript), such as Ecommerce Tracking, Custom Variable, Event tracking and more. +The PHP Tracker Client provides all features of the [Matomo Javascript Tracker](https://developer.matomo.org/api-reference/tracking-javascript), such as Ecommerce Tracking, Custom Variables, Event Tracking and more. ## Documentation and examples Check out our [Matomo-PHP-Tracker developer documentation](https://developer.matomo.org/api-reference/PHP-Piwik-Tracker) and [Matomo Tracking API guide](https://matomo.org/docs/tracking-api/). @@ -29,8 +29,8 @@ $matomoTracker->doTrackPageView($matomoPageTitle); ``` ## Requirements: -* json extension (json_decode, json_encode) -* CURL or STREAM extensions (to issue the HTTPS request to Matomo) +* JSON extension (json_decode, json_encode) +* cURL or stream extension (to issue the HTTPS request to Matomo) ## Installation From 54268f1987daa8da6cbc2dbf51acfe5ced3de037 Mon Sep 17 00:00:00 2001 From: Silver Shadow <36823114+silvershadowcc@users.noreply.github.com> Date: Sun, 26 Jul 2020 11:47:22 +0200 Subject: [PATCH 059/115] fixed typo fixed typo and passed test as such --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 7b7da6e..3286449 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1619,7 +1619,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $options[CURLOPT_POSTFIELDS] = $data; $options[CURLOPT_POST] = true; if (defined('CURL_REDIR_POST_ALL')) { - $curl_op[CURLOPT_POSTREDIR] = CURL_REDIR_POST_ALL; + $options[CURLOPT_POSTREDIR] = CURL_REDIR_POST_ALL; $options[CURLOPT_FOLLOWLOCATION] = true; } } elseif (!empty($data)) { From e523f4d7825a396e2d0e635e3dc53551418389e5 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 27 Jul 2020 11:58:43 +0200 Subject: [PATCH 060/115] Adds some basic methods to handle custom dimensions --- MatomoTracker.php | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index c36a99b..5d058c3 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -94,6 +94,7 @@ public function __construct($idSite, $apiUrl = '') $this->pageCustomVar = false; $this->ecommerceView = array(); $this->customParameters = array(); + $this->customDimensions = array(); $this->customData = false; $this->hasCookies = false; $this->token_auth = false; @@ -368,6 +369,38 @@ public function clearCustomVariables() $this->eventCustomVar = array(); } + /** + * Sets a specific custom dimension + * + * @param int $id id of custom dimension + * @param string $value value for custom dimension + * @return $this + */ + public function setCustomDimension($id, $value) + { + $this->customDimensions['dimension'.(int)$id] = $value; + return $this; + } + + /** + * Clears all previously set custom dimensions + */ + public function clearCustomDimensions() + { + $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($id) + { + return $this->customDimensions['dimension'.(int)$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 @@ -1552,8 +1585,9 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal . (!empty($this->userAgent) ? ('&ua=' . urlencode($this->userAgent)) : '') . (!empty($this->acceptLanguage) ? ('&lang=' . urlencode($this->acceptLanguage)) : ''); - // Clear custom variables so they don't get copied over to other users in the bulk request + // 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->acceptLanguage = false; @@ -1704,6 +1738,11 @@ protected function getRequest($idSite) $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) { @@ -1775,7 +1814,7 @@ protected function getRequest($idSite) (!empty($this->city) ? '&city=' . urlencode($this->city) : '') . (!empty($this->lat) ? '&lat=' . urlencode($this->lat) : '') . (!empty($this->long) ? '&long=' . urlencode($this->long) : '') . - $customFields . + $customFields . $customDimensions . (!$this->sendImageResponse ? '&send_image=0' : '') . // DEBUG @@ -1800,6 +1839,7 @@ protected function getRequest($idSite) $this->ecommerceView = array(); $this->pageCustomVar = array(); $this->eventCustomVar = array(); + $this->clearCustomDimensions(); $this->clearCustomTrackingParameters(); // force new visit only once, user must call again setForceNewVisit() From 94be4250d32333a17b24df316deb9d219f640627 Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 27 Jul 2020 16:20:47 +0200 Subject: [PATCH 061/115] avoid custom dimensions being set as custom tracking parameters --- MatomoTracker.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 5d058c3..2ed7112 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -406,13 +406,20 @@ public function getCustomDimension($id) * 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 'dimension1' + * @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($trackingApiParameter, $value) { + $matches = []; + + if (preg_match('/^dimension([0-9]+)$/', $trackingApiParameter, $matches)) { + $this->setCustomDimension($matches[1], $value); + return $this; + } + $this->customParameters[$trackingApiParameter] = $value; return $this; } From 9e6c40e1b517e40ed26bb9d701dbca7e332658ca Mon Sep 17 00:00:00 2001 From: diosmosis Date: Sun, 6 Sep 2020 16:57:53 -0700 Subject: [PATCH 062/115] Cookie now contains less values and certain tracking params are not needed. --- MatomoTracker.php | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index e178d5c..a21831b 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -146,10 +146,6 @@ public function __construct($idSite, $apiUrl = '') $this->currentTs = time(); $this->createTs = $this->currentTs; - $this->visitCount = 0; - $this->currentVisitTs = false; - $this->lastVisitTs = false; - $this->ecommerceLastOrderTimestamp = false; // Allow debug while blocking the request $this->requestTimeout = 600; @@ -967,7 +963,6 @@ public function getUrlTrackEcommerceOrder( } $url = $this->getUrlTrackEcommerce($grandTotal, $subTotal, $tax, $shipping, $discount); $url .= '&ec_id=' . urlencode($orderId); - $this->ecommerceLastOrderTimestamp = $this->getTimestamp(); return $url; } @@ -1366,16 +1361,11 @@ protected function loadVisitorIdCookie() if (strlen($parts[0]) != self::LENGTH_VISITOR_ID) { return false; } + /* $this->cookieVisitorId provides backward compatibility since getVisitorId() - didn't change any existing VisitorId value */ +didn't change any existing VisitorId value */ $this->cookieVisitorId = $parts[0]; $this->createTs = $parts[1]; - $this->visitCount = (int)$parts[2]; - $this->currentVisitTs = $parts[3]; - $this->lastVisitTs = $parts[4]; - if (isset($parts[5])) { - $this->ecommerceLastOrderTimestamp = $parts[5]; - } return true; } @@ -1834,10 +1824,6 @@ protected function getRequest($idSite) // Values collected from cookie '&_idts=' . $this->createTs . - '&_idvc=' . $this->visitCount . - (!empty($this->lastVisitTs) ? '&_viewts=' . $this->lastVisitTs : '') . - (!empty($this->ecommerceLastOrderTimestamp) ? - '&_ects=' . urlencode($this->ecommerceLastOrderTimestamp) : '') . // These parameters are set by the JS, but optional when using API (!empty($this->plugins) ? $this->plugins : '') . @@ -2068,9 +2054,7 @@ protected function setFirstPartyCookies() $this->setCookie('ses', '*', $this->configSessionCookieTimeout); // Set the 'id' cookie - $visitCount = $this->visitCount + 1; - $cookieValue = $this->getVisitorId() . '.' . $this->createTs . '.' . $visitCount . '.' . $this->currentTs . - '.' . $this->lastVisitTs . '.' . $this->ecommerceLastOrderTimestamp; + $cookieValue = $this->getVisitorId() . '.' . $this->createTs; $this->setCookie('id', $cookieValue, $this->configVisitorCookieTimeout); // Set the 'cvar' cookie From e36906e7925bf439ba9d2eaff4b1d123e681594d Mon Sep 17 00:00:00 2001 From: diosmosis Date: Sun, 6 Sep 2020 18:09:57 -0700 Subject: [PATCH 063/115] Adding phpunit + tests. --- .gitignore | 3 +- composer.json | 3 + composer.lock | 1978 ++++++++++++++++++++++++++++++ run_tests.sh | 3 + tests/Unit/MatomoTrackerTest.php | 74 ++ tests/phpunit.xml.dist | 23 + 6 files changed, 2083 insertions(+), 1 deletion(-) create mode 100644 composer.lock create mode 100755 run_tests.sh create mode 100644 tests/Unit/MatomoTrackerTest.php create mode 100644 tests/phpunit.xml.dist diff --git a/.gitignore b/.gitignore index 57f1cb2..f7f8ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/.idea/ \ No newline at end of file +/.idea/ +/vendor/ diff --git a/composer.json b/composer.json index b985ae5..d849d1c 100644 --- a/composer.json +++ b/composer.json @@ -25,5 +25,8 @@ }, "autoload": { "classmap": ["."] + }, + "require-dev": { + "phpunit/phpunit": "^9.3" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..36b6de2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1978 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8ec49efde7b8c6d6ea9c1b81422e501f", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.9.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/88e519766fc58bd46b8265561fb79b54e2e00b28", + "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2020-08-30T16:15:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0", + "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2020-06-27T14:39:04+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-08-15T11:14:08+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-06-27T10:12:23+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-07-08T12:44:21+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", + "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.8", + "php": "^7.3 || ^8.0", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-03T07:09:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-11T05:18:21+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-06T07:04:15+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:58:13+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", + "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": "^7.3 || ^8.0", + "phpspec/prophecy": "^1.11.1", + "phpunit/php-code-coverage": "^9.1.5", + "phpunit/php-file-iterator": "^3.0.4", + "phpunit/php-invoker": "^3.1", + "phpunit/php-text-template": "^2.0.2", + "phpunit/php-timer": "^5.0.1", + "sebastian/cli-parser": "^1.0", + "sebastian/code-unit": "^1.0.5", + "sebastian/comparator": "^4.0.3", + "sebastian/diff": "^4.0.2", + "sebastian/environment": "^5.1.2", + "sebastian/exporter": "^4.0.2", + "sebastian/global-state": "^5.0", + "sebastian/object-enumerator": "^4.0.2", + "sebastian/resource-operations": "^3.0.2", + "sebastian/type": "^2.2.1", + "sebastian/version": "^3.0.1" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-27T06:30:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", + "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-12T10:49:21+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:50:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "33fcd6a26656c6546f70871244ecba4b4dced097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/33fcd6a26656c6546f70871244ecba4b4dced097", + "reference": "33fcd6a26656c6546f70871244ecba4b4dced097", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-25T14:01:34+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-30T04:46:02+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:07:24+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "22ae663c951bdc39da96603edc3239ed3a299097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/22ae663c951bdc39da96603edc3239ed3a299097", + "reference": "22ae663c951bdc39da96603edc3239ed3a299097", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-07T04:09:03+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e02bf626f404b5daec382a7b8a6a4456e49017e5", + "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-22T18:33:42+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:11:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "127a46f6b057441b201253526f81d5406d6c7840" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:12:55+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0653718a5a629b065e91f774595267f8dc32e213" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:16:22+00:00" + }, + { + "name": "sebastian/type", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-05T08:31:53+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:18:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..dec16dc --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +cd tests && php ../vendor/bin/phpunit diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php new file mode 100644 index 0000000..7d45a1a --- /dev/null +++ b/tests/Unit/MatomoTrackerTest.php @@ -0,0 +1,74 @@ +assertEquals(16, strlen($testVisitorId)); + + $createTs = strtotime('2020-03-04 03:04:05'); + + $cookieName = '_pk_id_1_f609'; + $_COOKIE[$cookieName] = $testVisitorId . '.' . $createTs; + + $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker->setUrl('http://somesite.com'); + $url = $tracker->getUrlTrackPageView('test title'); + $url = preg_replace('/&r=\d+/', "", $url); + + $queryStr = parse_url($url, PHP_URL_QUERY); + parse_str($queryStr, $query); + + $this->assertEquals($testVisitorId, $query['_id']); + $this->assertEquals($createTs, $query['_idts']); + + $expected = 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&_idts=1583291045&_id=0958f111f2588a1b&url=http%3A%2F%2Fsomesite.com&urlref=&action_name=test+title'; + $this->assertEquals($expected, $url); + } + + public function test_trackingWithPreMatomo4CookieSetsCorrectUrl() + { + $testVisitorId = substr(md5('testother'), 0, 16); + $this->assertEquals(16, strlen($testVisitorId)); + + $createTs = strtotime('2020-03-04 05:04:05'); + $currentTs = strtotime('2020-03-05 05:04:05'); + $lastVisitTs = strtotime('2020-03-06 05:04:05'); + $ecommerceLastOrderTs = strtotime('2020-03-06 06:04:05'); + + $cookieName = '_pk_id_1_f609'; + $_COOKIE[$cookieName] = $testVisitorId . '.' . $createTs . '.5.' . $currentTs . '.' . $lastVisitTs . '.' . $ecommerceLastOrderTs; + + $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker->setUrl('http://somesite.com'); + $url = $tracker->getUrlTrackPageView('test title'); + $url = preg_replace('/&r=\d+/', "", $url); + + $queryStr = parse_url($url, PHP_URL_QUERY); + parse_str($queryStr, $query); + + $this->assertEquals($testVisitorId, $query['_id']); + $this->assertEquals($createTs, $query['_idts']); + + $expected = 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&_idts=1583298245&_id=b446c233274f79f0&url=http%3A%2F%2Fsomesite.com&urlref=&action_name=test+title'; + $this->assertEquals($expected, $url); + } +} \ No newline at end of file diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist new file mode 100644 index 0000000..fbb381c --- /dev/null +++ b/tests/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + + ./Unit + + + From fdde15de47ee01283dba8290687a3f58bbe0d8c7 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Sun, 6 Sep 2020 18:10:16 -0700 Subject: [PATCH 064/115] add result cache to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f7f8ac3..440cce3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.idea/ /vendor/ +/tests/.phpunit.result.cache + From b7fb9b369e909174fc6e9d3fe9eee5de07cbca94 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Fri, 20 Nov 2020 21:19:34 -0800 Subject: [PATCH 065/115] add two new changelog entries and set new version to 3.0 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 014d86f..3cfdc3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. - -## Matomo PHP Tracker 4.0.0 +## Matomo PHP Tracker 3.0.0 Attention: This version of Matomo PHP Tracker is no longer compatible with Matomo 3.x or earlier @@ -12,3 +11,5 @@ Attention: This version of Matomo PHP Tracker is no longer compatible with Matom - Sending requests using cURL will now throw an exception if an error occurs in a request. - Matomo does not longer support tracking of these browser plugins: Gears, Director. Therefor the signature of `setPlugins()` changed. - Implementation of ecommerce views changed from custom variables to raw parameters +- It is now possible to configure cookie options for Secure, HTTPOnly and SameSite. +- Add method setRequestMethodNonBulk() to allow (non bulk) POST requests. From 1c7db2d73385437895bfe8c585e270d90ce2be0f Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Tue, 22 Dec 2020 08:29:23 +1300 Subject: [PATCH 066/115] Mention deprecation in method itself fix https://github.com/matomo-org/matomo-php-tracker/issues/82 --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index a21831b..d55630f 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -200,7 +200,7 @@ public function setUrlReferrer($url) } /** - * Sets the time that generating the document on the server side took. + * 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 From 53bfa641f6467f3cd852810d999cfeb9fdd77dcc Mon Sep 17 00:00:00 2001 From: Scott Dutton Date: Mon, 17 May 2021 12:48:45 +0100 Subject: [PATCH 067/115] Sync to the BSD-3 licence Fixes #93 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d849d1c..44e608a 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "PHP Client for Matomo Analytics Tracking API", "keywords": ["matomo","piwik","tracker","analytics"], "homepage": "https://matomo.org", - "license": "BSD-2-Clause", + "license": "BSD-3-Clause", "authors": [ { "name": "The Matomo Team", From 3f1e66a15fcf735ceaf71982296e99a204e60973 Mon Sep 17 00:00:00 2001 From: Scott Dutton Date: Wed, 19 May 2021 13:11:06 +0100 Subject: [PATCH 068/115] Sync licence in readme Just noticed the readme has the 2 clause as well --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0340777..43056ff 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,4 @@ require_once("MatomoTracker.php"); ## License -Released under the [BSD License](http://www.opensource.org/licenses/bsd-license.php) +Released under the [BSD License](https://opensource.org/licenses/BSD-3-Clause) From 1cc8a2646ec6a72f957ade077d9e6969d2357196 Mon Sep 17 00:00:00 2001 From: JasonMortonNZ Date: Fri, 12 Nov 2021 18:12:23 +1300 Subject: [PATCH 069/115] Fix for double newline issue when a redirect is used --- MatomoTracker.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 9e6ef2f..ac5235d 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1708,15 +1708,21 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal ob_start(); $response = @curl_exec($ch); ob_end_clean(); + $header = ''; $content = ''; - + if ($response === false) { throw new \RuntimeException(curl_error($ch)); } - + if (!empty($response)) { - list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2); + // 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)); From 9d48a99292283dfa8bd87c5008d831ffcf9f9435 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Sun, 14 Nov 2021 22:54:23 +0100 Subject: [PATCH 070/115] Couple of fixes for PHP 8.1 --- MatomoTracker.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ac5235d..e3efc8f 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -82,9 +82,9 @@ class MatomoTracker */ public function __construct($idSite, $apiUrl = '') { - $this->ecommerceItems = array(); + $this->ecommerceItems = []; $this->attributionInfo = false; - $this->eventCustomVar = false; + $this->eventCustomVar = []; $this->forcedDatetime = false; $this->forcedNewVisit = false; $this->networkTime = false; @@ -93,10 +93,10 @@ public function __construct($idSite, $apiUrl = '') $this->domProcessingTime = false; $this->domCompletionTime = false; $this->onLoadTime = false; - $this->pageCustomVar = false; - $this->ecommerceView = array(); - $this->customParameters = array(); - $this->customDimensions = array(); + $this->pageCustomVar = []; + $this->ecommerceView = []; + $this->customParameters = []; + $this->customDimensions = []; $this->customData = false; $this->hasCookies = false; $this->token_auth = false; @@ -153,14 +153,14 @@ public function __construct($idSite, $apiUrl = '') // Allow debug while blocking the request $this->requestTimeout = 600; $this->doBulkRequests = false; - $this->storedTrackingActions = array(); + $this->storedTrackingActions = []; $this->sendImageResponse = true; $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); - $this->outgoingTrackerCookies = array(); - $this->incomingTrackerCookies = array(); + $this->outgoingTrackerCookies = []; + $this->incomingTrackerCookies = []; } /** @@ -365,9 +365,9 @@ public function getCustomVariable($id, $scope = 'visit') */ public function clearCustomVariables() { - $this->visitorCustomVar = array(); - $this->pageCustomVar = array(); - $this->eventCustomVar = array(); + $this->visitorCustomVar = []; + $this->pageCustomVar = []; + $this->eventCustomVar = []; } /** @@ -2105,13 +2105,13 @@ protected function setCookie($cookieName, $cookieValue, $cookieTTL) } /** - * @return bool|mixed + * @return array */ protected function getCustomVariablesFromCookie() { $cookie = $this->getCookieMatchingName('cvar'); if (!$cookie) { - return false; + return []; } return json_decode($cookie, $assoc = true); From 8626c762f6faab25e0d75f14e34284d255657853 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Mon, 15 Nov 2021 11:34:28 +0100 Subject: [PATCH 071/115] Another PHP 8.1 fix Fixes this deprecation warning: ``` PHP Deprecated: strtoupper(): Passing null to parameter #1 ($string) of type string is deprecated in /srv/matomo/vendor/matomo/matomo-php-tracker/MatomoTracker.php on line 1623 ``` --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index e3efc8f..fe3a0b0 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1620,7 +1620,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $forcePostUrlEncoded = false; if (!$this->doBulkRequests) { - if (strtoupper($this->requestMethod) === 'POST') { + if (!empty($this->requestMethod) && strtoupper($this->requestMethod) === 'POST') { // POST ALL parameters and have no GET parameters $urlParts = explode('?', $url); From 691997ac30edee2bf6ee211768d6aaed0aee1231 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Thu, 25 Nov 2021 11:20:39 +0100 Subject: [PATCH 072/115] Remove some files from exports --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..05bc5a6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +tests/ export-ignore +run_tests.sh export-ignore From add4c81063fc1cbc1e2b48b4b7080cd65cbf9bd4 Mon Sep 17 00:00:00 2001 From: Demichev Date: Sat, 7 May 2022 23:19:48 +1000 Subject: [PATCH 073/115] Added virtualization of curl/stream options --- MatomoTracker.php | 176 ++++++++++++++++++++++++++-------------------- 1 file changed, 98 insertions(+), 78 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index fe3a0b0..bbac5e2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1594,6 +1594,102 @@ private function getProxy() */ static public $DEBUG_LAST_REQUESTED_URL = false; + /** + * Returns array of curl options for request + */ + protected function prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded) + { + $options = array( + CURLOPT_URL => $url, + CURLOPT_USERAGENT => $this->userAgent, + CURLOPT_HEADER => true, + CURLOPT_TIMEOUT => $this->requestTimeout, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array( + '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 + */ + protected function prepareStreamOptions($method, $data, $forcePostUrlEncoded) + { + $stream_options = array( + 'http' => array( + '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 */ @@ -1649,59 +1745,8 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } } - $proxy = $this->getProxy(); - if (function_exists('curl_init') && function_exists('curl_exec')) { - $options = array( - CURLOPT_URL => $url, - CURLOPT_USERAGENT => $this->userAgent, - CURLOPT_HEADER => true, - CURLOPT_TIMEOUT => $this->requestTimeout, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => array( - 'Accept-Language: ' . $this->acceptLanguage, - ), - ); - - if ($method === 'GET') { - $options[CURLOPT_FOLLOWLOCATION] = true; - } - - if (defined('PATH_TO_CERTIFICATES_FILE')) { - $options[CURLOPT_CAINFO] = PATH_TO_CERTIFICATES_FILE; - } - - 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(); - } + $options = $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded); $ch = curl_init(); curl_setopt_array($ch, $options); @@ -1728,32 +1773,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $this->parseIncomingCookies(explode("\r\n", $header)); } elseif (function_exists('stream_context_create')) { - $stream_options = array( - 'http' => array( - 'method' => $method, - 'user_agent' => $this->userAgent, - 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n", - 'timeout' => $this->requestTimeout, - ), - ); - - 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(); - } + $stream_options = $this->prepareStreamOptions($method, $data, $forcePostUrlEncoded); $ctx = stream_context_create($stream_options); $response = file_get_contents($url, 0, $ctx); From aa9d124af848383b756129c79c0cd373406d2e9b Mon Sep 17 00:00:00 2001 From: sgiehl Date: Mon, 13 Jun 2022 15:10:44 +0200 Subject: [PATCH 074/115] Adds support for client hints --- MatomoTracker.php | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index bbac5e2..ac10b05 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -121,6 +121,14 @@ public function __construct($idSite, $apiUrl = '') $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->clientHints = []; + $this->setClientHints( + $_SERVER['HTTP_SEC_CH_UA_MODEL'] ?? '', + $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '', + $_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] ?? '', + $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST'] ?? '', + $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION'] ?? '' + ); if (!empty($apiUrl)) { self::$URL = $apiUrl; } @@ -482,6 +490,47 @@ public function setUserAgent($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. + * + * @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 $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($model = '', $platform = '', $platformVersion = '', $fullVersionList = '', $uaFullVersion = '') + { + if (is_string($fullVersionList)) { + $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; + $list = []; + + while (\preg_match($reg, $value, $matches)) { + $list[] = ['brand' => $matches[1], 'version' => $matches[2]]; + $value = \substr($value, \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. @@ -1702,6 +1751,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $this->storedTrackingActions[] = $url . (!empty($this->userAgent) ? ('&ua=' . urlencode($this->userAgent)) : '') + . (!empty($this->clientHints) ? ('&uadata=' . urlencode(json_encode($this->clientHints))) : '') . (!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 @@ -1709,6 +1759,7 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $this->clearCustomDimensions(); $this->clearCustomTrackingParameters(); $this->userAgent = false; + $this->clientHints = false; $this->acceptLanguage = false; return true; From 9c2ff78634754012ea7f91992ba71ed44dafdd5c Mon Sep 17 00:00:00 2001 From: sgiehl Date: Tue, 21 Jun 2022 11:34:12 +0200 Subject: [PATCH 075/115] use code compatible with PHP 5.3 --- MatomoTracker.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ac10b05..d0840c2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -123,11 +123,11 @@ public function __construct($idSite, $apiUrl = '') $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : false; $this->clientHints = []; $this->setClientHints( - $_SERVER['HTTP_SEC_CH_UA_MODEL'] ?? '', - $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '', - $_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] ?? '', - $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST'] ?? '', - $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION'] ?? '' + !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; @@ -494,6 +494,8 @@ public function setUserAgent($userAgent) * 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' @@ -510,9 +512,9 @@ public function setClientHints($model = '', $platform = '', $platformVersion = ' $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; $list = []; - while (\preg_match($reg, $value, $matches)) { + while (\preg_match($reg, $fullVersionList, $matches)) { $list[] = ['brand' => $matches[1], 'version' => $matches[2]]; - $value = \substr($value, \strlen($matches[0])); + $fullVersionList = \substr($fullVersionList, \strlen($matches[0])); } $fullVersionList = $list; @@ -1751,7 +1753,6 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $this->storedTrackingActions[] = $url . (!empty($this->userAgent) ? ('&ua=' . urlencode($this->userAgent)) : '') - . (!empty($this->clientHints) ? ('&uadata=' . urlencode(json_encode($this->clientHints))) : '') . (!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 @@ -1953,6 +1954,9 @@ protected function getRequest($idSite) $customFields . $customDimensions . (!$this->sendImageResponse ? '&send_image=0' : '') . + // client hints + (!empty($this->clientHints) ? ('&uadata=' . urlencode(json_encode($this->clientHints))) : '') . + // DEBUG $this->DEBUG_APPEND_URL; From 729a4cb99cfa181cbb6972e85a21730b1c862644 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Wed, 9 Nov 2022 10:36:59 +0100 Subject: [PATCH 076/115] fix possible notices on PHP 8.1 fixes #107 --- MatomoTracker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index d0840c2..75aca63 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1927,8 +1927,8 @@ protected function getRequest($idSite) (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters - '&url=' . urlencode($this->pageUrl) . - '&urlref=' . urlencode($this->urlReferrer) . + '&url=' . urlencode($this->pageUrl ?? '') . + '&urlref=' . urlencode($this->urlReferrer ?? '') . ((!empty($this->pageCharset) && $this->pageCharset != self::DEFAULT_CHARSET_PARAMETER_VALUES) ? '&cs=' . $this->pageCharset : '') . From e7496bf9cd5742479820a116413184a60e5b79bd Mon Sep 17 00:00:00 2001 From: diosmosis Date: Mon, 21 Nov 2022 16:04:20 -0800 Subject: [PATCH 077/115] add tracking methods for tracking crashes w/ CrashAnalytics --- MatomoTracker.php | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 75aca63..328f51c 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -907,6 +907,47 @@ public function doTrackEcommerceOrder( return $this->sendRequest($url); } + /** + * Tracks a PHP Throwable a crash (requires CrashAnalytics to be enabled in the target Matomo) + * + * @param Throwable $ex (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 $ex, $category = null) + { + $message = $ex->getMessage(); + $stack = $ex->getTraceAsString(); + $type = get_class($ex); + $location = $ex->getFile(); + $line = $ex->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($message, $type = null, $category = null, $stack = null, $location = null, $line = null, $column = null) + { + $url = $this->getUrlTrackCrash($message, $type, $category, $stack, $location, $line, $column); + + return $this->sendRequest($url); + } + /** * Sends a ping request. * @@ -1248,6 +1289,46 @@ public function getUrlTrackAction($actionUrl, $actionType) 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($message, $type = null, $category = null, $stack = null, $location = null, $line = null, $column = null) + { + $url = $this->getRequest($this->idSite); + $url .= '&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 From 26aebac44fb5fa400c38f8e6c905ec72a6eaac95 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Mon, 21 Nov 2022 16:21:18 -0800 Subject: [PATCH 078/115] forgot ca=1 parameter --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 328f51c..33998c2 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1306,7 +1306,7 @@ public function getUrlTrackAction($actionUrl, $actionType) public function getUrlTrackCrash($message, $type = null, $category = null, $stack = null, $location = null, $line = null, $column = null) { $url = $this->getRequest($this->idSite); - $url .= '&cra=' . urlencode($message); + $url .= '&ca=1&cra=' . urlencode($message); if ($type) { $url .= '&cra_tp=' . urlencode($type); } From e2b0e1467329c0fe0d0a8235711aa6554a34d12a Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Fri, 2 Dec 2022 15:19:27 +0100 Subject: [PATCH 079/115] update php requirement --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 44e608a..c667771 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": ">=5.3", + "php": "^7.0 || ^8.0", "ext-json": "*" }, "suggest": { From d33d78cc28996bf094e01dc25f2c0d4e2e8cbd81 Mon Sep 17 00:00:00 2001 From: Thomas Steur Date: Sun, 4 Dec 2022 11:09:38 +1300 Subject: [PATCH 080/115] Add method to disable bulk tracking Adds a method `disableBulkTracking` --- MatomoTracker.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 75aca63..c34edec 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -613,6 +613,16 @@ public function enableBulkTracking() $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() + { + $this->doBulkRequests = false; + } + /** * Enable Cookie Creation - this will cause a first party VisitorId cookie to be set when the VisitorId is set or reset * From 78932f61692b18c6b5de923dbc3880ad3380e328 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Wed, 18 Jan 2023 15:28:44 +0100 Subject: [PATCH 081/115] Temporarily allow dynamic properties refs #111 --- MatomoTracker.php | 1 + 1 file changed, 1 insertion(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 59fc9af..7fcf5ba 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -19,6 +19,7 @@ * @package MatomoTracker * @api */ +#[AllowDynamicProperties] class MatomoTracker { /** From cc90fd60f756fd252d858fc146663eb9e6664384 Mon Sep 17 00:00:00 2001 From: Max Vogl Date: Tue, 7 Mar 2023 13:06:21 +0000 Subject: [PATCH 082/115] Add function to set api url --- MatomoTracker.php | 5 +++++ tests/Unit/MatomoTrackerTest.php | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 7fcf5ba..62d21dd 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -172,6 +172,11 @@ public function __construct($idSite, $apiUrl = '') $this->incomingTrackerCookies = []; } + public function setApiUrl(string $url) + { + self::$URL = $url; + } + /** * By default, Matomo expects utf-8 encoded values, for example * for the page URL parameter values, Page Title, etc. diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 7d45a1a..229e016 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -71,4 +71,14 @@ public function test_trackingWithPreMatomo4CookieSetsCorrectUrl() $expected = 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&_idts=1583298245&_id=b446c233274f79f0&url=http%3A%2F%2Fsomesite.com&urlref=&action_name=test+title'; $this->assertEquals($expected, $url); } + + public function test_setApiUrl() + { + $newApiUrl = 'https://NEW-API-URL.com'; + $tracker = new \MatomoTracker(1, self::TEST_URL); + $tracker->setApiUrl('https://NEW-API-URL.com'); + $url = $tracker->getUrlTrackPageView('test title'); + + $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); + } } \ No newline at end of file From 196dc8efba926f4721e5fec57ef8d07f5b169352 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Tue, 7 Mar 2023 16:17:28 +0100 Subject: [PATCH 083/115] Update tests/Unit/MatomoTrackerTest.php --- tests/Unit/MatomoTrackerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 229e016..1dcf5c1 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -76,7 +76,7 @@ public function test_setApiUrl() { $newApiUrl = 'https://NEW-API-URL.com'; $tracker = new \MatomoTracker(1, self::TEST_URL); - $tracker->setApiUrl('https://NEW-API-URL.com'); + $tracker->setApiUrl($newApiUrl); $url = $tracker->getUrlTrackPageView('test title'); $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); From 949924c799e4e3ae6bcff20c499878d4d43817c1 Mon Sep 17 00:00:00 2001 From: vpapaloukas Date: Fri, 24 Mar 2023 15:17:25 +0200 Subject: [PATCH 084/115] add support for CURLOPT_CONNECTTIMEOUT --- MatomoTracker.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 62d21dd..6336f20 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -161,6 +161,7 @@ public function __construct($idSite, $apiUrl = '') // Allow debug while blocking the request $this->requestTimeout = 600; + $this->requestConnectTimeout = 300; $this->doBulkRequests = false; $this->storedTrackingActions = []; @@ -1697,6 +1698,32 @@ public function setRequestTimeout($timeout) return $this; } + /** + * Returns the maximum number of seconds the tracker will spend trying to connect to Matomo. + * Defaults to 0 seconds (unlimited). + */ + public function getRequestConnectTimeout() + { + 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($timeout) + { + if (!is_int($timeout) || $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 @@ -1752,6 +1779,7 @@ protected function prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded CURLOPT_USERAGENT => $this->userAgent, CURLOPT_HEADER => true, CURLOPT_TIMEOUT => $this->requestTimeout, + CURLOPT_CONNECTTIMEOUT => $this->requestConnectTimeout, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array( 'Accept-Language: ' . $this->acceptLanguage, From 390a90d6fa6b83691121c8e41aeb2e77d6030c01 Mon Sep 17 00:00:00 2001 From: vpapaloukas Date: Fri, 24 Mar 2023 16:03:13 +0200 Subject: [PATCH 085/115] fix getRequestConnectTimeout docblock --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 6336f20..1a06676 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1700,7 +1700,7 @@ public function setRequestTimeout($timeout) /** * Returns the maximum number of seconds the tracker will spend trying to connect to Matomo. - * Defaults to 0 seconds (unlimited). + * Defaults to 300 seconds. */ public function getRequestConnectTimeout() { From 1ae76376cd0060980b1928d30faed97dd5f379aa Mon Sep 17 00:00:00 2001 From: Christopher Georg Date: Wed, 19 Apr 2023 22:17:40 +0700 Subject: [PATCH 086/115] feat: remove composer.lock --- composer.lock | 1978 ------------------------------------------------- 1 file changed, 1978 deletions(-) delete mode 100644 composer.lock diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 36b6de2..0000000 --- a/composer.lock +++ /dev/null @@ -1,1978 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "8ec49efde7b8c6d6ea9c1b81422e501f", - "packages": [], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^6.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2020-05-29T17:27:14+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.10.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2020-06-29T13:22:24+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.9.1", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/88e519766fc58bd46b8265561fb79b54e2e00b28", - "reference": "88e519766fc58bd46b8265561fb79b54e2e00b28", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "time": "2020-08-30T16:15:20+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2020-06-27T14:33:11+00:00" - }, - { - "name": "phar-io/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0", - "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "time": "2020-06-27T14:39:04+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-08-15T11:14:08+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.11.1", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2", - "phpdocumentor/reflection-docblock": "^5.0", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2020-07-08T12:44:21+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "9.1.7", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", - "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.8", - "php": "^7.3 || ^8.0", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-03T07:09:19+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", - "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-11T05:18:21+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", - "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.0" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-08-06T07:04:15+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", - "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T11:55:37+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "5.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", - "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T11:58:13+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "9.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", - "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.3.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", - "phar-io/version": "^3.0.2", - "php": "^7.3 || ^8.0", - "phpspec/prophecy": "^1.11.1", - "phpunit/php-code-coverage": "^9.1.5", - "phpunit/php-file-iterator": "^3.0.4", - "phpunit/php-invoker": "^3.1", - "phpunit/php-text-template": "^2.0.2", - "phpunit/php-timer": "^5.0.1", - "sebastian/cli-parser": "^1.0", - "sebastian/code-unit": "^1.0.5", - "sebastian/comparator": "^4.0.3", - "sebastian/diff": "^4.0.2", - "sebastian/environment": "^5.1.2", - "sebastian/exporter": "^4.0.2", - "sebastian/global-state": "^5.0", - "sebastian/object-enumerator": "^4.0.2", - "sebastian/resource-operations": "^3.0.2", - "sebastian/type": "^2.2.1", - "sebastian/version": "^3.0.1" - }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.3-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ], - "files": [ - "src/Framework/Assert/Functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-08-27T06:30:58+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", - "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-08-12T10:49:21+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "c1e2df332c905079980b119c4db103117e5e5c90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", - "reference": "c1e2df332c905079980b119c4db103117e5e5c90", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:50:45+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", - "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:04:00+00:00" - }, - { - "name": "sebastian/comparator", - "version": "4.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", - "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:05:46+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "33fcd6a26656c6546f70871244ecba4b4dced097" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/33fcd6a26656c6546f70871244ecba4b4dced097", - "reference": "33fcd6a26656c6546f70871244ecba4b4dced097", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-25T14:01:34+00:00" - }, - { - "name": "sebastian/diff", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", - "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-30T04:46:02+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", - "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:07:24+00:00" - }, - { - "name": "sebastian/exporter", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "571d721db4aec847a0e59690b954af33ebf9f023" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", - "reference": "571d721db4aec847a0e59690b954af33ebf9f023", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:08:55+00:00" - }, - { - "name": "sebastian/global-state", - "version": "5.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "22ae663c951bdc39da96603edc3239ed3a299097" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/22ae663c951bdc39da96603edc3239ed3a299097", - "reference": "22ae663c951bdc39da96603edc3239ed3a299097", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-08-07T04:09:03+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e02bf626f404b5daec382a7b8a6a4456e49017e5", - "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-22T18:33:42+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", - "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:11:32+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "127a46f6b057441b201253526f81d5406d6c7840" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", - "reference": "127a46f6b057441b201253526f81d5406d6c7840", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:12:55+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", - "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:14:17+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0653718a5a629b065e91f774595267f8dc32e213" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", - "reference": "0653718a5a629b065e91f774595267f8dc32e213", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:16:22+00:00" - }, - { - "name": "sebastian/type", - "version": "2.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", - "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-05T08:31:53+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", - "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:18:43+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2020-07-12T23:59:07+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.9.1", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" - }, - "type": "library", - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2020-07-08T17:02:28+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.3", - "ext-json": "*" - }, - "platform-dev": [], - "plugin-api-version": "1.1.0" -} From 9be68b0575318f969cc401ddd816e18240505a5d Mon Sep 17 00:00:00 2001 From: Christopher Georg Date: Wed, 19 Apr 2023 22:17:52 +0700 Subject: [PATCH 087/115] feat: add composer.lock to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 440cce3..6923359 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ /vendor/ /tests/.phpunit.result.cache +composer.lock From 267a1b7fd0386015dd27be9fb67005d010ea3766 Mon Sep 17 00:00:00 2001 From: Christopher Georg Date: Wed, 19 Apr 2023 22:32:02 +0700 Subject: [PATCH 088/115] feat: default folder structure, allow phpunit 10 --- .gitignore | 2 +- composer.json | 9 ++++++++- phpunit.xml.dist | 12 ++++++++++++ run_tests.sh | 2 +- tests/phpunit.xml.dist | 23 ----------------------- 5 files changed, 22 insertions(+), 26 deletions(-) create mode 100644 phpunit.xml.dist delete mode 100644 tests/phpunit.xml.dist diff --git a/.gitignore b/.gitignore index 440cce3..c35d669 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /.idea/ /vendor/ -/tests/.phpunit.result.cache +.phpunit.result.cache diff --git a/composer.json b/composer.json index c667771..c4c6f98 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,14 @@ "autoload": { "classmap": ["."] }, + + "autoload-dev": { + "psr-4": { + "\\": "tests/" + } + }, + "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.3 || ^10.1" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..cb8caf5 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,12 @@ + + + + + + ./tests/Unit + + + diff --git a/run_tests.sh b/run_tests.sh index dec16dc..fc551d1 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -cd tests && php ../vendor/bin/phpunit +php vendor/bin/phpunit diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist deleted file mode 100644 index fbb381c..0000000 --- a/tests/phpunit.xml.dist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - ./Unit - - - From 4d40b7efd56e87fca407cf1cca3606272bb4860b Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 2 Oct 2023 15:22:08 +0200 Subject: [PATCH 089/115] Fix wrong method description --- MatomoTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 1a06676..824bf46 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1212,7 +1212,7 @@ public function getUrlTrackContentImpression($contentName, $contentPiece, $conte } /** - * Builds URL to track a content impression. + * Builds URL to track a content interaction. * * @see doTrackContentInteraction() * @param string $interaction The name of the interaction with the content. For instance a 'click' From 647cb0d5ed389afd4aa2d06a10a1bafaaf5fa4ac Mon Sep 17 00:00:00 2001 From: Eric Prokop Date: Fri, 13 Oct 2023 18:15:34 +0200 Subject: [PATCH 090/115] Functions to get and set the page view id manually --- MatomoTracker.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 824bf46..67b61c0 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -114,6 +114,7 @@ public function __construct($idSite, $apiUrl = '') $this->localMinute = false; $this->localSecond = false; $this->idPageview = false; + $this->idPageviewSetManually = false; $this->idSite = $idSite; $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false; @@ -705,12 +706,38 @@ protected function getCookieName($cookieName) */ public function doTrackPageView($documentTitle) { - $this->generateNewPageviewId(); + if (!$this->idPageviewSetManually) { + $this->generateNewPageviewId(); + } $url = $this->getUrlTrackPageView($documentTitle); 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). + * + * @param string $idPageview + */ + public function setPageviewId($idPageview) + { + $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 mixed The PageView id as string or false if there is none yet. + */ + public function getPageviewId() + { + return $this->idPageview; + } private function generateNewPageviewId() { From 82b907d086585edd236bc99d1884cb536f2bc70f Mon Sep 17 00:00:00 2001 From: mzaman Date: Mon, 25 Mar 2024 18:37:29 +0600 Subject: [PATCH 091/115] fix: Check for cURL error before throwing exception in sendRequest method Description: This commit addresses an issue where an exception was being thrown unconditionally in the `sendRequest` method of the `MatomoTracker` class when a cURL request failed. The error handling logic now checks if `curl_error($ch)` returns a non-empty value before throwing a `\RuntimeException`. This ensures that exceptions are only thrown when there is an actual cURL error, preventing unnecessary exceptions from being raised. This change improves error handling and provides more accurate feedback when cURL requests fail. --- MatomoTracker.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index 67b61c0..ab76e40 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1962,8 +1962,11 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal $content = ''; if ($response === false) { - throw new \RuntimeException(curl_error($ch)); - } + $curlError = curl_error($ch); + if (!empty($curlError)) { + throw new \RuntimeException($curlError); + } + } if (!empty($response)) { // extract header From 80fca688ad29344003cc9f05ccb7d3a08e42ff9e Mon Sep 17 00:00:00 2001 From: alutskevich Date: Thu, 18 Apr 2024 00:58:20 +0200 Subject: [PATCH 092/115] create properties --- MatomoTracker.php | 195 ++++++++++++++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 67 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ab76e40..a225947 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -71,6 +71,133 @@ class MatomoTracker 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 = []; + private $requestMethod = null; /** @@ -83,47 +210,12 @@ class MatomoTracker */ public function __construct($idSite, $apiUrl = '') { - $this->ecommerceItems = []; - $this->attributionInfo = false; - $this->eventCustomVar = []; - $this->forcedDatetime = false; - $this->forcedNewVisit = false; - $this->networkTime = false; - $this->serverTime = false; - $this->transferTime = false; - $this->domProcessingTime = false; - $this->domCompletionTime = false; - $this->onLoadTime = false; - $this->pageCustomVar = []; - $this->ecommerceView = []; - $this->customParameters = []; - $this->customDimensions = []; - $this->customData = false; - $this->hasCookies = false; - $this->token_auth = false; - $this->userAgent = false; - $this->country = false; - $this->region = false; - $this->city = false; - $this->lat = false; - $this->long = false; - $this->width = false; - $this->height = false; - $this->plugins = false; - $this->localHour = false; - $this->localMinute = false; - $this->localSecond = false; - $this->idPageview = false; - $this->idPageviewSetManually = false; - $this->idSite = $idSite; $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false; - $this->pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES; $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->clientHints = []; $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'] : '', @@ -135,43 +227,12 @@ public function __construct($idSite, $apiUrl = '') self::$URL = $apiUrl; } - // Life of the visitor cookie (in sec) - $this->configVisitorCookieTimeout = 33955200; // 13 months (365 + 28 days) - // Life of the session cookie (in sec) - $this->configSessionCookieTimeout = 1800; // 30 minutes - // Life of the session cookie (in sec) - $this->configReferralCookieTimeout = 15768000; // 6 months - - // Visitor Ids in order - $this->userId = false; - $this->forcedVisitorId = false; - $this->cookieVisitorId = false; - $this->randomVisitorId = false; - $this->setNewVisitorId(); - $this->configCookiesDisabled = false; - $this->configCookiePath = self::DEFAULT_COOKIE_PATH; - $this->configCookieDomain = ''; - $this->configCookieSameSite = ''; - $this->configCookieSecure = false; - $this->configCookieHTTPOnly = false; - $this->currentTs = time(); $this->createTs = $this->currentTs; - - // Allow debug while blocking the request - $this->requestTimeout = 600; - $this->requestConnectTimeout = 300; - $this->doBulkRequests = false; - $this->storedTrackingActions = []; - - $this->sendImageResponse = true; - + $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); - - $this->outgoingTrackerCookies = []; - $this->incomingTrackerCookies = []; } public function setApiUrl(string $url) From db0654f9a6a7bbdefb18ef1a199f29e354187ce2 Mon Sep 17 00:00:00 2001 From: alutskevich Date: Wed, 1 May 2024 23:22:55 +0200 Subject: [PATCH 093/115] min php version is 8.2 --- MatomoTracker.php | 20 ++++++-------------- composer.json | 6 ++---- tests/Unit/MatomoTrackerTest.php | 3 +++ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ab76e40..f266df6 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -26,10 +26,8 @@ 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 static string $URL = ''; /** * API Version @@ -37,7 +35,7 @@ class MatomoTracker * @ignore * @var int */ - const VERSION = 1; + public const VERSION = 1; /** * @ignore @@ -49,27 +47,21 @@ class MatomoTracker * * @ignore */ - const LENGTH_VISITOR_ID = 16; + public const LENGTH_VISITOR_ID = 16; /** * Charset * @see setPageCharset * @ignore */ - const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; + public const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; /** * See matomo.js */ - const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; - - /** - * Defines how many categories can be used max when calling addEcommerceItem(). - * @var int - */ - const MAX_NUM_ECOMMERCE_ITEM_CATEGORIES = 5; + public const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; - const DEFAULT_COOKIE_PATH = '/'; + public const DEFAULT_COOKIE_PATH = '/'; private $requestMethod = null; diff --git a/composer.json b/composer.json index c4c6f98..d9552e2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^7.0 || ^8.0", + "php": "^8.1", "ext-json": "*" }, "suggest": { @@ -26,14 +26,12 @@ "autoload": { "classmap": ["."] }, - "autoload-dev": { "psr-4": { "\\": "tests/" } }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^10.1" + "phpunit/phpunit": "^11.1" } } diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 1dcf5c1..703b631 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -1,4 +1,7 @@ Date: Wed, 1 May 2024 23:23:26 +0200 Subject: [PATCH 094/115] Revert "min php version is 8.2" This reverts commit db0654f9a6a7bbdefb18ef1a199f29e354187ce2. --- MatomoTracker.php | 20 ++++++++++++++------ composer.json | 6 ++++-- tests/Unit/MatomoTrackerTest.php | 3 --- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index f266df6..ab76e40 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -26,8 +26,10 @@ 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 */ - public static string $URL = ''; + static public $URL = ''; /** * API Version @@ -35,7 +37,7 @@ class MatomoTracker * @ignore * @var int */ - public const VERSION = 1; + const VERSION = 1; /** * @ignore @@ -47,21 +49,27 @@ class MatomoTracker * * @ignore */ - public const LENGTH_VISITOR_ID = 16; + const LENGTH_VISITOR_ID = 16; /** * Charset * @see setPageCharset * @ignore */ - public const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; + const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; /** * See matomo.js */ - public const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; + const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; + + /** + * Defines how many categories can be used max when calling addEcommerceItem(). + * @var int + */ + const MAX_NUM_ECOMMERCE_ITEM_CATEGORIES = 5; - public const DEFAULT_COOKIE_PATH = '/'; + const DEFAULT_COOKIE_PATH = '/'; private $requestMethod = null; diff --git a/composer.json b/composer.json index d9552e2..c4c6f98 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^8.1", + "php": "^7.0 || ^8.0", "ext-json": "*" }, "suggest": { @@ -26,12 +26,14 @@ "autoload": { "classmap": ["."] }, + "autoload-dev": { "psr-4": { "\\": "tests/" } }, + "require-dev": { - "phpunit/phpunit": "^11.1" + "phpunit/phpunit": "^9.3 || ^10.1" } } diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 703b631..1dcf5c1 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -1,7 +1,4 @@ Date: Wed, 1 May 2024 23:22:55 +0200 Subject: [PATCH 095/115] min php version is 8.1 --- MatomoTracker.php | 20 ++++++-------------- composer.json | 6 ++---- tests/Unit/MatomoTrackerTest.php | 3 +++ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index ab76e40..f266df6 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -26,10 +26,8 @@ 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 static string $URL = ''; /** * API Version @@ -37,7 +35,7 @@ class MatomoTracker * @ignore * @var int */ - const VERSION = 1; + public const VERSION = 1; /** * @ignore @@ -49,27 +47,21 @@ class MatomoTracker * * @ignore */ - const LENGTH_VISITOR_ID = 16; + public const LENGTH_VISITOR_ID = 16; /** * Charset * @see setPageCharset * @ignore */ - const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; + public const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; /** * See matomo.js */ - const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; - - /** - * Defines how many categories can be used max when calling addEcommerceItem(). - * @var int - */ - const MAX_NUM_ECOMMERCE_ITEM_CATEGORIES = 5; + public const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; - const DEFAULT_COOKIE_PATH = '/'; + public const DEFAULT_COOKIE_PATH = '/'; private $requestMethod = null; diff --git a/composer.json b/composer.json index c4c6f98..d9552e2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^7.0 || ^8.0", + "php": "^8.1", "ext-json": "*" }, "suggest": { @@ -26,14 +26,12 @@ "autoload": { "classmap": ["."] }, - "autoload-dev": { "psr-4": { "\\": "tests/" } }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^10.1" + "phpunit/phpunit": "^11.1" } } diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 1dcf5c1..703b631 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -1,4 +1,7 @@ Date: Thu, 2 May 2024 20:49:00 +0200 Subject: [PATCH 096/115] 118 - reset php version to 7.2 --- MatomoTracker.php | 2 +- composer.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index f266df6..a6eb70c 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -27,7 +27,7 @@ class MatomoTracker * Must be set before using the class by calling * MatomoTracker::$URL = 'http://yourwebsite.org/matomo/'; */ - public static string $URL = ''; + static public $URL = ''; /** * API Version diff --git a/composer.json b/composer.json index d9552e2..298087d 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^8.1", + "php": "^7.2 || ^8.0", "ext-json": "*" }, "suggest": { @@ -32,6 +32,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^11.1" + "phpunit/phpunit": "^9.3 || ^10.1" } } From 52e1e3da2fffd85caa926e290816ab30ccf12744 Mon Sep 17 00:00:00 2001 From: alutskevich Date: Thu, 2 May 2024 20:49:38 +0200 Subject: [PATCH 097/115] 118 - revert var type --- MatomoTracker.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index a6eb70c..0adda06 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -26,6 +26,8 @@ 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 = ''; From b748f81e420f077667d25beb1ac8cb7337ae617b Mon Sep 17 00:00:00 2001 From: alutskevich Date: Thu, 2 May 2024 21:02:25 +0200 Subject: [PATCH 098/115] 129 - define new property --- MatomoTracker.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index a225947..6ca4c08 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -198,6 +198,8 @@ class MatomoTracker public $incomingTrackerCookies = []; + public $visitorCustomVar; + private $requestMethod = null; /** From 75804cc138a2c3574b225f073abad1a3303a0f32 Mon Sep 17 00:00:00 2001 From: alutskevich Date: Wed, 8 May 2024 23:42:34 +0200 Subject: [PATCH 099/115] 118 - revert constant and update changelog --- CHANGELOG.md | 6 ++++++ MatomoTracker.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cfdc3e..843c00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. +## 3.3.0 +### Removed +- support PHP versions less than 7.2 +### Changed +- all constants are public + ## Matomo PHP Tracker 3.0.0 Attention: This version of Matomo PHP Tracker is no longer compatible with Matomo 3.x or earlier diff --git a/MatomoTracker.php b/MatomoTracker.php index 0adda06..f34a505 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -63,6 +63,12 @@ class MatomoTracker */ 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 = '/'; private $requestMethod = null; From d6a1142e1586f0870c005a0add7fb9aee8b5f5ed Mon Sep 17 00:00:00 2001 From: Michal Kleiner Date: Thu, 9 May 2024 10:13:43 +1200 Subject: [PATCH 100/115] Tweak changelog wording --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 843c00b..a84fded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. -## 3.3.0 +## Matomo PHP Tracker 3.3.0 ### Removed -- support PHP versions less than 7.2 +- support for PHP versions lower than 7.2 ### Changed -- all constants are public +- all `MatomoTracker` class constants are now explicitly public ## Matomo PHP Tracker 3.0.0 From ee98f14227a36a17ddc4512aa88bbb92f4dc626e Mon Sep 17 00:00:00 2001 From: alutskevich Date: Mon, 13 May 2024 22:21:29 +0200 Subject: [PATCH 101/115] 118 - update changelog.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a84fded..fcb693e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or - support for PHP versions lower than 7.2 ### Changed - all `MatomoTracker` class constants are now explicitly public +- all `MatomoTracker` dynamic properties are now explicitly public ## Matomo PHP Tracker 3.0.0 From 17942030aa78d2a148c664d881e3256c6a84aeca Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Fri, 17 May 2024 15:07:04 +0200 Subject: [PATCH 102/115] Create PHPUnit action --- .github/workflows/phpunit.yml | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/phpunit.yml diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..d68cd40 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,76 @@ +name: PHPUnit + +on: + pull_request: + push: + branches: [ master ] + +permissions: + actions: read + checks: read + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +jobs: + build: + name: PHPUnit + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest, windows-latest] + php-version: ['7.2', '8.3'] + include: + - php-version: 7.3 + operating-system: ubuntu-latest + - php-version: 7.4 + operating-system: ubuntu-latest + - php-version: 8.0 + operating-system: ubuntu-latest + - php-version: 8.1 + operating-system: ubuntu-latest + - php-version: 8.2 + operating-system: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + extensions: memcached + - name: "Composer install" + run: | + composer install --prefer-dist + - name: PHPUnit / PHP ${{ matrix.php-version }} + run: | + php -v + ./vendor/bin/phpunit + + build2: + name: PHPUnit + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ macOS-latest ] + php-version: [ '7.2', '8.3' ] + steps: + - uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + extensions: memcached + - name: "Composer install" + run: | + composer install --prefer-dist + - name: PHPUnit / PHP ${{ matrix.php-version }} + run: | + php -v + ./vendor/bin/phpunit From f1b9492b99fa57577d25dfe94a8469d53c8b537a Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Fri, 17 May 2024 15:14:53 +0200 Subject: [PATCH 103/115] Allow using PHPUnit 8.5 for PHP 7.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 298087d..f07c24e 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^10.1" + "phpunit/phpunit": "^8.5 || ^9.3 || ^10.1" } } From 92c2ce658111c7d86a552521c619083e41ebd834 Mon Sep 17 00:00:00 2001 From: Andrii Lutskevych Date: Tue, 21 May 2024 18:08:28 +0300 Subject: [PATCH 104/115] Fix: Curl Connection remaining open (#133) * add curl_close and fix undefined variable * change position of the curl_close * 128 - wrap by try-finally block * 128 - update changelog.md --------- Co-authored-by: alutskevich --- CHANGELOG.md | 4 ++++ MatomoTracker.php | 38 +++++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb693e..5afde19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. +## Matomo PHP Tracker 3.3.1 +### Fixed +- closed curl connection + ## Matomo PHP Tracker 3.3.0 ### Removed - support for PHP versions lower than 7.2 diff --git a/MatomoTracker.php b/MatomoTracker.php index 7b65ace..234c6bf 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2012,6 +2012,8 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal } } + $content = ''; + if (function_exists('curl_init') && function_exists('curl_exec')) { $options = $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded); @@ -2019,29 +2021,31 @@ protected function sendRequest($url, $method = 'GET', $data = null, $force = fal curl_setopt_array($ch, $options); ob_start(); $response = @curl_exec($ch); - ob_end_clean(); - $header = ''; - $content = ''; + try { + $header = ''; - if ($response === false) { - $curlError = curl_error($ch); - if (!empty($curlError)) { - throw new \RuntimeException($curlError); + 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); - } + if (!empty($response)) { + // extract header + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $header = substr($response, 0, $headerSize); - $this->parseIncomingCookies(explode("\r\n", $header)); + // 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); From 4fb85291a6fbcc43cafe9bbc05123f9c9b2ded71 Mon Sep 17 00:00:00 2001 From: Andrii Lutskevych Date: Tue, 18 Jun 2024 17:59:08 +0300 Subject: [PATCH 105/115] 134 - Strict types for arguments and return types (#135) * 134 - add types for arguments and return values * 134 - update changelog * 134 - delete self return type --------- Co-authored-by: alutskevich --- CHANGELOG.md | 5 + MatomoTracker.php | 524 ++++++++++++++++++++++++++-------------------- 2 files changed, 301 insertions(+), 228 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5afde19..0c9fa36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. +## Matomo PHP Tracker 3.4.0 +### Changed +- a lot of arguments of `MatomoTracker` methods have explicitly types +- a lot of `MatomoTracker` method return types have strict types + ## Matomo PHP Tracker 3.3.1 ### Fixed - closed curl connection diff --git a/MatomoTracker.php b/MatomoTracker.php index 234c6bf..0e4f41b 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -210,7 +210,7 @@ class MatomoTracker * @param string $apiUrl "http://example.org/matomo/" or "http://matomo.example.org/" * If set, will overwrite MatomoTracker::$URL */ - public function __construct($idSite, $apiUrl = '') + public function __construct(int $idSite, string $apiUrl = '') { $this->idSite = $idSite; $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false; @@ -237,7 +237,7 @@ public function __construct($idSite, $apiUrl = '') $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); } - public function setApiUrl(string $url) + public function setApiUrl(string $url): void { self::$URL = $url; } @@ -248,12 +248,12 @@ public function setApiUrl(string $url) * It is recommended to only send UTF-8 data to Matomo. * If required though, you can also specify another charset using this function. * - * @param string $charset * @return $this */ - public function setPageCharset($charset = '') + public function setPageCharset(string $charset = '') { $this->pageCharset = $charset; + return $this; } @@ -263,9 +263,10 @@ public function setPageCharset($charset = '') * @param string $url Raw URL (not URL encoded) * @return $this */ - public function setUrl($url) + public function setUrl(string $url) { $this->pageUrl = $url; + return $this; } @@ -275,9 +276,10 @@ public function setUrl($url) * @param string $url Raw URL (not URL encoded) * @return $this */ - public function setUrlReferrer($url) + public function setUrlReferrer(string $url) { $this->urlReferrer = $url; + return $this; } @@ -290,7 +292,7 @@ public function setUrlReferrer($url) * @deprecated this metric is deprecated please use performance timings instead * @see setPerformanceTimings */ - public function setGenerationTime($timeMs) + public function setGenerationTime(int $timeMs) { return $this; } @@ -307,21 +309,28 @@ public function setGenerationTime($timeMs) * @param null|int $onload Onload time in ms (loadEventEnd – loadEventStart) * @return $this */ - public function setPerformanceTimings($network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null) - { + 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() + public function clearPerformanceTimings(): void { $this->networkTime = false; $this->serverTime = false; @@ -335,9 +344,10 @@ public function clearPerformanceTimings() * @deprecated * @ignore */ - public function setUrlReferer($url) + public function setUrlReferer(string $url) { $this->setUrlReferrer($url); + return $this; } @@ -356,13 +366,14 @@ public function setUrlReferer($url) * @throws Exception * @see function getAttributionInfo() in https://github.com/matomo-org/matomo/blob/master/js/matomo.js */ - public function setAttributionInfo($jsonEncoded) + 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; } @@ -377,16 +388,17 @@ public function setAttributionInfo($jsonEncoded) * @return $this * @throws Exception */ - public function setCustomVariable($id, $name, $value, $scope = 'visit') - { - if (!is_int($id)) { - throw new Exception("Parameter id to setCustomVariable should be an integer"); - } - if ($scope == 'page') { + public function setCustomVariable( + int $id, + string $name, + string $value, + string $scope = 'visit' + ) { + if ($scope === 'page') { $this->pageCustomVar[$id] = array($name, $value); - } elseif ($scope == 'event') { + } elseif ($scope === 'event') { $this->eventCustomVar[$id] = array($name, $value); - } elseif ($scope == 'visit') { + } elseif ($scope === 'visit') { $this->visitorCustomVar[$id] = array($name, $value); } else { throw new Exception("Invalid 'scope' parameter value"); @@ -407,28 +419,29 @@ public function setCustomVariable($id, $name, $value, $scope = 'visit') * @return mixed An array with this format: array( 0 => CustomVariableName, 1 => CustomVariableValue ) or false * @see matomo.js getCustomVariable() */ - public function getCustomVariable($id, $scope = 'visit') + public function getCustomVariable(int $id, string $scope = 'visit') { - if ($scope == 'page') { - return isset($this->pageCustomVar[$id]) ? $this->pageCustomVar[$id] : false; - } elseif ($scope == 'event') { - return isset($this->eventCustomVar[$id]) ? $this->eventCustomVar[$id] : false; - } else { - if ($scope != 'visit') { - throw new Exception("Invalid 'scope' parameter value"); - } + 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_int($id)) { - throw new Exception("Parameter to getCustomVariable should be an integer"); - } + if (!is_array($cookieDecoded) || !isset($cookieDecoded[$id]) || !is_array($cookieDecoded[$id]) - || count($cookieDecoded[$id]) != 2 + || count($cookieDecoded[$id]) !== 2 ) { return false; } @@ -442,7 +455,7 @@ public function getCustomVariable($id, $scope = 'visit') * This can be useful when you have enabled bulk requests, * and you wish to clear Custom Variables of 'visit' scope. */ - public function clearCustomVariables() + public function clearCustomVariables(): void { $this->visitorCustomVar = []; $this->pageCustomVar = []; @@ -456,16 +469,17 @@ public function clearCustomVariables() * @param string $value value for custom dimension * @return $this */ - public function setCustomDimension($id, $value) + public function setCustomDimension(int $id, string $value) { - $this->customDimensions['dimension'.(int)$id] = $value; + $this->customDimensions['dimension'.$id] = $value; + return $this; } /** * Clears all previously set custom dimensions */ - public function clearCustomDimensions() + public function clearCustomDimensions(): void { $this->customDimensions = []; } @@ -476,9 +490,9 @@ public function clearCustomDimensions() * @param int $id id of custom dimension * @return string|null */ - public function getCustomDimension($id) + public function getCustomDimension(int $id): ?string { - return $this->customDimensions['dimension'.(int)$id] ?? null; + return $this->customDimensions['dimension'.$id] ?? null; } /** @@ -491,25 +505,27 @@ public function getCustomDimension($id) * @return $this * @throws Exception */ - public function setCustomTrackingParameter($trackingApiParameter, $value) + 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() + public function clearCustomTrackingParameters(): void { - $this->customParameters = array(); + $this->customParameters = []; } /** @@ -521,18 +537,19 @@ 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. * - * @param int $idSite * @return $this */ - public function setIdSite($idSite) + public function setIdSite(int $idSite) { $this->idSite = $idSite; + return $this; } @@ -542,9 +559,10 @@ public function setIdSite($idSite) * @param string $acceptLanguage For example "fr-fr" * @return $this */ - public function setBrowserLanguage($acceptLanguage) + public function setBrowserLanguage(string $acceptLanguage) { $this->acceptLanguage = $acceptLanguage; + return $this; } @@ -555,9 +573,10 @@ public function setBrowserLanguage($acceptLanguage) * @param string $userAgent * @return $this */ - public function setUserAgent($userAgent) + public function setUserAgent(string $userAgent) { $this->userAgent = $userAgent; + return $this; } @@ -570,15 +589,20 @@ public function setUserAgent($userAgent) * @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 $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|array $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($model = '', $platform = '', $platformVersion = '', $fullVersionList = '', $uaFullVersion = '') - { + public function setClientHints( + string $model = '', + string $platform = '', + string $platformVersion = '', + $fullVersionList = '', + string $uaFullVersion = '' + ) { if (is_string($fullVersionList)) { $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; $list = []; @@ -609,12 +633,13 @@ public function setClientHints($model = '', $platform = '', $platformVersion = ' * using either the visitor's IP address or language. * * Allowed only for Admin/Super User, must be used along with setTokenAuth(). - * @param string $country + * * @return $this */ - public function setCountry($country) + public function setCountry(string $country) { $this->country = $country; + return $this; } @@ -623,12 +648,13 @@ public function setCountry($country) * using the visitor's IP address (if configured to do so). * * Allowed only for Admin/Super User, must be used along with setTokenAuth(). - * @param string $region + * * @return $this */ - public function setRegion($region) + public function setRegion(string $region) { $this->region = $region; + return $this; } @@ -637,12 +663,13 @@ public function setRegion($region) * using the visitor's IP address (if configured to do so). * * Allowed only for Admin/Super User, must be used along with setTokenAuth(). - * @param string $city + * * @return $this */ - public function setCity($city) + public function setCity(string $city) { $this->city = $city; + return $this; } @@ -651,12 +678,13 @@ public function setCity($city) * latitude using the visitor's IP address (if configured to do so). * * Allowed only for Admin/Super User, must be used along with setTokenAuth(). - * @param float $lat + * * @return $this */ - public function setLatitude($lat) + public function setLatitude(float $lat) { $this->lat = $lat; + return $this; } @@ -665,21 +693,21 @@ public function setLatitude($lat) * longitude using the visitor's IP address (if configured to do so). * * Allowed only for Admin/Super User, must be used along with setTokenAuth(). - * @param float $long + * * @return $this */ - public function setLongitude($long) + 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() + public function enableBulkTracking(): void { $this->doBulkRequests = true; } @@ -687,9 +715,8 @@ public function enableBulkTracking() /** * 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() + public function disableBulkTracking(): void { $this->doBulkRequests = false; } @@ -704,8 +731,13 @@ public function disableBulkTracking() * @param bool $httpOnly (optional) Set HTTPOnly flag for cookies * @param string $sameSite (optional) Set SameSite flag for cookies */ - public function enableCookies($domain = '', $path = '/', $secure = false, $httpOnly = false, $sameSite = '') - { + 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; @@ -717,7 +749,7 @@ public function enableCookies($domain = '', $path = '/', $secure = false, $httpO /** * If image response is disabled Matomo will respond with a HTTP 204 header instead of responding with a gif. */ - public function disableSendImageResponse() + public function disableSendImageResponse(): void { $this->sendImageResponse = false; } @@ -744,15 +776,16 @@ protected static function domainFixup($domain) /** * Get cookie name with prefix and domain hash - * @param string $cookieName - * @return string */ - protected function getCookieName($cookieName) + 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 + ($this->configCookieDomain === '' + ? self::getCurrentHost() + : $this->configCookieDomain + ) . $this->configCookiePath ), 0, 4 @@ -767,7 +800,7 @@ protected function getCookieName($cookieName) * @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($documentTitle) + public function doTrackPageView(string $documentTitle) { if (!$this->idPageviewSetManually) { $this->generateNewPageviewId(); @@ -781,10 +814,8 @@ public function doTrackPageView($documentTitle) /** * 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). - * - * @param string $idPageview */ - public function setPageviewId($idPageview) + public function setPageviewId(string $idPageview): void { $this->idPageview = $idPageview; $this->idPageviewSetManually = true; @@ -795,14 +826,14 @@ public function setPageviewId($idPageview) * 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 mixed The PageView id as string or false if there is none yet. + * @return string|false The PageView id as string or false if there is none yet. */ public function getPageviewId() { return $this->idPageview; } - private function generateNewPageviewId() + private function generateNewPageviewId(): void { $this->idPageview = substr(md5(uniqid(rand(), true)), 0, 6); } @@ -816,8 +847,12 @@ private function generateNewPageviewId() * @param float|bool $value (optional) The Event's value * @return mixed Response string or true if using bulk requests. */ - public function doTrackEvent($category, $action, $name = false, $value = false) - { + public function doTrackEvent( + string $category, + string $action, + $name = false, + $value = false + ) { $url = $this->getUrlTrackEvent($category, $action, $name, $value); return $this->sendRequest($url); @@ -831,8 +866,11 @@ public function doTrackEvent($category, $action, $name = false, $value = false) * @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($contentName, $contentPiece = 'Unknown', $contentTarget = false) - { + public function doTrackContentImpression( + string $contentName, + string $contentPiece = 'Unknown', + $contentTarget = false + ) { $url = $this->getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget); return $this->sendRequest($url); @@ -849,12 +887,11 @@ public function doTrackContentImpression($contentName, $contentPiece = 'Unknown' * @return mixed Response string or true if using bulk requests. */ public function doTrackContentInteraction( - $interaction, - $contentName, - $contentPiece = 'Unknown', + string $interaction, + string $contentName, + string $contentPiece = 'Unknown', $contentTarget = false - ) - { + ) { $url = $this->getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget); return $this->sendRequest($url); @@ -870,8 +907,11 @@ public function doTrackContentInteraction( * * @return mixed Response or true if using bulk requests. */ - public function doTrackSiteSearch($keyword, $category = '', $countResults = false) - { + public function doTrackSiteSearch( + string $keyword, + string $category = '', + $countResults = false + ) { $url = $this->getUrlTrackSiteSearch($keyword, $category, $countResults); return $this->sendRequest($url); @@ -884,7 +924,7 @@ public function doTrackSiteSearch($keyword, $category = '', $countResults = fals * @param float $revenue Revenue for this conversion * @return mixed Response or true if using bulk request */ - public function doTrackGoal($idGoal, $revenue = 0.0) + public function doTrackGoal(int $idGoal, float $revenue = 0.0) { $url = $this->getUrlTrackGoal($idGoal, $revenue); @@ -898,7 +938,7 @@ public function doTrackGoal($idGoal, $revenue = 0.0) * @param string $actionType Type of the action: 'download' or 'link' * @return mixed Response or true if using bulk request */ - public function doTrackAction($actionUrl, $actionType) + 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); @@ -922,8 +962,13 @@ public function doTrackAction($actionUrl, $actionType) * @throws Exception * @return $this */ - public function addEcommerceItem($sku, $name = '', $category = '', $price = 0.0, $quantity = 1) - { + 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"); } @@ -931,6 +976,7 @@ public function addEcommerceItem($sku, $name = '', $category = '', $price = 0.0, $price = $this->forceDotAsSeparatorForDecimalPoint($price); $this->ecommerceItems[] = array($sku, $name, $category, $price, $quantity); + return $this; } @@ -944,7 +990,7 @@ public function addEcommerceItem($sku, $name = '', $category = '', $price = 0.0, * @param float $grandTotal Cart grandTotal (typically the sum of all items' prices) * @return mixed Response or true if using bulk request */ - public function doTrackEcommerceCartUpdate($grandTotal) + public function doTrackEcommerceCartUpdate(float $grandTotal) { $url = $this->getUrlTrackEcommerceCartUpdate($grandTotal); @@ -968,7 +1014,7 @@ public function doBulkTrack() ); } - $data = array('requests' => $this->storedTrackingActions); + $data = ['requests' => $this->storedTrackingActions]; // token_auth is not required by default, except if bulk_requests_require_authentication=1 if (!empty($this->token_auth)) { @@ -978,7 +1024,7 @@ public function doBulkTrack() $postData = json_encode($data); $response = $this->sendRequest($this->getBaseUrl(), 'POST', $postData, $force = true); - $this->storedTrackingActions = array(); + $this->storedTrackingActions = []; return $response; } @@ -1002,13 +1048,12 @@ public function doBulkTrack() */ public function doTrackEcommerceOrder( $orderId, - $grandTotal, - $subTotal = 0.0, - $tax = 0.0, - $shipping = 0.0, - $discount = 0.0 - ) - { + 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); @@ -1017,20 +1062,20 @@ public function doTrackEcommerceOrder( /** * Tracks a PHP Throwable a crash (requires CrashAnalytics to be enabled in the target Matomo) * - * @param Throwable $ex (required) the throwable to track. The message, stack trace, file location and line number + * @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 $ex, $category = null) + public function doTrackPhpThrowable(Throwable $throwable, ?string $category = null) { - $message = $ex->getMessage(); - $stack = $ex->getTraceAsString(); - $type = get_class($ex); - $location = $ex->getFile(); - $line = $ex->getLine(); + $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); } @@ -1048,8 +1093,15 @@ public function doTrackPhpThrowable(\Throwable $ex, $category = null) * @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($message, $type = null, $category = null, $stack = null, $location = null, $line = null, $column = null) - { + 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); @@ -1089,8 +1141,12 @@ public function doPing() * @param float $price Specify the price at which the item was displayed * @return $this */ - public function setEcommerceView($sku = '', $name = '', $category = '', $price = 0.0) - { + public function setEcommerceView( + string $sku = '', + string $name = '', + $category = '', + float $price = 0.0 + ) { $this->ecommerceView = []; if (!empty($category)) { @@ -1103,7 +1159,6 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = $this->ecommerceView['_pkc'] = $category; if (!empty($price)) { - $price = (float)$price; $price = $this->forceDotAsSeparatorForDecimalPoint($price); $this->ecommerceView['_pkp'] = $price; } @@ -1116,9 +1171,10 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = $this->ecommerceView['_pks'] = $sku; } if (empty($name)) { - $name = ""; + $name = ''; } $this->ecommerceView['_pkn'] = $name; + return $this; } @@ -1127,9 +1183,8 @@ public function setEcommerceView($sku = '', $name = '', $category = '', $price = * If for instance a German locale is used it would be a comma otherwise. * * @param float|string $value - * @return string */ - private function forceDotAsSeparatorForDecimalPoint($value) + private function forceDotAsSeparatorForDecimalPoint($value): string { if (null === $value || false === $value) { return $value; @@ -1146,9 +1201,7 @@ private function forceDotAsSeparatorForDecimalPoint($value) */ public function getUrlTrackEcommerceCartUpdate($grandTotal) { - $url = $this->getUrlTrackEcommerce($grandTotal); - - return $url; + return $this->getUrlTrackEcommerce($grandTotal); } /** @@ -1164,8 +1217,7 @@ public function getUrlTrackEcommerceOrder( $tax = 0.0, $shipping = 0.0, $discount = 0.0 - ) - { + ) { if (empty($orderId)) { throw new Exception("You must specifiy an orderId for the Ecommerce order"); } @@ -1226,7 +1278,7 @@ protected function getUrlTrackEcommerce($grandTotal, $subTotal = 0.0, $tax = 0.0 * @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($documentTitle = '') + public function getUrlTrackPageView(string $documentTitle = ''): string { $url = $this->getRequest($this->idSite); if (strlen($documentTitle) > 0) { @@ -1247,13 +1299,17 @@ public function getUrlTrackPageView($documentTitle = '') * @return string URL to matomo.php with all parameters set to track the pageview * @throws */ - public function getUrlTrackEvent($category, $action, $name = false, $value = false) - { + public function getUrlTrackEvent( + string $category, + string $action, + $name = false, + $value = false + ): string { $url = $this->getRequest($this->idSite); - if (strlen($category) == 0) { + if (strlen($category) === 0) { throw new Exception("You must specify an Event Category name (Music, Videos, Games...)."); } - if (strlen($action) == 0) { + if (strlen($action) === 0) { throw new Exception("You must specify an Event action (click, view, add...)."); } @@ -1281,11 +1337,14 @@ public function getUrlTrackEvent($category, $action, $name = false, $value = fal * @throws Exception In case $contentName is empty * @return string URL to matomo.php with all parameters set to track the pageview */ - public function getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget) - { + public function getUrlTrackContentImpression( + string $contentName, + string $contentPiece, + $contentTarget + ): string { $url = $this->getRequest($this->idSite); - if (strlen($contentName) == 0) { + if (strlen($contentName) === 0) { throw new Exception("You must specify a content name"); } @@ -1312,15 +1371,19 @@ public function getUrlTrackContentImpression($contentName, $contentPiece, $conte * @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($interaction, $contentName, $contentPiece, $contentTarget) - { + public function getUrlTrackContentInteraction( + string $interaction, + string $contentName, + string $contentPiece, + $contentTarget + ): string { $url = $this->getRequest($this->idSite); - if (strlen($interaction) == 0) { + if (strlen($interaction) === 0) { throw new Exception("You must specify a name for the interaction"); } - if (strlen($contentName) == 0) { + if (strlen($contentName) === 0) { throw new Exception("You must specify a content name"); } @@ -1341,12 +1404,8 @@ public function getUrlTrackContentInteraction($interaction, $contentName, $conte * Builds URL to track a site search. * * @see doTrackSiteSearch() - * @param string $keyword - * @param string $category - * @param int $countResults - * @return string */ - public function getUrlTrackSiteSearch($keyword, $category, $countResults) + public function getUrlTrackSiteSearch(string $keyword, string $category, int $countResults): string { $url = $this->getRequest($this->idSite); $url .= '&search=' . urlencode($keyword); @@ -1368,7 +1427,7 @@ public function getUrlTrackSiteSearch($keyword, $category, $countResults) * @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($idGoal, $revenue = 0.0) + public function getUrlTrackGoal(int $idGoal, float $revenue = 0.0): string { $url = $this->getRequest($this->idSite); $url .= '&idgoal=' . $idGoal; @@ -1388,7 +1447,7 @@ public function getUrlTrackGoal($idGoal, $revenue = 0.0) * @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($actionUrl, $actionType) + public function getUrlTrackAction(string $actionUrl, string $actionType): string { $url = $this->getRequest($this->idSite); $url .= '&' . $actionType . '=' . urlencode($actionUrl); @@ -1410,8 +1469,15 @@ public function getUrlTrackAction($actionUrl, $actionType) * @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($message, $type = null, $category = null, $stack = null, $location = null, $line = null, $column = null) - { + 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) { @@ -1447,9 +1513,10 @@ public function getUrlTrackCrash($message, $type = null, $category = null, $stac * 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($dateTime) + public function setForceVisitDateTime(string $dateTime) { $this->forcedDatetime = $dateTime; + return $this; } @@ -1463,6 +1530,7 @@ public function setForceVisitDateTime($dateTime) public function setForceNewVisit() { $this->forcedNewVisit = true; + return $this; } @@ -1474,9 +1542,10 @@ public function setForceNewVisit() * @param string $ip IP string, eg. 130.54.2.1 * @return $this */ - public function setIp($ip) + public function setIp(string $ip) { $this->ip = $ip; + return $this; } @@ -1489,12 +1558,13 @@ public function setIp($ip) * @return $this * @throws Exception */ - public function setUserId($userId) + public function setUserId(string $userId) { if ($userId === '') { throw new Exception("User ID cannot be empty."); } $this->userId = $userId; + return $this; } @@ -1503,10 +1573,9 @@ public function setUserId($userId) * * Note: matches implementation of Tracker\Request->getUserIdHashed() * - * @param $id * @return string */ - public static function getUserIdHashed($id) + public static function getUserIdHashed($id): string { return substr(sha1($id), 0, 16); } @@ -1523,10 +1592,10 @@ public static function getUserIdHashed($id) * @return $this * @throws Exception */ - public function setVisitorId($visitorId) + public function setVisitorId(string $visitorId) { $hexChars = '01234567890abcdefABCDEF'; - if (strlen($visitorId) != self::LENGTH_VISITOR_ID + if (strlen($visitorId) !== self::LENGTH_VISITOR_ID || strspn($visitorId, $hexChars) !== strlen($visitorId) ) { throw new Exception( @@ -1538,6 +1607,7 @@ public function setVisitorId($visitorId) ); } $this->forcedVisitorId = $visitorId; + return $this; } @@ -1599,14 +1669,14 @@ public function getUserId() * * @return bool True if cookie exists and is valid, False otherwise */ - protected function loadVisitorIdCookie() + protected function loadVisitorIdCookie(): bool { $idCookie = $this->getCookieMatchingName('id'); if ($idCookie === false) { return false; } $parts = explode('.', $idCookie); - if (strlen($parts[0]) != self::LENGTH_VISITOR_ID) { + if (strlen($parts[0]) !== self::LENGTH_VISITOR_ID) { return false; } @@ -1621,7 +1691,7 @@ protected function loadVisitorIdCookie() /** * Deletes all first party cookies from the client */ - public function deleteCookies() + public function deleteCookies(): void { $cookies = array('id', 'ses', 'cvar', 'ref'); foreach ($cookies as $cookie) { @@ -1659,9 +1729,10 @@ public function getAttributionInfo() * @param string $token_auth token_auth 32 chars token_auth string * @return $this */ - public function setTokenAuth($token_auth) + public function setTokenAuth(string $token_auth) { $this->token_auth = $token_auth; + return $this; } @@ -1671,12 +1742,13 @@ public function setTokenAuth($token_auth) * @param string $time HH:MM:SS format * @return $this */ - public function setLocalTime($time) + public function setLocalTime(string $time) { - list($hour, $minute, $second) = explode(':', $time); + [$hour, $minute, $second] = explode(':', $time); $this->localHour = (int)$hour; $this->localMinute = (int)$minute; $this->localSecond = (int)$second; + return $this; } @@ -1687,10 +1759,11 @@ public function setLocalTime($time) * @param int $height * @return $this */ - public function setResolution($width, $height) + public function setResolution(int $width, int $height) { $this->width = $width; $this->height = $height; + return $this; } @@ -1698,48 +1771,41 @@ public function setResolution($width, $height) * Sets if the browser supports cookies * This is reported in "List of plugins" report in Matomo. * - * @param bool $bool * @return $this */ - public function setBrowserHasCookies($bool) + public function setBrowserHasCookies(bool $hasCookies) { - $this->hasCookies = $bool; + $this->hasCookies = $hasCookies; + return $this; } /** * Will append a custom string at the end of the Tracking request. - * @param string $string + * * @return $this */ - public function setDebugStringAppend($string) + public function setDebugStringAppend(string $debugString) { - $this->DEBUG_APPEND_URL = '&' . $string; + $this->DEBUG_APPEND_URL = '&' . $debugString; + return $this; } /** * Sets visitor browser supported plugins * - * @param bool $flash - * @param bool $java - * @param bool $quickTime - * @param bool $realPlayer - * @param bool $pdf - * @param bool $windowsMedia - * @param bool $silverlight * @return $this */ public function setPlugins( - $flash = false, - $java = false, - $quickTime = false, - $realPlayer = false, - $pdf = false, - $windowsMedia = false, - $silverlight = false - ) - { + 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 . @@ -1748,6 +1814,7 @@ public function setPlugins( '&pdf=' . (int)$pdf . '&wma=' . (int)$windowsMedia . '&ag=' . (int)$silverlight; + return $this; } @@ -1756,7 +1823,7 @@ public function setPlugins( * from the request and write updated cookies in the response (using setrawcookie). * This can be disabled by calling this function. */ - public function disableCookieSupport() + public function disableCookieSupport(): void { $this->configCookiesDisabled = true; } @@ -1765,7 +1832,7 @@ public function disableCookieSupport() * Returns the maximum number of seconds the tracker will spend waiting for a response * from Matomo. Defaults to 600 seconds. */ - public function getRequestTimeout() + public function getRequestTimeout(): int { return $this->requestTimeout; } @@ -1774,17 +1841,17 @@ public function getRequestTimeout() * Sets the maximum number of seconds that the tracker will spend waiting for a response * from Matomo. * - * @param int $timeout * @return $this * @throws Exception */ - public function setRequestTimeout($timeout) + public function setRequestTimeout(int $timeout) { - if (!is_int($timeout) || $timeout < 0) { + if ($timeout < 0) { throw new Exception("Invalid value supplied for request timeout: $timeout"); } $this->requestTimeout = $timeout; + return $this; } @@ -1792,7 +1859,7 @@ public function setRequestTimeout($timeout) * Returns the maximum number of seconds the tracker will spend trying to connect to Matomo. * Defaults to 300 seconds. */ - public function getRequestConnectTimeout() + public function getRequestConnectTimeout(): int { return $this->requestConnectTimeout; } @@ -1804,13 +1871,14 @@ public function getRequestConnectTimeout() * @return $this * @throws Exception */ - public function setRequestConnectTimeout($timeout) + public function setRequestConnectTimeout(int $timeout) { - if (!is_int($timeout) || $timeout < 0) { + if ($timeout < 0) { throw new Exception("Invalid value supplied for request connect timeout: $timeout"); } $this->requestConnectTimeout = $timeout; + return $this; } @@ -1823,18 +1891,18 @@ public function setRequestConnectTimeout($timeout) * @param string $method Either 'POST' or 'GET' * @return $this */ - public function setRequestMethodNonBulk($method) + 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" - * @param int $proxyPort */ - public function setProxy($proxy, $proxyPort = 80) + public function setProxy(string $proxy, int $proxyPort = 80): void { $this->proxy = $proxy; $this->proxyPort = $proxyPort; @@ -1844,7 +1912,7 @@ public function setProxy($proxy, $proxyPort = 80) * 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() + private function getProxy(): ?string { if (isset($this->proxy) && isset($this->proxyPort)) { return $this->proxy.":".$this->proxyPort; @@ -1861,20 +1929,26 @@ private function getProxy() /** * Returns array of curl options for request + * + * @return array */ - protected function prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded) - { - $options = array( + 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 => array( + CURLOPT_HTTPHEADER => [ 'Accept-Language: ' . $this->acceptLanguage, - ), - ); + ], + ]; if ($method === 'GET') { $options[CURLOPT_FOLLOWLOCATION] = true; @@ -1922,17 +1996,19 @@ protected function prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded /** * Returns array of stream options for request + * + * @return array{http: array} */ - protected function prepareStreamOptions($method, $data, $forcePostUrlEncoded) + protected function prepareStreamOptions(string $method, $data, bool $forcePostUrlEncoded): array { - $stream_options = array( - 'http' => 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)) { @@ -1959,7 +2035,7 @@ protected function prepareStreamOptions($method, $data, $forcePostUrlEncoded) /** * @ignore */ - protected function sendRequest($url, $method = 'GET', $data = null, $force = false) + protected function sendRequest(string $url, string $method = 'GET', $data = null, bool $force = false): string { self::$DEBUG_LAST_REQUESTED_URL = $url; @@ -2073,7 +2149,7 @@ protected function getTimestamp() /** * Returns the base URL for the Matomo server. */ - protected function getBaseUrl() + protected function getBaseUrl(): string { if (empty(self::$URL)) { throw new Exception( @@ -2094,7 +2170,7 @@ protected function getBaseUrl() /** * @ignore */ - protected function getRequest($idSite) + protected function getRequest(int $idSite): string { $this->setFirstPartyCookies(); @@ -2198,9 +2274,9 @@ protected function getRequest($idSite) } // Reset page level custom variables after this page view - $this->ecommerceView = array(); - $this->pageCustomVar = array(); - $this->eventCustomVar = array(); + $this->ecommerceView = []; + $this->pageCustomVar = []; + $this->eventCustomVar = []; $this->clearCustomDimensions(); $this->clearCustomTrackingParameters(); @@ -2214,11 +2290,10 @@ protected function getRequest($idSite) /** * Returns a first party cookie which name contains $name * - * @param string $name * @return string String value of cookie, or false if not found * @ignore */ - protected function getCookieMatchingName($name) + protected function getCookieMatchingName(string $name) { if ($this->configCookiesDisabled) { return false; @@ -2244,10 +2319,9 @@ protected function getCookieMatchingName($name) * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "/dir1/dir2/index.php" * - * @return string * @ignore */ - protected static function getCurrentScriptName() + protected static function getCurrentScriptName(): string { $url = ''; if (!empty($_SERVER['PATH_INFO'])) { @@ -2281,10 +2355,10 @@ protected static function getCurrentScriptName() * @return string 'https' or 'http' * @ignore */ - protected static function getCurrentScheme() + protected static function getCurrentScheme(): string { if (isset($_SERVER['HTTPS']) - && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true) + && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === true) ) { return 'https'; } @@ -2296,10 +2370,9 @@ protected static function getCurrentScheme() * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "http://example.org" * - * @return string * @ignore */ - protected static function getCurrentHost() + protected static function getCurrentHost(): string { if (isset($_SERVER['HTTP_HOST'])) { return $_SERVER['HTTP_HOST']; @@ -2312,10 +2385,9 @@ protected static function getCurrentHost() * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "?param1=value1¶m2=value2" * - * @return string * @ignore */ - protected static function getCurrentQueryString() + protected static function getCurrentQueryString(): string { $url = ''; if (isset($_SERVER['QUERY_STRING']) @@ -2330,10 +2402,9 @@ protected static function getCurrentQueryString() /** * Returns the current full URL (scheme, host, path and query string. * - * @return string * @ignore */ - protected static function getCurrentUrl() + protected static function getCurrentUrl(): string { return self::getCurrentScheme() . '://' . self::getCurrentHost() @@ -2379,12 +2450,9 @@ protected function setFirstPartyCookies() * * This replicates the matomo.js tracker algorithms for consistency and better accuracy. * - * @param $cookieName - * @param $cookieValue - * @param $cookieTTL * @return $this */ - protected function setCookie($cookieName, $cookieValue, $cookieTTL) + protected function setCookie(string $cookieName, $cookieValue, int $cookieTTL) { $cookieExpire = $this->currentTs + $cookieTTL; if (!headers_sent()) { @@ -2411,7 +2479,7 @@ protected function getCustomVariablesFromCookie() return []; } - return json_decode($cookie, $assoc = true); + return json_decode($cookie, true); } /** @@ -2451,9 +2519,9 @@ public function getIncomingTrackerCookie($name) * * @param array $headers Array with HTTP response headers as values */ - protected function parseIncomingCookies($headers) + protected function parseIncomingCookies(array $headers): void { - $this->incomingTrackerCookies = array(); + $this->incomingTrackerCookies = []; if (!empty($headers)) { $headerName = 'set-cookie:'; From cf6327a9c986ec4094a4f777cdbcc6f9202a981c Mon Sep 17 00:00:00 2001 From: Mathias Brodala Date: Wed, 22 Jan 2025 14:59:09 +0100 Subject: [PATCH 106/115] Exclude development resources from dist archive (#138) Neither Git(hub) nor PHPUnit resources are necessary on production. --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 05bc5a6..a05f94a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ +.git* export-ignore +phpunit* export-ignore tests/ export-ignore run_tests.sh export-ignore From 9ddccf000b2d1f8cd09d97042729b7c425287a37 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Tue, 2 Dec 2025 15:46:41 +0100 Subject: [PATCH 107/115] Fix deprecation notice for $http_response_header (#143) fixes #142 --- MatomoTracker.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 0e4f41b..d3cb209 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2129,6 +2129,10 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $response = file_get_contents($url, 0, $ctx); $content = $response; + if (function_exists('http_get_last_response_headers')) { + $http_response_header = http_get_last_response_headers(); + } + $this->parseIncomingCookies($http_response_header); } From b029aa2b8dcc113ddcff575a127010a5585321a2 Mon Sep 17 00:00:00 2001 From: George Steel Date: Fri, 12 Dec 2025 12:39:49 +0000 Subject: [PATCH 108/115] Declare supported PHP versions explicitly and run tests on all supported versions (#147) * Declare supported PHP versions explicitly and run tests on all supported versions * Apply suggestion from @sgiehl --------- Co-authored-by: Stefan Giehl --- .github/workflows/phpunit.yml | 38 ++--------------------------------- composer.json | 2 +- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d68cd40..fbcb7de 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -23,42 +23,8 @@ jobs: runs-on: ${{ matrix.operating-system }} strategy: matrix: - operating-system: [ubuntu-latest, windows-latest] - php-version: ['7.2', '8.3'] - include: - - php-version: 7.3 - operating-system: ubuntu-latest - - php-version: 7.4 - operating-system: ubuntu-latest - - php-version: 8.0 - operating-system: ubuntu-latest - - php-version: 8.1 - operating-system: ubuntu-latest - - php-version: 8.2 - operating-system: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer:v2 - extensions: memcached - - name: "Composer install" - run: | - composer install --prefer-dist - - name: PHPUnit / PHP ${{ matrix.php-version }} - run: | - php -v - ./vendor/bin/phpunit - - build2: - name: PHPUnit - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: [ macOS-latest ] - php-version: [ '7.2', '8.3' ] + operating-system: [ubuntu-latest, windows-latest, macOS-latest] + php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v2 - name: Install PHP diff --git a/composer.json b/composer.json index f07c24e..c740a25 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "^7.2 || ^8.0", + "php": "~7.2 || ~7.3 || ~7.4 || ~8.0 || ~8.1 || ~8.2 || ~8.3 || ~8.4 || ~8.5", "ext-json": "*" }, "suggest": { From 9462dc6eb718c711545ea1b0f590b9ae892a4212 Mon Sep 17 00:00:00 2001 From: Andrii Lutskevych Date: Sat, 20 Dec 2025 19:55:41 +0100 Subject: [PATCH 109/115] Fix: Not possible to create multiple Piwik tracker instances having different API urls (#145) * Allow setting form factors client hint (#136) * Allow settting form factors client hint * update changelog * Fix handling of some attributes * Apply review feedback Co-authored-by: Michal Kleiner --------- Co-authored-by: Michal Kleiner * deprecate $URL * use old $URL if apiUrl is empty --------- Co-authored-by: Stefan Giehl Co-authored-by: Michal Kleiner Co-authored-by: Andrii Lutskevych --- CHANGELOG.md | 10 +++++++ MatomoTracker.php | 48 +++++++++++++++++++++++++------- tests/Unit/MatomoTrackerTest.php | 9 ++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c9fa36..880e1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,19 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or ## Matomo PHP Tracker 3.4.0 ### Changed + +- Fixed PHP 8.5 deprecation notice +- static `$URL` is deprecated - a lot of arguments of `MatomoTracker` methods have explicitly types - a lot of `MatomoTracker` method return types have strict types +### Added +- new private property `apiUrl` for storing API URL + +## Matomo PHP Tracker 3.3.2 +### Changed +- Support for formFactors client hint parameter, supported as of Matomo 5.2.0 + ## Matomo PHP Tracker 3.3.1 ### Fixed - closed curl connection diff --git a/MatomoTracker.php b/MatomoTracker.php index d3cb209..d2ce799 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -28,6 +28,7 @@ class MatomoTracker * MatomoTracker::$URL = 'http://yourwebsite.org/matomo/'; * * @var string + * @deprecated */ static public $URL = ''; @@ -202,6 +203,8 @@ class MatomoTracker private $requestMethod = null; + private $apiUrl = ''; + /** * Builds a MatomoTracker object, used to track visits, pages and Goal conversions * for a specific website, by using the Matomo Tracking API. @@ -223,10 +226,12 @@ public function __construct(int $idSite, string $apiUrl = '') !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'] : '' + !empty($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION']) ? $_SERVER['HTTP_SEC_CH_UA_FULL_VERSION'] : '', + !empty($_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS']) ? $_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS'] : '' ); if (!empty($apiUrl)) { self::$URL = $apiUrl; + $this->apiUrl = $apiUrl; } $this->setNewVisitorId(); @@ -240,6 +245,7 @@ public function __construct(int $idSite, string $apiUrl = '') public function setApiUrl(string $url): void { self::$URL = $url; + $this->apiUrl = $url; } /** @@ -593,6 +599,8 @@ public function setUserAgent(string $userAgent) * 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' + * @param string|array $formFactors Value of the header 'HTTP_SEC_CH_UA_FORM_FACTORS' + * or an array containing all form factors with structure ["Desktop", "XR"] * * @return $this */ @@ -601,7 +609,8 @@ public function setClientHints( string $platform = '', string $platformVersion = '', $fullVersionList = '', - string $uaFullVersion = '' + string $uaFullVersion = '', + $formFactors = '' ) { if (is_string($fullVersionList)) { $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; @@ -617,12 +626,25 @@ public function setClientHints( $fullVersionList = []; } + if (is_string($formFactors)) { + $formFactors = explode(',', $formFactors); + $formFactors = array_filter(array_map( + function ($item) { + return trim($item, '" '); + }, + $formFactors + )); + } elseif (!is_array($formFactors)) { + $formFactors = []; + } + $this->clientHints = array_filter([ 'model' => $model, 'platform' => $platform, 'platformVersion' => $platformVersion, 'uaFullVersion' => $uaFullVersion, 'fullVersionList' => $fullVersionList, + 'formFactors' => $formFactors, ]); return $this; @@ -810,7 +832,7 @@ public function doTrackPageView(string $documentTitle) 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). @@ -2097,7 +2119,7 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null curl_setopt_array($ch, $options); ob_start(); $response = @curl_exec($ch); - + try { $header = ''; @@ -2152,23 +2174,29 @@ protected function getTimestamp() /** * Returns the base URL for the Matomo server. + * + * @throws Exception */ protected function getBaseUrl(): string { - if (empty(self::$URL)) { + $apiUrl = $this->apiUrl === '' + ? self::$URL + : $this->apiUrl; + + if ($apiUrl === '') { 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 + if (strpos($apiUrl, '/matomo.php') === false + && strpos($apiUrl, '/proxy-matomo.php') === false ) { - self::$URL = rtrim(self::$URL, '/'); - self::$URL .= '/matomo.php'; + $apiUrl = rtrim($apiUrl, '/'); + $apiUrl .= '/matomo.php'; } - return self::$URL; + return $apiUrl; } /** diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 703b631..75b1669 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -84,4 +84,13 @@ public function test_setApiUrl() $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); } + + public function testUsageApiUrl(): void + { + $newApiUrl = 'https://NEW-API-URL.com'; + $tracker = new \MatomoTracker(1, $newApiUrl); + $url = $tracker->getUrlTrackPageView('test title'); + + $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); + } } \ No newline at end of file From 262acfdd6dab5e70e7c72b8bdbb2055be23a30d5 Mon Sep 17 00:00:00 2001 From: George Steel Date: Fri, 16 Jan 2026 16:19:23 +0000 Subject: [PATCH 110/115] Conditionally close the curl handle (#150) `curl_close` has no effect since PHP 8.0 and is deprecated since 8.5 https://www.php.net/curl_close --- MatomoTracker.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index d2ce799..b40606b 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2141,7 +2141,11 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $this->parseIncomingCookies(explode("\r\n", $header)); } finally { - curl_close($ch); + // curl_close has no effect since PHP 8.0 + if (PHP_VERSION_ID < 80000) { + curl_close($ch); + } + ob_end_clean(); } } elseif (function_exists('stream_context_create')) { From f2afbb8f15056fdd607384763b2addad2162215e Mon Sep 17 00:00:00 2001 From: dizzy Date: Fri, 24 Jul 2026 13:53:43 +0000 Subject: [PATCH 111/115] Add method to track an AI bot request, if the current user agent is a known AI bot (#148) * add methods to detect and track pageviews if the current user agent is an AI bot * add supported query parameters for AI bot tracking * add more tests for AI bot tracking methods * use recMode parameter * make user agent substrings array a public const and fill out missing php docs * remove phpstorm added spacing * update tests * set url in test * remove Devin user agent check Co-authored-by: Thomas ZILLIOX * Apply suggestions from code review Co-authored-by: Stefan Giehl --------- Co-authored-by: Thomas ZILLIOX Co-authored-by: Stefan Giehl --- MatomoTracker.php | 122 ++++++++++++++++++++++++++----- tests/Unit/MatomoTrackerTest.php | 86 ++++++++++++++++++++++ 2 files changed, 190 insertions(+), 18 deletions(-) diff --git a/MatomoTracker.php b/MatomoTracker.php index b40606b..989bdfe 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -32,6 +32,15 @@ class MatomoTracker */ static public $URL = ''; + public const AI_BOT_USER_AGENT_SUBSTRINGS = [ + 'ChatGPT-User', + 'MistralAI-User', + 'Gemini-Deep-Research', + 'Claude-User', + 'Perplexity-User', + 'Google-NotebookLM', + ]; + /** * API Version * @@ -161,11 +170,11 @@ class MatomoTracker // Visitor Ids in order public $userId = false; - + public $forcedVisitorId = false; - + public $cookieVisitorId = false; - + public $randomVisitorId = false; public $configCookiesDisabled = false; @@ -186,11 +195,11 @@ class MatomoTracker // Allow debug while blocking the request public $requestTimeout = 600; - + public $requestConnectTimeout = 300; - + public $doBulkRequests = false; - + public $storedTrackingActions = []; public $sendImageResponse = true; @@ -238,7 +247,7 @@ public function __construct(int $idSite, string $apiUrl = '') $this->currentTs = time(); $this->createTs = $this->currentTs; - + $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); } @@ -735,7 +744,7 @@ public function enableBulkTracking(): void } /** - * Disables the bulk request feature. Make sure to call `doBulkTrack()` before disabling it if you have stored + * 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 @@ -833,6 +842,29 @@ public function doTrackPageView(string $documentTitle) return $this->sendRequest($url); } + /** + * If the current user agent belongs to a known AI bot, tracks a pageview action. + * + * This method should be used server side to track AI bots that do not execute + * JavaScript. If the current user agent is not a known AI bot, nothing is tracked + * and null is returned. + * + * @param int|null $httpStatus the request's HTTP status code, if known. + * @param int|null $responseSizeBytes the size of the response sent to the AI bot, if known. + * @param int|null $serverTimeMs the number of milliseconds it took to process the request, if known. + * @param string|null $source the source/proxy that served the request (max 50 chars), if known. + * @return string|null Response string, or null if the current user agent is not a known AI bot. + */ + 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). @@ -847,7 +879,7 @@ public function setPageviewId(string $idPageview): void * 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() @@ -891,7 +923,7 @@ public function doTrackEvent( public function doTrackContentImpression( string $contentName, string $contentPiece = 'Unknown', - $contentTarget = false + $contentTarget = false ) { $url = $this->getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget); @@ -1215,6 +1247,40 @@ private function forceDotAsSeparatorForDecimalPoint($value): string return str_replace(',', '.', $value); } + /** + * Builds a URL to track a request from an AI bot. + * + * @param int|null $httpStatus the request's HTTP status code, if it is known. + * @param int|null $responseSizeBytes the size of the response sent to the AI bot, if known. + * @param int|null $serverTimeMs the number of milliseconds it took to process the request, if known. + * @param string|null $source the source/proxy that served the request (max 50 chars), if known. + * @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 ($httpStatus !== null) { + $url .= '&http_status=' . $httpStatus; + } + + if ($responseSizeBytes !== null) { + $url .= '&bw_bytes=' . $responseSizeBytes; + } + + if ($serverTimeMs !== null) { + $url .= '&pf_srv=' . $serverTimeMs; + } + + if ($source !== null && $source !== '') { + $url .= '&source=' . rawurlencode(substr($source, 0, 50)); + } + + return $url; + } + /** * Returns URL used to track Ecommerce Cart updates * Calling this function will reinitializes the property ecommerceItems to empty array @@ -1362,7 +1428,7 @@ public function getUrlTrackEvent( public function getUrlTrackContentImpression( string $contentName, string $contentPiece, - $contentTarget + $contentTarget ): string { $url = $this->getRequest($this->idSite); @@ -1876,7 +1942,7 @@ public function setRequestTimeout(int $timeout) return $this; } - + /** * Returns the maximum number of seconds the tracker will spend trying to connect to Matomo. * Defaults to 300 seconds. @@ -1904,7 +1970,7 @@ public function setRequestConnectTimeout(int $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, @@ -1957,7 +2023,7 @@ private function getProxy(): ?string protected function prepareCurlOptions( string $url, string $method, - $data, + $data, bool $forcePostUrlEncoded ): array { $options = [ @@ -2374,7 +2440,7 @@ protected static function getCurrentScriptName(): string if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) { $url = $_SERVER['SCRIPT_NAME']; } elseif (empty($url)) { - $url = '/'; + $url = '/'; } if (!empty($url) && $url[0] !== '/') { @@ -2443,9 +2509,9 @@ protected static function getCurrentQueryString(): string protected static function getCurrentUrl(): string { return self::getCurrentScheme() . '://' - . self::getCurrentHost() - . self::getCurrentScriptName() - . self::getCurrentQueryString(); + . self::getCurrentHost() + . self::getCurrentScriptName() + . self::getCurrentQueryString(); } /** @@ -2576,6 +2642,26 @@ protected function parseIncomingCookies(array $headers): void } } } + + /** + * 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; + } } /** diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 75b1669..dc68871 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -85,6 +85,92 @@ public function test_setApiUrl() $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); } + /** + * @dataProvider getTestDataForIsUserAgentAIBot + */ + public function test_isUserAgentAIBot($userAgent, $expected) + { + $this->assertSame($expected, \MatomoTracker::isUserAgentAIBot($userAgent)); + } + + public function getTestDataForIsUserAgentAIBot(): array + { + return [ + ['', false], + + ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.3', false], + ['Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.3', false], + + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot', true], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.1; +https://openai.com/gptbot', false], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; MistralAI-User/1.0; +https://docs.mistral.ai/robots)', true], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Gemini-Deep-Research; +https://gemini.google/overview/deep-research/) Chrome/135.0.0.0 Safari/537.36', true], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +Claude-User@anthropic.com)', true], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)', true], + ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36; Devin/1.0; +devin.ai', false], + ]; + } + + /** + * @dataProvider getTestDataForGetUrlTrackAIBot + */ + public function test_getUrlTrackAIBot(?int $httpStatus, ?int $responseSizeBytes, ?int $serverTimeMs, ?string $source, string $expected) + { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot'; + + $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker->setUrl('https://example.com/page'); + $tracker->setVisitorId('abcdef01234517ab'); + + $actual = $tracker->getUrlTrackAIBot($httpStatus, $responseSizeBytes, $serverTimeMs, $source); + $actual = $this->normalizeTrackingUrl($actual); + + $this->assertEquals($expected, $actual); + } + + public function getTestDataForGetUrlTrackAIBot(): array + { + return [ + [ + 200, + 34567, + 123, + 'wordpress', + 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&r=&r=&cid=abcdef01234517ab&url=https%3A%2F%2Fexample.com%2Fpage&urlref=&recMode=1&http_status=200&bw_bytes=34567&pf_srv=123&source=wordpress', + ], + + [ + null, + 34567, + null, + 'something else', + 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&r=&r=&cid=abcdef01234517ab&url=https%3A%2F%2Fexample.com%2Fpage&urlref=&recMode=1&bw_bytes=34567&source=something%20else', + ], + + [ + null, + null, + null, + null, + 'http://mymatomo.com/matomo.php?idsite=1&rec=1&apiv=1&r=&r=&cid=abcdef01234517ab&url=https%3A%2F%2Fexample.com%2Fpage&urlref=&recMode=1', + ], + ]; + } + + private function normalizeTrackingUrl(string $url) + { + $nonDeterministicParams = [ + 'r', + '_idts', + ]; + + foreach ($nonDeterministicParams as $param) { + $url = preg_replace('/&' . preg_quote($param) . '=[^&]+/', '&r=', $url); + } + + return $url; + } + public function testUsageApiUrl(): void { $newApiUrl = 'https://NEW-API-URL.com'; From bde2d19e7d03c4d4af739200116f72fcd8099d57 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Mon, 27 Jul 2026 16:16:03 +0200 Subject: [PATCH 112/115] Modernize tracker for 4.0.0: PHP 8.1, strict types, static analysis, full test coverage (#152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Raise PHP requirement to 8.1 and add static-analysis dev tooling - Drop PHP 7.2-8.0 support; require ^8.1. - Bump PHPUnit to ^10.5 (PHP 8.1 floor rules out PHPUnit 11). - Add PHPStan ^2, PHP_CodeSniffer ^3.10, the Matomo coding standard (matomo-org/matomo-coding-standards via VCS repo) and the phpcs composer-installer plugin. - Pin config.platform.php to 8.1.0 and allow the installer plugin. - Add composer scripts: test, phpstan, phpcs, phpcbf. * CI: run on PHP 8.1-8.5 and add PHPStan + PHPCS quality workflow - phpunit.yml: drop PHP 7.2-8.0 from the matrix, bump to actions/checkout@v4, and install the json + curl extensions. - Add quality.yml running PHPStan and PHP_CodeSniffer on PHP 8.1 for every pull request and push to master. * Add strict types and modern type hints; fix latent type bugs Add declare(strict_types=1) to both source files and give every method proper parameter and return type hints aligned with how Matomo core coerces each tracking parameter. Declare all class properties explicitly with concrete/nullable types and array-shape PHPDocs (the class keeps #[AllowDynamicProperties] for consumer BC). Optional 'unset' parameters and their backing properties/getters move from the legacy `= false` sentinel to nullable (`?T = null`); fluent setters now declare `: self`, and the do* tracking methods declare `: string|bool`. Fixes latent type issues that become fatal TypeErrors under strict types (previously masked by weak-mode coercion), including: - sendRequest() now returns string|bool (bulk mode really returns true, not the coerced "1"); cascaded to all do* methods. - forceDotAsSeparatorForDecimalPoint() no longer returns null/false from a : string method. - setCustomTrackingParameter() casts the dimension id to int. - doTrackSiteSearch()/getUrlTrackSiteSearch() use ?int and only emit &search_count when a count is provided (no longer always sends 0). - getUrlTrackCrash()/getUrlTrackEcommerceOrder() cast int/id values before urlencode(); getRequest() casts lat/long before urlencode(). - getUrlTrackEvent() guards the nullable name/value correctly. - clientHints is reset to [] (not false) in bulk mode. - getCustomVariablesFromCookie() validates decoded cookie data. BREAKING CHANGE: passing mismatched scalar types now throws TypeError instead of being silently coerced, and unset sentinels are null rather than false. This targets a new major release. * Add PHPStan configuration at max level Analyse both source files at level max with the PHP 8.1 platform. No baseline: the codebase is fully clean at max level. * Add PHP_CodeSniffer config (Matomo standard) and fix violations Add phpcs.xml.dist referencing the Matomo coding standard with a 400-char line limit and scoped exceptions: the two source files intentionally mix a class with global helper functions/includes and stay in the global namespace for backwards compatibility. Apply the resulting fixes (blank line after the open tag, control structure / operator spacing, trailing newline) and clean up the test file: reorder the file header, add constant visibility, and rename the test methods to camelCase. * Migrate PHPUnit configuration to 10.x and update tests - phpunit.xml.dist: run --migrate-configuration (drop the removed verbose attribute, refresh the schema to 10.5, add cacheDirectory); keep backupGlobals. - Make the data-provider methods static as required by PHPUnit 10. - Ignore the new .phpunit.cache directory. * Document PHP 8.1 requirement and the 4.0.0 major release - README: state the PHP 8.1+ requirement and add a Development section covering the composer test/phpstan/phpcs scripts. - CHANGELOG: add the 4.0.0 entry describing the breaking changes (PHP floor, strict types, false->null sentinels, do* return type, search_count / stream-fallback behaviour) and the new tooling. - .gitattributes: export-ignore phpstan.neon.dist and phpcs.xml.dist so dist archives stay lean. * Remove #[AllowDynamicProperties] All properties are now declared explicitly, so the tracker no longer creates dynamic properties and the attribute is unnecessary. Dropping it makes undeclared dynamic property writes raise the standard PHP 8.2 deprecation, in line with the strict, fully-typed rewrite. * Allow setUserId(null) to de-assign a previously set User ID The method documentation always promised that the User ID can be unset again, but the string type hint made that impossible. Accept null to reset it, matching the null sentinel used everywhere else. * Fix issues found in adversarial review - Cast cookie names to string before strpos(): cookies with purely numeric names are exposed as integer keys in $_COOKIE and caused a TypeError under strict_types, breaking tracker construction. - Do not read $http_response_header when the stream fallback request failed without an HTTP response; previously this caused a TypeError in parseIncomingCookies() instead of returning false. - Send latitude/longitude values of 0.0 instead of silently dropping coordinates on the equator or prime meridian. - Guard the POST URL split against URLs without a query string and replace an always-true elseif condition with else. - Remove dead branches in setEcommerceView() that guarded against values the string type hints now make impossible. - Restore the dropped $category docblock in doTrackSiteSearch(), fix the setClientHints() fullVersionList docblock shape, remove leftover array() syntax and strval() calls, and document all behavior changes in the 4.0.0 changelog. * Greatly expand the unit test suite and report coverage in CI - Add tests for every tracking URL parameter, all do*/getUrlTrack* methods, custom variables/dimensions, ecommerce, client hints, attribution info, visitor/user id handling, bulk tracking, cookie handling and request preparation, including regression tests for the behavior changes of the 4.0.0 release (search_count omission, bulk true return, null sentinels, numeric cookie names). - Introduce a TestableMatomoTracker capturing requests and cookies instead of performing network or header I/O. - Analyse the test suite with PHPStan at max level. - Add a phpunit section and a CI coverage job (pcov). * Improve compatibility with Matomo's tracker usage; add test-only raw param helper Findings from auditing tracker usage across Matomo core and plugins: - setUrlReferrer()/setUrlReferer() accept ?string again so callers can unset the referrer with null (used by several Matomo fixtures). - setCustomTrackingParameter() accepts string|array again; array values are serialized via http_build_query exactly as before 4.0.0, restoring multi-value parameters used by FormAnalytics and HeatmapSessionRecording. - Add setDebugTrackingParameter(string, string) (@internal): appends a raw, unvalidated parameter that overrides any built-in of the same name, so Matomo integration tests can verify server-side handling of malformed values without bypassing the tracker. Cleared per request. Adds unit tests and CHANGELOG entries for all three. * CI: harden workflows (pin setup-php, disable credential persistence) - Pin shivammathur/setup-php to a commit hash (2.37.2) instead of the floating @v2 tag, mitigating supply-chain risk (Aikido high finding). - Set persist-credentials: false on actions/checkout steps so the GITHUB_TOKEN is not left in the git config for later steps (Aikido). * CI: remove the code coverage job Drop the pcov-based coverage job for now; the standard PHPUnit matrix, PHPStan and PHPCS jobs remain. * Distinguish unset from zero for goal/ecommerce revenue amounts Core treats an absent 'revenue' as 'use the goal's configured revenue', and defaults the optional ecommerce amounts to false (not 0), so a real zero is meaningful and must be distinguishable from 'not provided'. - doTrackGoal()/getUrlTrackGoal() (+ Matomo_/Piwik_ helpers) take ?float $revenue = null: null omits revenue, 0.0 now sends revenue=0. - doTrackEcommerceOrder()/getUrlTrackEcommerceOrder()/getUrlTrackEcommerce() take ?float $subTotal/$tax/$shipping/$discount = null, sent only when provided; the required grand total is now always sent (0.0 -> revenue=0). Previously an explicit 0/0.0 was silently dropped by the !empty() guards. Adds tests and a CHANGELOG entry. * Address open issues: URL-encode cip (#151), drop dead curl_close (#149) - URL-encode the cip (override IP) tracking parameter before adding it to the request, matching every other value and preventing tracking-parameter injection when an application sets cip from untrusted input. Fixes #151. - Remove the curl_close() call guarded by PHP_VERSION_ID < 80000: it is dead code now that PHP 8.1 is the minimum, and curl_close() is a no-op (and deprecated in PHP 8.5) on all supported versions. Fixes #149. * Send ca=1 on event and content tracking requests (#80) Append &ca=1 (custom action) to the event and content-tracking URLs, matching the JS tracker. This prevents Matomo from falling back to recording these requests as page views when the handling plugin is disabled. Fixes #80. * Lower default request timeouts to sane in-page values (#88) Change the default requestTimeout from 600s to 5s and connect timeout from 300s to 2s, so a slow or unreachable Matomo can no longer block the calling page for up to 10 minutes. Callers can still raise them via setRequestTimeout()/setRequestConnectTimeout(). Fixes #88. * Add setCurlOptions() for custom cURL options (#92) Allow callers to pass additional cURL options (e.g. CURLOPT_IPRESOLVE, CURLOPT_HTTP_VERSION) that are applied after the built-in options, so they can tune the request without the library committing to opinionated defaults. Fixes #92. * Unify request-failure handling; add opt-in fail-safe mode (#105) Previously the cURL transport threw a RuntimeException on DNS/connection failures while the stream transport silently returned false. Both now throw by default (preserving cURL behavior and making the two transports consistent). Call setExceptionsEnabled(false) to make failed requests return false instead, so tracking never breaks the calling application. Refs #105. * Consistently encode tracking parameters; keep the request URL out of exceptions - URL-encode the remaining raw query values: _refts (from attribution data), customData (data) and pageCharset (cs), so a value can no longer inject extra tracking parameters. - Validate the visitor ID read from the first-party cookie as a 16-character hexadecimal string in loadVisitorIdCookie(), matching setVisitorId(). - Request-failure exceptions now include only the target host, never the full URL (which can carry token_auth/PII in its query string). * Fix cookie header serialization/parsing; redact attribution exception - Build the outgoing Cookie header as url-encoded pairs joined with '; ' (was http_build_query, which joined with '&'), and parse every incoming Set-Cookie header by its first '=' so multiple cookies accumulate (parse_str previously overwrote the whole set per header and applied query-string bracket semantics). getIncomingTrackerCookie() now returns string|false. - setAttributionInfo() no longer echoes the supplied JSON in its exception message and marks the parameter #[\SensitiveParameter]. * Address security-review findings (sensitive params, POST token, transports, bulk) - Mark sendRequest() $url and $data as #[\SensitiveParameter] so they are redacted from exception stack traces (token_auth/PII). - Force POST when token_auth is placed in the request body, so the stream transport no longer sends it as a GET body that Matomo ignores. - Stream transport sets ignore_errors so HTTP 4xx/5xx return the response body (parity with cURL) instead of a failure. - Bulk tracking uses a >=30s timeout and retains the queued actions when a batch fails to send (previously discarded), allowing retry. - Docs: clarify setUserId(null) does not unlink the current server-side visit; drop the stale 'coverage reported in CI' CHANGELOG claim. Adds tests for all of the above. * Encode the action type and harden the transport selection - getUrlTrackAction()/doTrackAction() now URL-encode the action type (not just the value), so a crafted or plugin-specific action parameter cannot inject an additional query-string parameter. - Extract the visitor-id hex character set into a single HEX_CHARACTERS constant used by both setVisitorId() and loadVisitorIdCookie(). - Make cURL detection overridable via hasCurlSupport() so the stream transport can be exercised in tests; add coverage for the stream failure (host-only exception message) and fail-safe paths. * Document error handling/timeouts and clarify config comments - README: add an 'Error handling and timeouts' section covering the default exceptions, the short 5s/2s timeouts, setExceptionsEnabled(), setRequestTimeout()/setRequestConnectTimeout() and bulk retention. - CHANGELOG: correct the multi-value BC note to reference pre-3.4.0 and mention that the download/link action type is now URL-encoded. - phpcs.xml.dist: explain that the 400-char line limit matches Matomo core's own coding standard rather than being an arbitrary value. * Mark request URL/body sensitive in the transport option builders sendRequest() appends token_auth into the URL/body and passes them to prepareCurlOptions()/prepareStreamOptions(). #[\SensitiveParameter] only redacts the frame where the parameter is declared, so mark those params too: should either builder (or something it calls) ever throw, the token and PII carried in $url/$data are redacted from that frame as well. * CHANGELOG: warn that the removed false sentinel now coerces to 0/'' The blanket 'mismatched scalar now throws a TypeError' claim was misleading: strict/weak mode is set by the *caller's* file, so a normal (non-strict) consumer still gets weak-mode coercion. Legacy calls that passed false to mean 'not known' (doTrackEvent value, addEcommerceItem price, setLatitude/setLongitude, ...) now silently send 0/0.0/'' instead of omitting the parameter. Add an explicit upgrade note and correct the TypeError wording so the transition is spelled out. * Merge custom CURLOPT_HTTPHEADER instead of replacing the built-in headers setCurlOptions() applied caller options via array_replace(), so a caller adding a single header wholesale-replaced the tracker's own header list. For POST/bulk requests that dropped the Content-Type, making Matomo unable to parse the body — a silent failure. Merge (append) custom HTTP headers onto the built-in ones instead, and document the special-casing. Also extend the sensitive-parameter test to assert redaction on the transport option builders (prepareCurlOptions/prepareStreamOptions), not just sendRequest, since the URL/body are forwarded to them. --- .gitattributes | 2 + .github/workflows/phpunit.yml | 10 +- .github/workflows/quality.yml | 57 + .gitignore | 1 + CHANGELOG.md | 43 + MatomoTracker.php | 1111 +++++++++------- PiwikTracker.php | 19 +- README.md | 38 + composer.json | 28 +- phpcs.xml.dist | 38 + phpstan.neon.dist | 7 + phpunit.xml.dist | 22 +- tests/Unit/MatomoTrackerTest.php | 1775 +++++++++++++++++++++++++- tests/Unit/TestableMatomoTracker.php | 177 +++ 14 files changed, 2836 insertions(+), 492 deletions(-) create mode 100644 .github/workflows/quality.yml create mode 100644 phpcs.xml.dist create mode 100644 phpstan.neon.dist create mode 100644 tests/Unit/TestableMatomoTracker.php diff --git a/.gitattributes b/.gitattributes index a05f94a..0d46871 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,6 @@ .git* export-ignore phpunit* export-ignore +phpstan.neon.dist export-ignore +phpcs.xml.dist export-ignore tests/ export-ignore run_tests.sh export-ignore diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index fbcb7de..b929af7 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -24,15 +24,17 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest, macOS-latest] - php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php-version }} tools: composer:v2 - extensions: memcached + extensions: curl - name: "Composer install" run: | composer install --prefer-dist diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..f280782 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,57 @@ +name: Code quality + +on: + pull_request: + push: + branches: [ master ] + +permissions: + actions: read + checks: read + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.1' + tools: composer:v2 + extensions: curl + coverage: none + - name: "Composer install" + run: composer install --prefer-dist + - name: PHPStan + run: ./vendor/bin/phpstan analyse --no-progress + + phpcs: + name: PHP_CodeSniffer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.1' + tools: composer:v2 + extensions: curl + coverage: none + - name: "Composer install" + run: composer install --prefer-dist + - name: PHP_CodeSniffer + run: ./vendor/bin/phpcs diff --git a/.gitignore b/.gitignore index 68908d3..ce9dcbc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /vendor/ .phpunit.result.cache /tests/.phpunit.result.cache +/.phpunit.cache/ composer.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 880e1bc..b558746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. +## Matomo PHP Tracker 4.0.0 + +Attention: this is a major release with breaking changes. + +> **Upgrade note — the `false` "not known" sentinel is gone.** Tracker 3.x let you pass `false` to many optional arguments to mean "value not known" (e.g. `doTrackEvent($cat, $act, $name, false)`, `addEcommerceItem($sku, $name, $cat, false)`, `setLatitude(false)`). Those arguments are now typed (`?T` or numeric unions). If your calling code does **not** use `declare(strict_types=1)` — the usual case for a drop-in tracker — PHP's weak-mode coercion silently turns `false` into `0` / `0.0` / `''` instead of raising an error, so such calls now **send a value** (`e_v=0`, `lat=0`, item price `0`) where 3.x omitted the parameter. Replace every `false` "not known" argument with `null` or simply omit it; passing `false` no longer means "unset". + +### Removed +- Support for PHP versions lower than 8.1. The tracker now requires PHP 8.1 or newer. +- The `#[AllowDynamicProperties]` attribute. All properties are now declared explicitly, so setting undeclared dynamic properties on a tracker instance is no longer supported (extend `MatomoTracker` and declare the property instead). + +### Changed +- `declare(strict_types=1)` is now enabled and every method has proper parameter and return type hints aligned with how Matomo core handles the corresponding tracking parameters. Passing a value whose type cannot be coerced now throws a `TypeError` (for example a non-numeric string for a numeric parameter, or any type mismatch when the calling code itself declares `strict_types=1`). Note that for ordinary (non-strict) callers PHP's weak-mode coercion still applies, so e.g. `false` becomes `0`/`''` rather than raising — see the upgrade note above about the removed `false` sentinel. +- Optional "unset" parameters and their corresponding properties and getters now use `null` instead of the previous `false` sentinel. For example `getUserId()`, `getUserAgent()`, `getIp()` and `getPageviewId()` now return `null` (not `false`) when no value is set, and `doTrackEvent()`/`getUrlTrackEvent()` default the event name and value to `null`. +- All public properties are now natively typed. Assigning a legacy sentinel value such as `false` to e.g. `$tracker->userAgent` now throws a `TypeError`; the `attributionInfo` property defaults to an empty array instead of `false`. Subclasses overriding methods with the old untyped signatures may need to be updated to the new signatures. +- `setUserId()` now accepts `null` to de-assign a previously set User ID, as the method documentation always promised (previously the `string` type hint made that impossible). +- `setUrlReferrer()` (and the deprecated `setUrlReferer()`) accept `null` to unset the referrer. +- `setCustomTrackingParameter()` accepts an array value again (serialized via `http_build_query`, as the JS tracker does); this restores the pre-3.4.0 behavior for multi-value parameters. +- `setLatitude()` / `setLongitude()` values of `0.0` (equator / prime meridian) are now sent to Matomo. Previously coordinates of exactly zero were silently dropped. +- Goal and Ecommerce revenue amounts now distinguish "not set" from an explicit `0`. `doTrackGoal()` / `getUrlTrackGoal()` (and the `Matomo_`/`Piwik_` goal helpers) take `?float $revenue = null`: `null` omits `revenue` (so Matomo uses the goal's configured revenue) while `0.0` now sends `revenue=0`. Likewise the optional Ecommerce amounts (`$subTotal`, `$tax`, `$shipping`, `$discount` of `doTrackEcommerceOrder()` etc.) are `?float = null` and only sent when provided, and the required Ecommerce grand total is now always sent (a `0.0` order/cart sends `revenue=0`). Previously an explicit `0`/`0.0` was silently omitted for all of these. +- The `do*` tracking methods now declare a `string|bool` return type. In bulk mode they return boolean `true` (previously the value was coerced to the string `"1"`). +- `doTrackSiteSearch()` / `getUrlTrackSiteSearch()` accept `?int $countResults` and only send `&search_count` when a count is provided (previously `&search_count=0` was always sent). +- Both transports now consistently throw a `RuntimeException` on request failure (DNS, connection or timeout errors) by default; previously only the cURL transport threw while the stream fallback silently returned `false`. Call `setExceptionsEnabled(false)` to make failed requests return `false` instead, so tracking never breaks the calling application (#105). +- Lowered the default request timeouts from 600s/300s to 5s/2s so a slow or unreachable Matomo can no longer block the calling page for minutes (#88). Raise them again via `setRequestTimeout()` / `setRequestConnectTimeout()` if needed. +- Bumped the test suite to PHPUnit 10.5. + +### Fixed +- All tracking parameter names and values are now consistently URL-encoded (including `_refts`, `data`/`customData`, `cs`/charset and the `download`/`link` action type passed to `getUrlTrackAction()`/`doTrackAction()`), and the visitor ID read from the first-party cookie is validated as a 16-character hexadecimal string. +- Request-failure exceptions no longer include the full request URL (only the target host), so its query string is never surfaced in error messages/logs. The request URL and body are also marked `#[\SensitiveParameter]` so they are redacted from exception stack traces. +- Authenticated requests that carry `token_auth` in the request body are now sent as `POST`; previously the stream transport sent them as `GET`, so Matomo ignored the token in the body. +- The stream transport now returns the response body for HTTP 4xx/5xx responses (like cURL) instead of turning them into a failure. +- Bulk tracking uses a more generous request timeout (at least 30s) and no longer discards the queued actions when a batch fails to send, so the batch can be retried. +- Outgoing tracker cookies are now joined with `; ` (not `&`), and all incoming `Set-Cookie` response headers are parsed instead of only the last one; `getIncomingTrackerCookie()` returns `string|false`. +- `setAttributionInfo()` no longer includes the supplied payload in its exception message (the parameter is also marked `#[\SensitiveParameter]`). +- Event and content tracking requests now send `&ca=1` (custom action), so Matomo no longer falls back to recording them as page views if the handling plugin is disabled (#80). +- The `cip` (override IP) tracking parameter is now URL-encoded like every other value (#151). +- No longer calls the deprecated `curl_close()` (it was already a no-op on the supported PHP versions) (#149). + +### Added +- PHPStan static analysis at max level (`phpstan.neon.dist`) and the Matomo coding standard via PHP_CodeSniffer (`phpcs.xml.dist`), both enforced for every pull request through GitHub Actions. +- A greatly expanded unit test suite covering all tracking parameters, cookie handling and request preparation. +- `setDebugTrackingParameter()` (`@internal` test helper) to append a raw, unvalidated tracking parameter that overrides any built-in parameter of the same name, so integration tests can verify server-side handling of malformed values. +- `setCurlOptions(array)` to pass additional cURL options (e.g. `CURLOPT_IPRESOLVE`, `CURLOPT_HTTP_VERSION`) for the tracking requests; they are applied after the built-in options (#92). Custom `CURLOPT_HTTPHEADER` entries are merged with the tracker's own headers rather than replacing them, so adding a header no longer drops the built-in `Content-Type` (which would otherwise break bulk requests). + ## Matomo PHP Tracker 3.4.0 ### Changed diff --git a/MatomoTracker.php b/MatomoTracker.php index 989bdfe..26f5a28 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -1,4 +1,5 @@ , 3: string, 4: int}> + */ + public array $ecommerceItems = []; + + /** + * @var array + */ + public array $attributionInfo = []; - public $attributionInfo = false; + /** + * @var array + */ + public array $eventCustomVar = []; - public $eventCustomVar = []; + public ?string $forcedDatetime = null; - public $forcedDatetime = false; + public bool $forcedNewVisit = false; - public $forcedNewVisit = false; + public ?int $networkTime = null; - public $networkTime = false; + public ?int $serverTime = null; - public $serverTime = false; + public ?int $transferTime = null; - public $transferTime = false; + public ?int $domProcessingTime = null; - public $domProcessingTime = false; + public ?int $domCompletionTime = null; - public $domCompletionTime = false; + public ?int $onLoadTime = null; - public $onLoadTime = false; + /** + * @var array + */ + public array $pageCustomVar = []; - public $pageCustomVar = []; + /** + * @var array + */ + public array $ecommerceView = []; - public $ecommerceView = []; + /** + * @var array> + */ + public array $customParameters = []; - public $customParameters = []; + /** + * Raw tracking parameters set via setDebugTrackingParameter(). Their names and values are + * URL-encoded and appended after the built-in parameters, overriding any of the same name. + * + * @var array + * @internal + */ + public array $debugParameters = []; - public $customDimensions = []; + /** + * @var array + */ + public array $customDimensions = []; - public $customData = false; + public ?string $customData = null; - public $hasCookies = false; + public bool $hasCookies = false; - public $token_auth = false; + public ?string $token_auth = null; - public $userAgent = false; + public ?string $userAgent = null; - public $country = false; + public ?string $country = null; - public $region = false; + public ?string $region = null; - public $city = false; + public ?string $city = null; - public $lat = false; + public ?float $lat = null; - public $long = false; + public ?float $long = null; - public $width = false; + public ?int $width = null; - public $height = false; + public ?int $height = null; - public $plugins = false; + public ?string $plugins = null; - public $localHour = false; + public ?int $localHour = null; - public $localMinute = false; + public ?int $localMinute = null; - public $localSecond = false; + public ?int $localSecond = null; - public $idPageview = false; + public ?string $idPageview = null; - public $idPageviewSetManually = false; + public bool $idPageviewSetManually = false; - public $idSite; + public int $idSite; - public $urlReferrer; + public ?string $urlReferrer = null; - public $pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES; + public string $pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES; - public $pageUrl; + public string $pageUrl = ''; - public $ip; + public ?string $ip = null; - public $acceptLanguage; + public ?string $acceptLanguage = null; - public $clientHints = []; + /** + * @var array + */ + public array $clientHints = []; // Life of the visitor cookie (in sec) - public $configVisitorCookieTimeout = 33955200; // 13 months (365 + 28 days) + public int $configVisitorCookieTimeout = 33955200; // 13 months (365 + 28 days) // Life of the session cookie (in sec) - public $configSessionCookieTimeout = 1800; // 30 minutes + public int $configSessionCookieTimeout = 1800; // 30 minutes // Life of the session cookie (in sec) - public $configReferralCookieTimeout = 15768000; // 6 months + public int $configReferralCookieTimeout = 15768000; // 6 months // Visitor Ids in order - public $userId = false; + public ?string $userId = null; - public $forcedVisitorId = false; + public ?string $forcedVisitorId = null; - public $cookieVisitorId = false; + public ?string $cookieVisitorId = null; - public $randomVisitorId = false; + public string $randomVisitorId = ''; - public $configCookiesDisabled = false; + public bool $configCookiesDisabled = false; - public $configCookiePath = self::DEFAULT_COOKIE_PATH; + public string $configCookiePath = self::DEFAULT_COOKIE_PATH; - public $configCookieDomain = ''; + public string $configCookieDomain = ''; - public $configCookieSameSite = ''; + public string $configCookieSameSite = ''; - public $configCookieSecure = false; + public bool $configCookieSecure = false; - public $configCookieHTTPOnly = false; + public bool $configCookieHTTPOnly = false; - public $currentTs; + public int $currentTs; - public $createTs; + public int $createTs; // Allow debug while blocking the request - public $requestTimeout = 600; + public int $requestTimeout = 5; + + public int $requestConnectTimeout = 2; + + public bool $doBulkRequests = false; + + /** + * @var list + */ + public array $storedTrackingActions = []; + + public bool $sendImageResponse = true; - public $requestConnectTimeout = 300; + // When true (default), failed tracking requests throw a RuntimeException; set false to return false instead. + public bool $exceptionsEnabled = true; - public $doBulkRequests = false; + /** + * @var array + */ + public array $outgoingTrackerCookies = []; - public $storedTrackingActions = []; + /** + * @var array + */ + public array $incomingTrackerCookies = []; - public $sendImageResponse = true; + /** + * @var array + */ + public array $visitorCustomVar = []; - public $outgoingTrackerCookies = []; + private ?string $requestMethod = null; - public $incomingTrackerCookies = []; + private string $apiUrl = ''; - public $visitorCustomVar; + private ?string $proxy = null; - private $requestMethod = null; + private int $proxyPort = 80; - private $apiUrl = ''; + /** + * Additional cURL options set via setCurlOptions(), applied last so they override the defaults. + * + * @var array + */ + private array $curlOptions = []; /** * Builds a MatomoTracker object, used to track visits, pages and Goal conversions @@ -225,18 +292,18 @@ class MatomoTracker public function __construct(int $idSite, string $apiUrl = '') { $this->idSite = $idSite; - $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false; + $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? self::toStringValue($_SERVER['HTTP_REFERER']) : null; $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->ip = !empty($_SERVER['REMOTE_ADDR']) ? self::toStringValue($_SERVER['REMOTE_ADDR']) : null; + $this->acceptLanguage = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? self::toStringValue($_SERVER['HTTP_ACCEPT_LANGUAGE']) : null; + $this->userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? self::toStringValue($_SERVER['HTTP_USER_AGENT']) : null; $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'] : '', - !empty($_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS']) ? $_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS'] : '' + !empty($_SERVER['HTTP_SEC_CH_UA_MODEL']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_MODEL']) : '', + !empty($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) : '', + !empty($_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION']) : '', + !empty($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION_LIST']) : '', + !empty($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_FULL_VERSION']) : '', + !empty($_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS']) ? self::toStringValue($_SERVER['HTTP_SEC_CH_UA_FORM_FACTORS']) : '' ); if (!empty($apiUrl)) { self::$URL = $apiUrl; @@ -265,7 +332,7 @@ public function setApiUrl(string $url): void * * @return $this */ - public function setPageCharset(string $charset = '') + public function setPageCharset(string $charset = ''): self { $this->pageCharset = $charset; @@ -278,7 +345,7 @@ public function setPageCharset(string $charset = '') * @param string $url Raw URL (not URL encoded) * @return $this */ - public function setUrl(string $url) + public function setUrl(string $url): self { $this->pageUrl = $url; @@ -288,10 +355,10 @@ public function setUrl(string $url) /** * Sets the URL referrer used to track Referrers details for new visits. * - * @param string $url Raw URL (not URL encoded) + * @param string|null $url Raw URL (not URL encoded), or null to unset the referrer * @return $this */ - public function setUrlReferrer(string $url) + public function setUrlReferrer(?string $url): self { $this->urlReferrer = $url; @@ -307,7 +374,7 @@ public function setUrlReferrer(string $url) * @deprecated this metric is deprecated please use performance timings instead * @see setPerformanceTimings */ - public function setGenerationTime(int $timeMs) + public function setGenerationTime(int $timeMs): self { return $this; } @@ -331,7 +398,7 @@ public function setPerformanceTimings( ?int $domProcessing = null, ?int $domCompletion = null, ?int $onload = null - ) { + ): self { $this->networkTime = $network; $this->serverTime = $server; $this->transferTime = $transfer; @@ -347,19 +414,19 @@ public function setPerformanceTimings( */ public function clearPerformanceTimings(): void { - $this->networkTime = false; - $this->serverTime = false; - $this->transferTime = false; - $this->domProcessingTime = false; - $this->domCompletionTime = false; - $this->onLoadTime = false; + $this->networkTime = null; + $this->serverTime = null; + $this->transferTime = null; + $this->domProcessingTime = null; + $this->domCompletionTime = null; + $this->onLoadTime = null; } /** * @deprecated * @ignore */ - public function setUrlReferer(string $url) + public function setUrlReferer(?string $url): self { $this->setUrlReferrer($url); @@ -381,11 +448,11 @@ public function setUrlReferer(string $url) * @throws Exception * @see function getAttributionInfo() in https://github.com/matomo-org/matomo/blob/master/js/matomo.js */ - public function setAttributionInfo(string $jsonEncoded) + public function setAttributionInfo(#[\SensitiveParameter] string $jsonEncoded): self { - $decoded = json_decode($jsonEncoded, $assoc = true); + $decoded = json_decode($jsonEncoded, true); if (!is_array($decoded)) { - throw new Exception("setAttributionInfo() is expecting a JSON encoded string, $jsonEncoded given"); + throw new Exception("setAttributionInfo() is expecting a JSON encoded string"); } $this->attributionInfo = $decoded; @@ -408,13 +475,13 @@ public function setCustomVariable( string $name, string $value, string $scope = 'visit' - ) { + ): self { if ($scope === 'page') { - $this->pageCustomVar[$id] = array($name, $value); + $this->pageCustomVar[$id] = [$name, $value]; } elseif ($scope === 'event') { - $this->eventCustomVar[$id] = array($name, $value); + $this->eventCustomVar[$id] = [$name, $value]; } elseif ($scope === 'visit') { - $this->visitorCustomVar[$id] = array($name, $value); + $this->visitorCustomVar[$id] = [$name, $value]; } else { throw new Exception("Invalid 'scope' parameter value"); } @@ -431,10 +498,11 @@ public function setCustomVariable( * @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 + * @return array{0: string, 1: string}|false An array with this format: + * array( 0 => CustomVariableName, 1 => CustomVariableValue ) or false * @see matomo.js getCustomVariable() */ - public function getCustomVariable(int $id, string $scope = 'visit') + public function getCustomVariable(int $id, string $scope = 'visit'): array|false { if ($scope === 'page') { return $this->pageCustomVar[$id] ?? false; @@ -451,17 +519,8 @@ public function getCustomVariable(int $id, string $scope = 'visit') 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]; + return $this->getCustomVariablesFromCookie()[$id] ?? false; } /** @@ -484,9 +543,9 @@ public function clearCustomVariables(): void * @param string $value value for custom dimension * @return $this */ - public function setCustomDimension(int $id, string $value) + public function setCustomDimension(int $id, string $value): self { - $this->customDimensions['dimension'.$id] = $value; + $this->customDimensions['dimension' . $id] = $value; return $this; } @@ -507,7 +566,7 @@ public function clearCustomDimensions(): void */ public function getCustomDimension(int $id): ?string { - return $this->customDimensions['dimension'.$id] ?? null; + return $this->customDimensions['dimension' . $id] ?? null; } /** @@ -516,16 +575,17 @@ public function getCustomDimension(int $id): ?string * 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. + * @param string|array $value Tracking parameter value that shall be sent for this tracking parameter. + * An array value is serialized the same way as the Matomo JS tracker does it (via http_build_query). * @return $this * @throws Exception */ - public function setCustomTrackingParameter(string $trackingApiParameter, string $value) + public function setCustomTrackingParameter(string $trackingApiParameter, string|array $value): self { $matches = []; - if (preg_match('/^dimension([0-9]+)$/', $trackingApiParameter, $matches)) { - $this->setCustomDimension($matches[1], $value); + if (is_string($value) && preg_match('/^dimension([0-9]+)$/', $trackingApiParameter, $matches)) { + $this->setCustomDimension((int) $matches[1], $value); return $this; } @@ -543,15 +603,37 @@ public function clearCustomTrackingParameters(): void $this->customParameters = []; } + /** + * Test helper: sets a raw tracking parameter, bypassing the typed setters and any + * client-side validation, so integration tests (e.g. in Matomo itself) can verify how the + * server handles malformed or invalid parameter values. + * + * The name and value are URL-encoded (like any other parameter) and appended after the + * built-in parameters, so this overrides any built-in parameter of the same name. Send a + * value that is invalid once decoded server-side (raw bytes are not sent unencoded). Like + * the other custom parameters, it is cleared after each tracking request. Not for production use. + * + * @internal + * @param string $name The tracking API parameter name, eg 'idsite' or '_cvar' + * @param string $value The raw value to send (may be intentionally invalid) + * @return $this + */ + public function setDebugTrackingParameter(string $name, string $value): self + { + $this->debugParameters[$name] = $value; + + return $this; + } + /** * Sets the current visitor ID to a random new one. * @return $this */ - public function setNewVisitorId() + public function setNewVisitorId(): self { - $this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID); - $this->forcedVisitorId = false; - $this->cookieVisitorId = false; + $this->randomVisitorId = substr(md5(uniqid((string) rand(), true)), 0, self::LENGTH_VISITOR_ID); + $this->forcedVisitorId = null; + $this->cookieVisitorId = null; return $this; } @@ -561,7 +643,7 @@ public function setNewVisitorId() * * @return $this */ - public function setIdSite(int $idSite) + public function setIdSite(int $idSite): self { $this->idSite = $idSite; @@ -574,7 +656,7 @@ public function setIdSite(int $idSite) * @param string $acceptLanguage For example "fr-fr" * @return $this */ - public function setBrowserLanguage(string $acceptLanguage) + public function setBrowserLanguage(string $acceptLanguage): self { $this->acceptLanguage = $acceptLanguage; @@ -588,7 +670,7 @@ public function setBrowserLanguage(string $acceptLanguage) * @param string $userAgent * @return $this */ - public function setUserAgent(string $userAgent) + public function setUserAgent(string $userAgent): self { $this->userAgent = $userAgent; @@ -604,8 +686,8 @@ public function setUserAgent(string $userAgent) * @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 $fullVersionList Value of header 'HTTP_SEC_CH_UA_FULL_VERSION_LIST' - * or an array containing all brands with the structure + * @param string|list $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' * @param string|array $formFactors Value of the header 'HTTP_SEC_CH_UA_FORM_FACTORS' @@ -617,10 +699,10 @@ public function setClientHints( string $model = '', string $platform = '', string $platformVersion = '', - $fullVersionList = '', + string|array $fullVersionList = '', string $uaFullVersion = '', - $formFactors = '' - ) { + string|array $formFactors = '' + ): self { if (is_string($fullVersionList)) { $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; $list = []; @@ -631,8 +713,6 @@ public function setClientHints( } $fullVersionList = $list; - } elseif (!is_array($fullVersionList)) { - $fullVersionList = []; } if (is_string($formFactors)) { @@ -643,8 +723,6 @@ function ($item) { }, $formFactors )); - } elseif (!is_array($formFactors)) { - $formFactors = []; } $this->clientHints = array_filter([ @@ -667,7 +745,7 @@ function ($item) { * * @return $this */ - public function setCountry(string $country) + public function setCountry(string $country): self { $this->country = $country; @@ -682,7 +760,7 @@ public function setCountry(string $country) * * @return $this */ - public function setRegion(string $region) + public function setRegion(string $region): self { $this->region = $region; @@ -697,7 +775,7 @@ public function setRegion(string $region) * * @return $this */ - public function setCity(string $city) + public function setCity(string $city): self { $this->city = $city; @@ -712,7 +790,7 @@ public function setCity(string $city) * * @return $this */ - public function setLatitude(float $lat) + public function setLatitude(float $lat): self { $this->lat = $lat; @@ -727,7 +805,7 @@ public function setLatitude(float $lat) * * @return $this */ - public function setLongitude(float $long) + public function setLongitude(float $long): self { $this->long = $long; @@ -788,7 +866,7 @@ public function disableSendImageResponse(): void /** * Fix-up domain */ - protected static function domainFixup($domain) + protected static function domainFixup(string $domain): string { if (strlen($domain) > 0) { $dl = strlen($domain) - 1; @@ -829,9 +907,9 @@ protected function getCookieName(string $cookieName): string * 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. + * @return string|bool Response string or true if using bulk requests. */ - public function doTrackPageView(string $documentTitle) + public function doTrackPageView(string $documentTitle): string|bool { if (!$this->idPageviewSetManually) { $this->generateNewPageviewId(); @@ -853,9 +931,9 @@ public function doTrackPageView(string $documentTitle) * @param int|null $responseSizeBytes the size of the response sent to the AI bot, if known. * @param int|null $serverTimeMs the number of milliseconds it took to process the request, if known. * @param string|null $source the source/proxy that served the request (max 50 chars), if known. - * @return string|null Response string, or null if the current user agent is not a known AI bot. + * @return string|bool|null Response string, or null if the current user agent is not a known AI bot. */ - public function doTrackPageViewIfAIBot(?int $httpStatus = null, ?int $responseSizeBytes = null, ?int $serverTimeMs = null, ?string $source = null) + public function doTrackPageViewIfAIBot(?int $httpStatus = null, ?int $responseSizeBytes = null, ?int $serverTimeMs = null, ?string $source = null): string|bool|null { if (!self::isUserAgentAIBot($this->userAgent)) { return null; @@ -880,16 +958,16 @@ public function setPageviewId(string $idPageview): void * 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. + * @return string|null The PageView id as string or null if there is none yet. */ - public function getPageviewId() + public function getPageviewId(): ?string { return $this->idPageview; } private function generateNewPageviewId(): void { - $this->idPageview = substr(md5(uniqid(rand(), true)), 0, 6); + $this->idPageview = substr(md5(uniqid((string) rand(), true)), 0, 6); } /** @@ -897,16 +975,16 @@ private function generateNewPageviewId(): void * * @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. + * @param string|null $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...) + * @param int|float|null $value (optional) The Event's value + * @return string|bool Response string or true if using bulk requests. */ public function doTrackEvent( string $category, string $action, - $name = false, - $value = false - ) { + ?string $name = null, + int|float|null $value = null + ): string|bool { $url = $this->getUrlTrackEvent($category, $action, $name, $value); return $this->sendRequest($url); @@ -917,14 +995,14 @@ public function doTrackEvent( * * @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. + * @param string|null $contentTarget (optional) The target of the content. For instance the URL of a landing page. + * @return string|bool Response string or true if using bulk requests. */ public function doTrackContentImpression( string $contentName, string $contentPiece = 'Unknown', - $contentTarget = false - ) { + ?string $contentTarget = null + ): string|bool { $url = $this->getUrlTrackContentImpression($contentName, $contentPiece, $contentTarget); return $this->sendRequest($url); @@ -937,15 +1015,15 @@ public function 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. + * @param string|null $contentTarget (optional) The target the content leading to when an interaction occurs. For instance the URL of a landing page. + * @return string|bool Response string or true if using bulk requests. */ public function doTrackContentInteraction( string $interaction, string $contentName, string $contentPiece = 'Unknown', - $contentTarget = false - ) { + ?string $contentTarget = null + ): string|bool { $url = $this->getUrlTrackContentInteraction($interaction, $contentName, $contentPiece, $contentTarget); return $this->sendRequest($url); @@ -957,15 +1035,15 @@ public function doTrackContentInteraction( * * @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. + * @param int|null $countResults (optional) results displayed on the search result page. Used to track "zero result" keywords. * - * @return mixed Response or true if using bulk requests. + * @return string|bool Response or true if using bulk requests. */ public function doTrackSiteSearch( string $keyword, string $category = '', - $countResults = false - ) { + ?int $countResults = null + ): string|bool { $url = $this->getUrlTrackSiteSearch($keyword, $category, $countResults); return $this->sendRequest($url); @@ -975,10 +1053,11 @@ public function doTrackSiteSearch( * 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 + * @param float|null $revenue Revenue for this conversion. Pass null (default) to omit the + * revenue so Matomo uses the goal's configured revenue; pass 0.0 to force a zero revenue. + * @return string|bool Response or true if using bulk request */ - public function doTrackGoal(int $idGoal, float $revenue = 0.0) + public function doTrackGoal(int $idGoal, ?float $revenue = null): string|bool { $url = $this->getUrlTrackGoal($idGoal, $revenue); @@ -990,9 +1069,9 @@ public function doTrackGoal(int $idGoal, float $revenue = 0.0) * * @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 + * @return string|bool Response or true if using bulk request */ - public function doTrackAction(string $actionUrl, string $actionType) + public function doTrackAction(string $actionUrl, string $actionType): string|bool { // Referrer could be udpated to be the current URL temporarily (to mimic JS behavior) $url = $this->getUrlTrackAction($actionUrl, $actionType); @@ -1010,8 +1089,8 @@ public function doTrackAction(string $actionUrl, string $actionType) * * @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 string|array $category (optional) Product category, or array of product categories (up to 5 categories can be specified for a given product) + * @param int|float|string $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 @@ -1019,17 +1098,17 @@ public function doTrackAction(string $actionUrl, string $actionType) public function addEcommerceItem( string $sku, string $name = '', - $category = '', - $price = 0.0, + string|array $category = '', + int|float|string $price = 0.0, int $quantity = 1 - ) { + ): self { if (empty($sku)) { throw new Exception("You must specify a SKU for the Ecommerce item"); } - $price = $this->forceDotAsSeparatorForDecimalPoint($price); + $priceNormalized = $this->forceDotAsSeparatorForDecimalPoint($price); - $this->ecommerceItems[] = array($sku, $name, $category, $price, $quantity); + $this->ecommerceItems[] = [$sku, $name, $category, $priceNormalized, $quantity]; return $this; } @@ -1042,9 +1121,9 @@ public function addEcommerceItem( * 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 + * @return string|bool Response or true if using bulk request */ - public function doTrackEcommerceCartUpdate(float $grandTotal) + public function doTrackEcommerceCartUpdate(float $grandTotal): string|bool { $url = $this->getUrlTrackEcommerceCartUpdate($grandTotal); @@ -1057,9 +1136,9 @@ public function doTrackEcommerceCartUpdate(float $grandTotal) * To enable bulk tracking, call enableBulkTracking(). * * @throws Exception - * @return string Response + * @return string|bool Response */ - public function doBulkTrack() + public function doBulkTrack(): string|bool { if (empty($this->storedTrackingActions)) { throw new Exception( @@ -1076,9 +1155,26 @@ public function doBulkTrack() } $postData = json_encode($data); - $response = $this->sendRequest($this->getBaseUrl(), 'POST', $postData, $force = true); + if ($postData === false) { + throw new Exception("Failed to JSON encode the bulk tracking request"); + } - $this->storedTrackingActions = []; + // Bulk imports can carry many actions and take longer than a single in-page request, so + // give them a more generous timeout (never below the caller-configured value). + $originalTimeout = $this->requestTimeout; + $this->requestTimeout = max($this->requestTimeout, self::DEFAULT_BULK_REQUEST_TIMEOUT); + try { + $response = $this->sendRequest($this->getBaseUrl(), 'POST', $postData, true); + } finally { + $this->requestTimeout = $originalTimeout; + } + + // Only drop the queued actions once they were sent successfully, so a failed batch (in + // fail-safe mode, where sendRequest returns false) can be retried by calling doBulkTrack() + // again instead of being silently lost. + if ($response !== false) { + $this->storedTrackingActions = []; + } return $response; } @@ -1094,20 +1190,20 @@ public function doBulkTrack() * 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 + * @param float|null $subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied). Pass null to omit, 0.0 to send an explicit zero. + * @param float|null $tax (optional) Tax amount for this order + * @param float|null $shipping (optional) Shipping amount for this order + * @param float|null $discount (optional) Discounted amount in this order + * @return string|bool Response or true if using bulk request */ public function doTrackEcommerceOrder( - $orderId, + string|int $orderId, float $grandTotal, - float $subTotal = 0.0, - float $tax = 0.0, - float $shipping = 0.0, - float $discount = 0.0 - ) { + ?float $subTotal = null, + ?float $tax = null, + ?float $shipping = null, + ?float $discount = null + ): string|bool { $url = $this->getUrlTrackEcommerceOrder($orderId, $grandTotal, $subTotal, $tax, $shipping, $discount); return $this->sendRequest($url); @@ -1121,9 +1217,9 @@ public function doTrackEcommerceOrder( * 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 + * @return string|bool Response or true if using bulk request */ - public function doTrackPhpThrowable(Throwable $throwable, ?string $category = null) + public function doTrackPhpThrowable(Throwable $throwable, ?string $category = null): string|bool { $message = $throwable->getMessage(); $stack = $throwable->getTraceAsString(); @@ -1145,7 +1241,7 @@ public function doTrackPhpThrowable(Throwable $throwable, ?string $category = nu * @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 + * @return string|bool Response or true if using bulk request */ public function doTrackCrash( string $message, @@ -1155,7 +1251,7 @@ public function doTrackCrash( ?string $location = null, ?int $line = null, ?int $column = null - ) { + ): string|bool { $url = $this->getUrlTrackCrash($message, $type, $category, $stack, $location, $line, $column); return $this->sendRequest($url); @@ -1168,9 +1264,9 @@ public function doTrackCrash( * 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 + * @return string|bool Response or true if using bulk request */ - public function doPing() + public function doPing(): string|bool { $url = $this->getRequest($this->idSite); $url .= '&ping=1'; @@ -1183,14 +1279,14 @@ public function doPing() * * 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. + * On a category page, you may set the parameter $category only and leave the other parameters empty. * * 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. + * @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 @@ -1198,23 +1294,20 @@ public function doPing() public function setEcommerceView( string $sku = '', string $name = '', - $category = '', + string|array $category = '', float $price = 0.0 - ) { + ): self { $this->ecommerceView = []; - if (!empty($category)) { - if (is_array($category)) { - $category = json_encode($category); - } - } else { - $category = ""; + if (empty($category)) { + $category = ''; + } elseif (is_array($category)) { + $category = (string) json_encode($category); } $this->ecommerceView['_pkc'] = $category; if (!empty($price)) { - $price = $this->forceDotAsSeparatorForDecimalPoint($price); - $this->ecommerceView['_pkp'] = $price; + $this->ecommerceView['_pkp'] = $this->forceDotAsSeparatorForDecimalPoint($price); } // On a category page, do not record "Product name not defined" @@ -1224,9 +1317,6 @@ public function setEcommerceView( if (!empty($sku)) { $this->ecommerceView['_pks'] = $sku; } - if (empty($name)) { - $name = ''; - } $this->ecommerceView['_pkn'] = $name; return $this; @@ -1236,15 +1326,11 @@ public function setEcommerceView( * 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 + * @param int|float|string $value */ - private function forceDotAsSeparatorForDecimalPoint($value): string + private function forceDotAsSeparatorForDecimalPoint(int|float|string $value): string { - if (null === $value || false === $value) { - return $value; - } - - return str_replace(',', '.', $value); + return str_replace(',', '.', (string) $value); } /** @@ -1287,7 +1373,7 @@ public function getUrlTrackAIBot(?int $httpStatus = null, ?int $responseSizeByte * so items will have to be added again via addEcommerceItem() * @ignore */ - public function getUrlTrackEcommerceCartUpdate($grandTotal) + public function getUrlTrackEcommerceCartUpdate(float $grandTotal): string { return $this->getUrlTrackEcommerce($grandTotal); } @@ -1299,18 +1385,18 @@ public function getUrlTrackEcommerceCartUpdate($grandTotal) * @ignore */ public function getUrlTrackEcommerceOrder( - $orderId, - $grandTotal, - $subTotal = 0.0, - $tax = 0.0, - $shipping = 0.0, - $discount = 0.0 - ) { + string|int $orderId, + float $grandTotal, + ?float $subTotal = null, + ?float $tax = null, + ?float $shipping = null, + ?float $discount = null + ): string { 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); + $url .= '&ec_id=' . urlencode((string) $orderId); return $url; } @@ -1323,38 +1409,33 @@ public function getUrlTrackEcommerceOrder( * * @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)"); - } - + protected function getUrlTrackEcommerce( + float $grandTotal, + ?float $subTotal = null, + ?float $tax = null, + ?float $shipping = null, + ?float $discount = null + ): string { $url = $this->getRequest($this->idSite); $url .= '&idgoal=0'; - if (!empty($grandTotal)) { - $grandTotal = $this->forceDotAsSeparatorForDecimalPoint($grandTotal); - $url .= '&revenue=' . $grandTotal; + // grandTotal is required, so it is always sent (including an explicit 0). + $url .= '&revenue=' . $this->forceDotAsSeparatorForDecimalPoint($grandTotal); + if ($subTotal !== null) { + $url .= '&ec_st=' . $this->forceDotAsSeparatorForDecimalPoint($subTotal); } - if (!empty($subTotal)) { - $subTotal = $this->forceDotAsSeparatorForDecimalPoint($subTotal); - $url .= '&ec_st=' . $subTotal; + if ($tax !== null) { + $url .= '&ec_tx=' . $this->forceDotAsSeparatorForDecimalPoint($tax); } - if (!empty($tax)) { - $tax = $this->forceDotAsSeparatorForDecimalPoint($tax); - $url .= '&ec_tx=' . $tax; + if ($shipping !== null) { + $url .= '&ec_sh=' . $this->forceDotAsSeparatorForDecimalPoint($shipping); } - if (!empty($shipping)) { - $shipping = $this->forceDotAsSeparatorForDecimalPoint($shipping); - $url .= '&ec_sh=' . $shipping; - } - if (!empty($discount)) { - $discount = $this->forceDotAsSeparatorForDecimalPoint($discount); - $url .= '&ec_dt=' . $discount; + if ($discount !== null) { + $url .= '&ec_dt=' . $this->forceDotAsSeparatorForDecimalPoint($discount); } if (!empty($this->ecommerceItems)) { - $url .= '&ec_items=' . urlencode(json_encode($this->ecommerceItems)); + $url .= '&ec_items=' . urlencode((string) json_encode($this->ecommerceItems)); } - $this->ecommerceItems = array(); + $this->ecommerceItems = []; return $url; } @@ -1382,16 +1463,16 @@ public function getUrlTrackPageView(string $documentTitle = ''): string * @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 + * @param string|null $name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...) + * @param int|float|null $value (optional) The Event's value * @return string URL to matomo.php with all parameters set to track the pageview - * @throws + * @throws Exception */ public function getUrlTrackEvent( string $category, string $action, - $name = false, - $value = false + ?string $name = null, + int|float|null $value = null ): string { $url = $this->getRequest($this->idSite); if (strlen($category) === 0) { @@ -1403,13 +1484,14 @@ public function getUrlTrackEvent( $url .= '&e_c=' . urlencode($category); $url .= '&e_a=' . urlencode($action); + // mark as a custom action so Matomo does not fall back to tracking it as a page view + $url .= '&ca=1'; - if (strlen($name) > 0) { + if ($name !== null && $name !== '') { $url .= '&e_n=' . urlencode($name); } - if (strlen($value) > 0) { - $value = $this->forceDotAsSeparatorForDecimalPoint($value); - $url .= '&e_v=' . $value; + if ($value !== null) { + $url .= '&e_v=' . $this->forceDotAsSeparatorForDecimalPoint($value); } return $url; @@ -1421,14 +1503,14 @@ public function getUrlTrackEvent( * @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. + * @param string|null $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 $contentTarget ): string { $url = $this->getRequest($this->idSite); @@ -1437,11 +1519,13 @@ public function getUrlTrackContentImpression( } $url .= '&c_n=' . urlencode($contentName); + // mark as a custom action so Matomo does not fall back to tracking it as a page view + $url .= '&ca=1'; - if (!empty($contentPiece) && strlen($contentPiece) > 0) { + if (!empty($contentPiece)) { $url .= '&c_p=' . urlencode($contentPiece); } - if (!empty($contentTarget) && strlen($contentTarget) > 0) { + if (!empty($contentTarget)) { $url .= '&c_t=' . urlencode($contentTarget); } @@ -1455,7 +1539,7 @@ public function getUrlTrackContentImpression( * @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. + * @param string|null $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 */ @@ -1463,7 +1547,7 @@ public function getUrlTrackContentInteraction( string $interaction, string $contentName, string $contentPiece, - $contentTarget + ?string $contentTarget ): string { $url = $this->getRequest($this->idSite); @@ -1477,11 +1561,13 @@ public function getUrlTrackContentInteraction( $url .= '&c_i=' . urlencode($interaction); $url .= '&c_n=' . urlencode($contentName); + // mark as a custom action so Matomo does not fall back to tracking it as a page view + $url .= '&ca=1'; - if (!empty($contentPiece) && strlen($contentPiece) > 0) { + if (!empty($contentPiece)) { $url .= '&c_p=' . urlencode($contentPiece); } - if (!empty($contentTarget) && strlen($contentTarget) > 0) { + if (!empty($contentTarget)) { $url .= '&c_t=' . urlencode($contentTarget); } @@ -1493,15 +1579,15 @@ public function getUrlTrackContentInteraction( * * @see doTrackSiteSearch() */ - public function getUrlTrackSiteSearch(string $keyword, string $category, int $countResults): string + public function getUrlTrackSiteSearch(string $keyword, string $category, ?int $countResults = null): 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; + if ($countResults !== null) { + $url .= '&search_count=' . $countResults; } return $url; @@ -1512,16 +1598,16 @@ public function getUrlTrackSiteSearch(string $keyword, string $category, int $co * * @see doTrackGoal() * @param int $idGoal Id Goal to record a conversion - * @param float $revenue Revenue for this conversion + * @param float|null $revenue Revenue for this conversion. Pass null (default) to omit the + * revenue so Matomo uses the goal's configured revenue; pass 0.0 to force a zero revenue. * @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 + public function getUrlTrackGoal(int $idGoal, ?float $revenue = null): string { $url = $this->getRequest($this->idSite); $url .= '&idgoal=' . $idGoal; - if (!empty($revenue)) { - $revenue = $this->forceDotAsSeparatorForDecimalPoint($revenue); - $url .= '&revenue=' . $revenue; + if ($revenue !== null) { + $url .= '&revenue=' . $this->forceDotAsSeparatorForDecimalPoint($revenue); } return $url; @@ -1532,13 +1618,14 @@ public function getUrlTrackGoal(int $idGoal, float $revenue = 0.0): string * * @see doTrackAction() * @param string $actionUrl URL of the download or outlink - * @param string $actionType Type of the action: 'download' or 'link' + * @param string $actionType Type of the action, usually 'download' or 'link' (a plugin may + * define its own action parameter, so the value is URL-encoded rather than restricted). * @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); + $url .= '&' . urlencode($actionType) . '=' . urlencode($actionUrl); return $url; } @@ -1581,10 +1668,10 @@ public function getUrlTrackCrash( $url .= '&cra_ru=' . urlencode($location); } if ($line) { - $url .= '&cra_rl=' . urlencode($line); + $url .= '&cra_rl=' . urlencode((string) $line); } if ($column) { - $url .= '&cra_rc=' . urlencode($column); + $url .= '&cra_rc=' . urlencode((string) $column); } return $url; @@ -1601,7 +1688,7 @@ public function getUrlTrackCrash( * 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) + public function setForceVisitDateTime(string $dateTime): self { $this->forcedDatetime = $dateTime; @@ -1615,7 +1702,7 @@ public function setForceVisitDateTime(string $dateTime) * If you call setForceNewVisit() before calling doTrack*, then a new visit will be created for this request. * @return $this */ - public function setForceNewVisit() + public function setForceNewVisit(): self { $this->forcedNewVisit = true; @@ -1630,7 +1717,7 @@ public function setForceNewVisit() * @param string $ip IP string, eg. 130.54.2.1 * @return $this */ - public function setIp(string $ip) + public function setIp(string $ip): self { $this->ip = $ip; @@ -1642,11 +1729,14 @@ public function setIp(string $ip) * * 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. + * @param string|null $userId Any user ID string (eg. email address, ID, username). Must be non-empty. + * Set to null to stop sending a User ID on subsequent requests. Note this does not retroactively + * remove the User ID from the visitor's current Matomo visit; for logout isolation, also start a + * new visit with a fresh visitor id (see setForceNewVisit() / setVisitorId()). * @return $this * @throws Exception */ - public function setUserId(string $userId) + public function setUserId(?string $userId): self { if ($userId === '') { throw new Exception("User ID cannot be empty."); @@ -1660,10 +1750,8 @@ public function setUserId(string $userId) * 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 + public static function getUserIdHashed(string $id): string { return substr(sha1($id), 0, 16); } @@ -1680,10 +1768,11 @@ public static function getUserIdHashed($id): string * @return $this * @throws Exception */ - public function setVisitorId(string $visitorId) + public function setVisitorId(string $visitorId): self { - $hexChars = '01234567890abcdefABCDEF'; - if (strlen($visitorId) !== self::LENGTH_VISITOR_ID + $hexChars = self::HEX_CHARACTERS; + if ( + strlen($visitorId) !== self::LENGTH_VISITOR_ID || strspn($visitorId, $hexChars) !== strlen($visitorId) ) { throw new Exception( @@ -1711,12 +1800,12 @@ public function setVisitorId(string $visitorId) * * @return string 16 hex chars visitor ID string */ - public function getVisitorId() + public function getVisitorId(): string { if (!empty($this->forcedVisitorId)) { return $this->forcedVisitorId; } - if ($this->loadVisitorIdCookie()) { + if ($this->loadVisitorIdCookie() && $this->cookieVisitorId !== null) { return $this->cookieVisitorId; } @@ -1725,18 +1814,16 @@ public function getVisitorId() /** * Returns the currently set user agent. - * @return string */ - public function getUserAgent() + public function getUserAgent(): ?string { return $this->userAgent; } /** * Returns the currently set IP address. - * @return string */ - public function getIp() + public function getIp(): ?string { return $this->ip; } @@ -1744,10 +1831,8 @@ public function getIp() /** * Returns the User ID string, which may have been set via: * $v->setUserId('username@example.org'); - * - * @return bool */ - public function getUserId() + public function getUserId(): ?string { return $this->userId; } @@ -1764,14 +1849,20 @@ protected function loadVisitorIdCookie(): bool return false; } $parts = explode('.', $idCookie); - if (strlen($parts[0]) !== self::LENGTH_VISITOR_ID) { + $hexChars = self::HEX_CHARACTERS; + if ( + strlen($parts[0]) !== self::LENGTH_VISITOR_ID + || strspn($parts[0], $hexChars) !== 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]; + if (isset($parts[1])) { + $this->createTs = (int) $parts[1]; + } return true; } @@ -1781,7 +1872,7 @@ protected function loadVisitorIdCookie(): bool */ public function deleteCookies(): void { - $cookies = array('id', 'ses', 'cvar', 'ref'); + $cookies = ['id', 'ses', 'cvar', 'ref']; foreach ($cookies as $cookie) { $this->setCookie($cookie, '', -86400); } @@ -1793,11 +1884,11 @@ public function deleteCookies(): void * 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. + * @return string|false 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() + public function getAttributionInfo(): string|false { if (!empty($this->attributionInfo)) { return json_encode($this->attributionInfo); @@ -1817,7 +1908,7 @@ public function getAttributionInfo() * @param string $token_auth token_auth 32 chars token_auth string * @return $this */ - public function setTokenAuth(string $token_auth) + public function setTokenAuth(#[\SensitiveParameter] string $token_auth): self { $this->token_auth = $token_auth; @@ -1830,7 +1921,7 @@ public function setTokenAuth(string $token_auth) * @param string $time HH:MM:SS format * @return $this */ - public function setLocalTime(string $time) + public function setLocalTime(string $time): self { [$hour, $minute, $second] = explode(':', $time); $this->localHour = (int)$hour; @@ -1847,7 +1938,7 @@ public function setLocalTime(string $time) * @param int $height * @return $this */ - public function setResolution(int $width, int $height) + public function setResolution(int $width, int $height): self { $this->width = $width; $this->height = $height; @@ -1861,7 +1952,7 @@ public function setResolution(int $width, int $height) * * @return $this */ - public function setBrowserHasCookies(bool $hasCookies) + public function setBrowserHasCookies(bool $hasCookies): self { $this->hasCookies = $hasCookies; @@ -1873,7 +1964,7 @@ public function setBrowserHasCookies(bool $hasCookies) * * @return $this */ - public function setDebugStringAppend(string $debugString) + public function setDebugStringAppend(string $debugString): self { $this->DEBUG_APPEND_URL = '&' . $debugString; @@ -1893,7 +1984,7 @@ public function setPlugins( bool $pdf = false, bool $windowsMedia = false, bool $silverlight = false - ) { + ): self { $this->plugins = '&fla=' . (int)$flash . '&java=' . (int)$java . @@ -1918,7 +2009,7 @@ public function disableCookieSupport(): void /** * Returns the maximum number of seconds the tracker will spend waiting for a response - * from Matomo. Defaults to 600 seconds. + * from Matomo. Defaults to 5 seconds. */ public function getRequestTimeout(): int { @@ -1932,7 +2023,7 @@ public function getRequestTimeout(): int * @return $this * @throws Exception */ - public function setRequestTimeout(int $timeout) + public function setRequestTimeout(int $timeout): self { if ($timeout < 0) { throw new Exception("Invalid value supplied for request timeout: $timeout"); @@ -1945,7 +2036,7 @@ public function setRequestTimeout(int $timeout) /** * Returns the maximum number of seconds the tracker will spend trying to connect to Matomo. - * Defaults to 300 seconds. + * Defaults to 2 seconds. */ public function getRequestConnectTimeout(): int { @@ -1959,7 +2050,7 @@ public function getRequestConnectTimeout(): int * @return $this * @throws Exception */ - public function setRequestConnectTimeout(int $timeout) + public function setRequestConnectTimeout(int $timeout): self { if ($timeout < 0) { throw new Exception("Invalid value supplied for request connect timeout: $timeout"); @@ -1979,7 +2070,7 @@ public function setRequestConnectTimeout(int $timeout) * @param string $method Either 'POST' or 'GET' * @return $this */ - public function setRequestMethodNonBulk(string $method) + public function setRequestMethodNonBulk(string $method): self { $this->requestMethod = strtoupper($method) === 'POST' ? 'POST' : 'GET'; @@ -1996,24 +2087,96 @@ public function setProxy(string $proxy, int $proxyPort = 80): void $this->proxyPort = $proxyPort; } + /** + * Sets additional cURL options (a map of CURLOPT_* constant => value) for the tracking + * requests. They are applied after the built-in options, so they can extend them (e.g. + * `CURLOPT_IPRESOLVE`, `CURLOPT_HTTP_VERSION`) or override them. Only used on the cURL + * transport. Overriding core options such as CURLOPT_RETURNTRANSFER or CURLOPT_HEADER may + * break response handling, so use with care. + * + * `CURLOPT_HTTPHEADER` is a special case: any headers supplied here are merged with (appended + * to) the tracker's own headers rather than replacing them, so you can add a custom header + * without accidentally dropping the built-in ones (e.g. the Content-Type for POST/bulk). + * + * @param array $curlOptions + * @return $this + */ + public function setCurlOptions(array $curlOptions): self + { + $this->curlOptions = $curlOptions; + + return $this; + } + + /** + * Controls how failed tracking requests are handled. + * + * By default a request that fails to reach Matomo (DNS, connection or timeout errors) + * throws a RuntimeException. Call setExceptionsEnabled(false) to have such failures return + * false instead, so tracking never breaks the calling application. + * + * @param bool $enabled + * @return $this + */ + public function setExceptionsEnabled(bool $enabled = true): self + { + $this->exceptionsEnabled = $enabled; + + return $this; + } + /** * 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; + if ($this->proxy !== null) { + return $this->proxy . ":" . $this->proxyPort; } return null; } + /** + * Returns the given value with any line breaks removed so it stays a single-line + * value when used in an outbound HTTP request header. + */ + private function normalizeHeaderValue(?string $value): string + { + return str_replace(["\r", "\n"], '', (string) $value); + } + + /** + * Builds a single-line Cookie header value ("a=1; b=2") from the outgoing tracker cookies, + * URL-encoding each name and value. + */ + private function buildOutgoingCookieHeader(): string + { + $pairs = []; + foreach ($this->outgoingTrackerCookies as $name => $value) { + $pairs[] = urlencode((string) $name) . '=' . urlencode($value); + } + + return implode('; ', $pairs); + } + + /** + * Whether the cURL extension is available. Used to choose the transport in sendRequest(); + * overridable so the stream fallback can be exercised in tests. + * + * @ignore + */ + protected function hasCurlSupport(): bool + { + return function_exists('curl_init') && function_exists('curl_exec'); + } + /** * Used in tests to output useful error messages. * * @ignore */ - static public $DEBUG_LAST_REQUESTED_URL = false; + public static string|false $DEBUG_LAST_REQUESTED_URL = false; /** * Returns array of curl options for request @@ -2021,20 +2184,20 @@ private function getProxy(): ?string * @return array */ protected function prepareCurlOptions( - string $url, + #[\SensitiveParameter] string $url, string $method, - $data, + #[\SensitiveParameter] ?string $data, bool $forcePostUrlEncoded ): array { $options = [ CURLOPT_URL => $url, - CURLOPT_USERAGENT => $this->userAgent, + CURLOPT_USERAGENT => $this->normalizeHeaderValue($this->userAgent), CURLOPT_HEADER => true, CURLOPT_TIMEOUT => $this->requestTimeout, CURLOPT_CONNECTTIMEOUT => $this->requestConnectTimeout, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ - 'Accept-Language: ' . $this->acceptLanguage, + 'Accept-Language: ' . $this->normalizeHeaderValue($this->acceptLanguage), ], ]; @@ -2075,8 +2238,20 @@ protected function prepareCurlOptions( } if (!empty($this->outgoingTrackerCookies)) { - $options[CURLOPT_COOKIE] = http_build_query($this->outgoingTrackerCookies); - $this->outgoingTrackerCookies = array(); + $options[CURLOPT_COOKIE] = $this->buildOutgoingCookieHeader(); + $this->outgoingTrackerCookies = []; + } + + // Caller-supplied options are applied last so they can extend or override the defaults. + if (!empty($this->curlOptions)) { + // Preserve the tracker's own HTTP headers: a plain array_replace() would let a caller + // that only wants to add one header silently drop the built-in headers (notably the + // Content-Type for POST/bulk requests, which would make Matomo unable to parse the body). + $ownHeaders = $options[CURLOPT_HTTPHEADER]; + $options = array_replace($options, $this->curlOptions); + if (isset($this->curlOptions[CURLOPT_HTTPHEADER]) && is_array($this->curlOptions[CURLOPT_HTTPHEADER])) { + $options[CURLOPT_HTTPHEADER] = array_merge($ownHeaders, $this->curlOptions[CURLOPT_HTTPHEADER]); + } } return $options; @@ -2087,14 +2262,17 @@ protected function prepareCurlOptions( * * @return array{http: array} */ - protected function prepareStreamOptions(string $method, $data, bool $forcePostUrlEncoded): array + protected function prepareStreamOptions(string $method, #[\SensitiveParameter] ?string $data, bool $forcePostUrlEncoded): array { $stream_options = [ 'http' => [ 'method' => $method, - 'user_agent' => $this->userAgent, - 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n", + 'user_agent' => $this->normalizeHeaderValue($this->userAgent), + 'header' => "Accept-Language: " . $this->normalizeHeaderValue($this->acceptLanguage) . "\r\n", 'timeout' => $this->requestTimeout, + // Return the response body for HTTP error codes (4xx/5xx) instead of returning + // false, so this transport behaves like cURL, which also returns the error body. + 'ignore_errors' => true, ], ]; @@ -2113,8 +2291,8 @@ protected function prepareStreamOptions(string $method, $data, bool $forcePostUr } if (!empty($this->outgoingTrackerCookies)) { - $stream_options['http']['header'] .= 'Cookie: ' . http_build_query($this->outgoingTrackerCookies) . "\r\n"; - $this->outgoingTrackerCookies = array(); + $stream_options['http']['header'] .= 'Cookie: ' . $this->buildOutgoingCookieHeader() . "\r\n"; + $this->outgoingTrackerCookies = []; } return $stream_options; @@ -2123,7 +2301,7 @@ protected function prepareStreamOptions(string $method, $data, bool $forcePostUr /** * @ignore */ - protected function sendRequest(string $url, string $method = 'GET', $data = null, bool $force = false): string + protected function sendRequest(#[\SensitiveParameter] string $url, string $method = 'GET', #[\SensitiveParameter] ?string $data = null, bool $force = false): string|bool { self::$DEBUG_LAST_REQUESTED_URL = $url; @@ -2138,9 +2316,9 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $this->clearCustomVariables(); $this->clearCustomDimensions(); $this->clearCustomTrackingParameters(); - $this->userAgent = false; - $this->clientHints = false; - $this->acceptLanguage = false; + $this->userAgent = null; + $this->clientHints = []; + $this->acceptLanguage = null; return true; } @@ -2152,7 +2330,7 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $urlParts = explode('?', $url); $url = $urlParts[0]; - $data = $urlParts[1]; + $data = $urlParts[1] ?? ''; $forcePostUrlEncoded = true; $method = 'POST'; @@ -2162,14 +2340,18 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $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 + // Only post token_auth but use GET URL parameters for everything else. + // The request must actually be a POST, otherwise Matomo reads $_GET/$_POST and + // never sees a token sent in the body (this matters on the stream transport; + // cURL forces POST via CURLOPT_POST below). $forcePostUrlEncoded = true; + $method = 'POST'; 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)) { + } else { // Use GET for all URL parameters $url .= $appendTokenString; } @@ -2178,7 +2360,7 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $content = ''; - if (function_exists('curl_init') && function_exists('curl_exec')) { + if ($this->hasCurlSupport()) { $options = $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded); $ch = curl_init(); @@ -2192,13 +2374,17 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null if ($response === false) { $curlError = curl_error($ch); if (!empty($curlError)) { - throw new \RuntimeException($curlError); + if ($this->exceptionsEnabled) { + throw new \RuntimeException($curlError); + } + // fail-safe: a failed tracking request must not break the calling application + $content = false; } } - if (!empty($response)) { + if (!empty($response) && is_string($response)) { // extract header - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($response, 0, $headerSize); // extract content @@ -2207,25 +2393,32 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null $this->parseIncomingCookies(explode("\r\n", $header)); } finally { - // curl_close has no effect since PHP 8.0 - if (PHP_VERSION_ID < 80000) { - 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); + $response = @file_get_contents($url, false, $ctx); + if ($response === false && $this->exceptionsEnabled) { + // Only include the host (never the query string, which carries token_auth/PII) in the message. + throw new \RuntimeException('Failed to send the tracking request to ' . (parse_url($url, PHP_URL_HOST) ?: 'the Matomo server')); + } $content = $response; + $responseHeaders = []; if (function_exists('http_get_last_response_headers')) { - $http_response_header = http_get_last_response_headers(); + $headers = http_get_last_response_headers(); + if (is_array($headers)) { + $responseHeaders = $headers; + } + } elseif ($response !== false) { + // PHP populates $http_response_header in the local scope whenever an HTTP response + // was received; the $response !== false guard guarantees that is the case here. + $responseHeaders = $http_response_header; } - $this->parseIncomingCookies($http_response_header); + $this->parseIncomingCookies($responseHeaders); } return $content; @@ -2233,13 +2426,17 @@ protected function sendRequest(string $url, string $method = 'GET', $data = null /** * Returns current timestamp, or forced timestamp/datetime if it was set - * @return string|int */ - protected function getTimestamp() + protected function getTimestamp(): int { - return !empty($this->forcedDatetime) - ? strtotime($this->forcedDatetime) - : time(); + if (!empty($this->forcedDatetime)) { + $timestamp = strtotime($this->forcedDatetime); + if ($timestamp !== false) { + return $timestamp; + } + } + + return time(); } /** @@ -2259,7 +2456,8 @@ protected function getBaseUrl(): string MatomoTracker::$URL = \'http://your-website.org/matomo/\';' ); } - if (strpos($apiUrl, '/matomo.php') === false + if ( + strpos($apiUrl, '/matomo.php') === false && strpos($apiUrl, '/proxy-matomo.php') === false ) { $apiUrl = rtrim($apiUrl, '/'); @@ -2296,15 +2494,15 @@ protected function getRequest(int $idSite): string 'idsite=' . $idSite . '&rec=1' . '&apiv=' . self::VERSION . - '&r=' . substr(strval(mt_rand()), 2, 6) . + '&r=' . substr((string) 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']) : '') . + '&XDEBUG_SESSION_START=' . urlencode(self::toStringValue($_GET['XDEBUG_SESSION_START'])) : '') . + (!empty($_GET['KEY']) ? '&KEY=' . urlencode(self::toStringValue($_GET['KEY'])) : '') . // Only allowed for Admin/Super User, token_auth required, - ((!empty($this->ip) && !empty($this->token_auth)) ? '&cip=' . $this->ip : '') . + ((!empty($this->ip) && !empty($this->token_auth)) ? '&cip=' . urlencode($this->ip) : '') . (!empty($this->userId) ? '&uid=' . urlencode($this->userId) : '') . (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') . (!empty($this->forcedNewVisit) ? '&new_visit=1' : '') . @@ -2314,60 +2512,60 @@ protected function getRequest(int $idSite): string // 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) ? + (($this->localHour !== null && $this->localMinute !== null && $this->localSecond !== null) ? '&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 : '') . + (!empty($this->hasCookies) ? '&cookie=' . (int) $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->customData) ? '&data=' . urlencode($this->customData) : '') . + (!empty($this->visitorCustomVar) ? '&_cvar=' . urlencode((string) json_encode($this->visitorCustomVar)) : '') . + (!empty($this->pageCustomVar) ? '&cvar=' . urlencode((string) json_encode($this->pageCustomVar)) : '') . + (!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode((string) json_encode($this->eventCustomVar)) : '') . (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) . // URL parameters - '&url=' . urlencode($this->pageUrl ?? '') . + '&url=' . urlencode($this->pageUrl) . '&urlref=' . urlencode($this->urlReferrer ?? '') . ((!empty($this->pageCharset) && $this->pageCharset != self::DEFAULT_CHARSET_PARAMETER_VALUES) ? - '&cs=' . $this->pageCharset : '') . + '&cs=' . urlencode($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]) : '') . + (!empty($this->attributionInfo[0]) ? '&_rcn=' . urlencode(self::toStringValue($this->attributionInfo[0])) : '') . // Campaign keyword - (!empty($this->attributionInfo[1]) ? '&_rck=' . urlencode($this->attributionInfo[1]) : '') . + (!empty($this->attributionInfo[1]) ? '&_rck=' . urlencode(self::toStringValue($this->attributionInfo[1])) : '') . // Timestamp at which the referrer was set - (!empty($this->attributionInfo[2]) ? '&_refts=' . $this->attributionInfo[2] : '') . + (!empty($this->attributionInfo[2]) ? '&_refts=' . urlencode(self::toStringValue($this->attributionInfo[2])) : '') . // Referrer URL - (!empty($this->attributionInfo[3]) ? '&_ref=' . urlencode($this->attributionInfo[3]) : '') . + (!empty($this->attributionInfo[3]) ? '&_ref=' . urlencode(self::toStringValue($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) : '') . + ($this->lat !== null ? '&lat=' . urlencode((string) $this->lat) : '') . + ($this->long !== null ? '&long=' . urlencode((string) $this->long) : '') . $customFields . $customDimensions . (!$this->sendImageResponse ? '&send_image=0' : '') . // client hints - (!empty($this->clientHints) ? ('&uadata=' . urlencode(json_encode($this->clientHints))) : '') . + (!empty($this->clientHints) ? ('&uadata=' . urlencode((string) 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->networkTime !== null ? '&pf_net=' . $this->networkTime : '') . + ($this->serverTime !== null ? '&pf_srv=' . $this->serverTime : '') . + ($this->transferTime !== null ? '&pf_tfr=' . $this->transferTime : '') . + ($this->domProcessingTime !== null ? '&pf_dm1=' . $this->domProcessingTime : '') . + ($this->domCompletionTime !== null ? '&pf_dm2=' . $this->domCompletionTime : '') . + ($this->onLoadTime !== null ? '&pf_onl=' . $this->onLoadTime : ''); $this->clearPerformanceTimings(); } @@ -2375,10 +2573,16 @@ protected function getRequest(int $idSite): string $url .= '&' . $param . '=' . urlencode($value); } + // Raw debug parameters are appended last so they override any built-in parameter of the same name. + foreach ($this->debugParameters as $param => $value) { + $url .= '&' . urlencode($param) . '=' . urlencode($value); + } + // Reset page level custom variables after this page view $this->ecommerceView = []; $this->pageCustomVar = []; $this->eventCustomVar = []; + $this->debugParameters = []; $this->clearCustomDimensions(); $this->clearCustomTrackingParameters(); @@ -2392,25 +2596,23 @@ protected function getRequest(int $idSite): string /** * Returns a first party cookie which name contains $name * - * @return string String value of cookie, or false if not found + * @return string|false String value of cookie, or false if not found * @ignore */ - protected function getCookieMatchingName(string $name) + protected function getCookieMatchingName(string $name): string|false { 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; + // cookie names that are numeric strings are exposed as integer array keys + if (strpos((string) $cookieName, $name) !== false) { + return self::toStringValue($cookieValue); } } @@ -2427,18 +2629,19 @@ protected static function getCurrentScriptName(): string { $url = ''; if (!empty($_SERVER['PATH_INFO'])) { - $url = $_SERVER['PATH_INFO']; + $url = self::toStringValue($_SERVER['PATH_INFO']); } else { if (!empty($_SERVER['REQUEST_URI'])) { - if (($pos = strpos($_SERVER['REQUEST_URI'], '?')) !== false) { - $url = substr($_SERVER['REQUEST_URI'], 0, $pos); + $requestUri = self::toStringValue($_SERVER['REQUEST_URI']); + if (($pos = strpos($requestUri, '?')) !== false) { + $url = substr($requestUri, 0, $pos); } else { - $url = $_SERVER['REQUEST_URI']; + $url = $requestUri; } } } if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) { - $url = $_SERVER['SCRIPT_NAME']; + $url = self::toStringValue($_SERVER['SCRIPT_NAME']); } elseif (empty($url)) { $url = '/'; } @@ -2459,7 +2662,8 @@ protected static function getCurrentScriptName(): string */ protected static function getCurrentScheme(): string { - if (isset($_SERVER['HTTPS']) + if ( + isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] === true) ) { return 'https'; @@ -2477,7 +2681,7 @@ protected static function getCurrentScheme(): string protected static function getCurrentHost(): string { if (isset($_SERVER['HTTP_HOST'])) { - return $_SERVER['HTTP_HOST']; + return self::toStringValue($_SERVER['HTTP_HOST']); } return 'unknown'; @@ -2492,10 +2696,8 @@ protected static function getCurrentHost(): string protected static function getCurrentQueryString(): string { $url = ''; - if (isset($_SERVER['QUERY_STRING']) - && !empty($_SERVER['QUERY_STRING']) - ) { - $url .= '?' . $_SERVER['QUERY_STRING']; + if (!empty($_SERVER['QUERY_STRING'])) { + $url .= '?' . self::toStringValue($_SERVER['QUERY_STRING']); } return $url; @@ -2514,12 +2716,23 @@ protected static function getCurrentUrl(): string . self::getCurrentQueryString(); } + /** + * Safely converts a request value of unknown type (e.g. a superglobal entry) to a string. + * Non-scalar values (arrays, objects) become an empty string. + * + * @ignore + */ + protected static function toStringValue(mixed $value): string + { + return is_scalar($value) ? (string) $value : ''; + } + /** * 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() + protected function setFirstPartyCookies(): self { if ($this->configCookiesDisabled) { return $this; @@ -2543,7 +2756,7 @@ protected function setFirstPartyCookies() $this->setCookie('id', $cookieValue, $this->configVisitorCookieTimeout); // Set the 'cvar' cookie - $this->setCookie('cvar', json_encode($this->visitorCustomVar), $this->configSessionCookieTimeout); + $this->setCookie('cvar', (string) json_encode($this->visitorCustomVar), $this->configSessionCookieTimeout); return $this; } @@ -2554,7 +2767,7 @@ protected function setFirstPartyCookies() * * @return $this */ - protected function setCookie(string $cookieName, $cookieValue, int $cookieTTL) + protected function setCookie(string $cookieName, string $cookieValue, int $cookieTTL): self { $cookieExpire = $this->currentTs + $cookieTTL; if (!headers_sent()) { @@ -2572,30 +2785,42 @@ protected function setCookie(string $cookieName, $cookieValue, int $cookieTTL) } /** - * @return array + * @return array */ - protected function getCustomVariablesFromCookie() + protected function getCustomVariablesFromCookie(): array { $cookie = $this->getCookieMatchingName('cvar'); if (!$cookie) { return []; } - return json_decode($cookie, true); + $decoded = json_decode($cookie, true); + + if (!is_array($decoded)) { + return []; + } + + $customVariables = []; + foreach ($decoded as $id => $pair) { + if (is_array($pair) && isset($pair[0], $pair[1])) { + $customVariables[(int) $id] = [self::toStringValue($pair[0]), self::toStringValue($pair[1])]; + } + } + + return $customVariables; } /** * Sets a cookie to be sent to the tracking server. * - * @param $name - * @param $value + * @param string $name + * @param string|null $value Cookie value, or null to remove a previously set cookie. */ - public function setOutgoingTrackerCookie($name, $value) + public function setOutgoingTrackerCookie(string $name, ?string $value): void { if ($value === null) { unset($this->outgoingTrackerCookies[$name]); - } - else { + } else { $this->outgoingTrackerCookies[$name] = $value; } } @@ -2603,53 +2828,55 @@ public function setOutgoingTrackerCookie($name, $value) /** * Gets a cookie which was set by the tracking server. * - * @param $name + * @param string $name * - * @return bool|string + * @return string|false The cookie value, or false if no cookie with the given name was received. */ - public function getIncomingTrackerCookie($name) + public function getIncomingTrackerCookie(string $name): string|false { - if (isset($this->incomingTrackerCookies[$name])) { - return $this->incomingTrackerCookies[$name]; - } - - return false; + return $this->incomingTrackerCookies[$name] ?? false; } /** * Reads incoming tracking server cookies. * - * @param array $headers Array with HTTP response headers as values + * @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); + $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); + foreach ($headers as $header) { + $header = self::toStringValue($header); + if (strpos(strtolower($header), $headerName) !== 0) { + continue; + } + $cookie = trim(substr($header, $headerNameLength)); + $posEnd = strpos($cookie, ';'); + if ($posEnd !== false) { + $cookie = substr($cookie, 0, $posEnd); } + // Parse only the first "=" so each cookie accumulates (parse_str would overwrite the + // whole set per header and apply query-string bracket semantics to the names). + $eqPos = strpos($cookie, '='); + if ($eqPos === false) { + continue; + } + $name = urldecode(trim(substr($cookie, 0, $eqPos))); + $value = urldecode(trim(substr($cookie, $eqPos + 1))); + $this->incomingTrackerCookies[$name] = $value; } } /** * Returns true if the given user agent belongs to a known AI bot. * - * @param string $userAgent - * @return bool + * @param string|null $userAgent */ - public static function isUserAgentAIBot(string $userAgent): bool + public static function isUserAgentAIBot(?string $userAgent): bool { if (empty($userAgent)) { return false; @@ -2667,11 +2894,11 @@ public static function isUserAgentAIBot(string $userAgent): bool /** * Helper function to quickly generate the URL to track a page view. * - * @param $idSite + * @param int $idSite * @param string $documentTitle * @return string */ -function Matomo_getUrlTrackPageView($idSite, $documentTitle = '') +function Matomo_getUrlTrackPageView(int $idSite, string $documentTitle = ''): string { $tracker = new MatomoTracker($idSite); @@ -2681,12 +2908,12 @@ function Matomo_getUrlTrackPageView($idSite, $documentTitle = '') /** * Helper function to quickly generate the URL to track a goal. * - * @param $idSite - * @param $idGoal - * @param float $revenue + * @param int $idSite + * @param int $idGoal + * @param float|null $revenue * @return string */ -function Matomo_getUrlTrackGoal($idSite, $idGoal, $revenue = 0.0) +function Matomo_getUrlTrackGoal(int $idSite, int $idGoal, ?float $revenue = null): string { $tracker = new MatomoTracker($idSite); diff --git a/PiwikTracker.php b/PiwikTracker.php index ea5ce68..a221dae 100644 --- a/PiwikTracker.php +++ b/PiwikTracker.php @@ -1,4 +1,5 @@ doTrackPageView($matomoPageTitle); ``` ## Requirements: +* PHP 8.1 or newer * JSON extension (json_decode, json_encode) * cURL or stream extension (to issue the HTTPS request to Matomo) @@ -48,6 +49,43 @@ Alternatively, you can download the files and require the Matomo tracker manuall require_once("MatomoTracker.php"); ``` +## Error handling and timeouts + +By default a tracking request that fails to reach Matomo (DNS, connection or timeout errors) +throws a `RuntimeException`, so if you call the tracker inline in a page you should either wrap +it in a `try`/`catch` or opt into fail-safe behavior: + +```php +$matomoTracker->setExceptionsEnabled(false); // failed requests return false instead of throwing +``` + +The default timeouts are intentionally short so a slow or unreachable Matomo cannot block the +calling page for long: **5 seconds** total and **2 seconds** to connect. Raise them for slow +endpoints or large synchronous imports: + +```php +$matomoTracker->setRequestTimeout(30); // seconds, total +$matomoTracker->setRequestConnectTimeout(5); // seconds, connect +``` + +Combined, this means an unreachable Matomo throws (or, in fail-safe mode, returns `false`) after +at most a few seconds rather than hanging the request. Bulk tracking (`doBulkTrack()`) uses a more +generous timeout automatically and keeps the queued actions if a batch fails so it can be retried. + +## Development + +Install the development dependencies with Composer and use the provided scripts: + +``` +composer install +composer test # run the PHPUnit test suite +composer phpstan # run static analysis (PHPStan, max level) +composer phpcs # check the coding standard (Matomo) +composer phpcbf # auto-fix coding standard violations +``` + +PHPStan and PHP_CodeSniffer are also run for every pull request via GitHub Actions. + ## License Released under the [BSD License](https://opensource.org/licenses/BSD-3-Clause) diff --git a/composer.json b/composer.json index c740a25..e54bbfd 100644 --- a/composer.json +++ b/composer.json @@ -17,12 +17,18 @@ "source": "https://github.com/matomo-org/matomo-php-tracker" }, "require": { - "php": "~7.2 || ~7.3 || ~7.4 || ~8.0 || ~8.1 || ~8.2 || ~8.3 || ~8.4 || ~8.5", + "php": "^8.1", "ext-json": "*" }, "suggest": { "ext-curl": "Using this extension to issue the HTTPS request to Matomo" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/matomo-org/matomo-coding-standards.git" + } + ], "autoload": { "classmap": ["."] }, @@ -32,6 +38,24 @@ } }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.3 || ^10.1" + "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^2", + "squizlabs/php_codesniffer": "^3.10", + "matomo-org/matomo-coding-standards": "dev-master", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "config": { + "platform": { + "php": "8.1.0" + }, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "scripts": { + "test": "phpunit", + "phpstan": "phpstan analyse", + "phpcs": "phpcs", + "phpcbf": "phpcbf" } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..8e2d458 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,38 @@ + + + Matomo PHP Tracker Coding Standard + + + + + + MatomoTracker.php + PiwikTracker.php + tests + + + + + + + + + + + + + + MatomoTracker.php + PiwikTracker.php + + + + + MatomoTracker.php + PiwikTracker.php + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..8228e29 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: max + phpVersion: 80100 + paths: + - MatomoTracker.php + - PiwikTracker.php + - tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cb8caf5..4df188b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,12 +1,14 @@ - - - - - ./tests/Unit - - + + + + ./tests/Unit + + + + + MatomoTracker.php + PiwikTracker.php + + diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index dc68871..670fe19 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -1,7 +1,5 @@ setUrl('http://somesite.com'); + + return $tracker; + } + + /** + * @return array|string> + */ + private static function parseQueryParams(string $url): array + { + $queryStr = parse_url($url, PHP_URL_QUERY); + self::assertIsString($queryStr); + parse_str($queryStr, $query); + + /** @var array|string> $query */ + return $query; + } - public function test_trackingWithCookieSetsCorrectUrl() + public function testTrackingWithCookieSetsCorrectUrl(): void { $testVisitorId = substr(md5('testuuid'), 0, 16); $this->assertEquals(16, strlen($testVisitorId)); @@ -32,13 +63,12 @@ public function test_trackingWithCookieSetsCorrectUrl() $cookieName = '_pk_id_1_f609'; $_COOKIE[$cookieName] = $testVisitorId . '.' . $createTs; - $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker = new \MatomoTracker(1, self::TEST_URL); $tracker->setUrl('http://somesite.com'); $url = $tracker->getUrlTrackPageView('test title'); - $url = preg_replace('/&r=\d+/', "", $url); + $url = (string) preg_replace('/&r=\d+/', "", $url); - $queryStr = parse_url($url, PHP_URL_QUERY); - parse_str($queryStr, $query); + $query = self::parseQueryParams($url); $this->assertEquals($testVisitorId, $query['_id']); $this->assertEquals($createTs, $query['_idts']); @@ -47,7 +77,7 @@ public function test_trackingWithCookieSetsCorrectUrl() $this->assertEquals($expected, $url); } - public function test_trackingWithPreMatomo4CookieSetsCorrectUrl() + public function testTrackingWithPreMatomo4CookieSetsCorrectUrl(): void { $testVisitorId = substr(md5('testother'), 0, 16); $this->assertEquals(16, strlen($testVisitorId)); @@ -60,13 +90,12 @@ public function test_trackingWithPreMatomo4CookieSetsCorrectUrl() $cookieName = '_pk_id_1_f609'; $_COOKIE[$cookieName] = $testVisitorId . '.' . $createTs . '.5.' . $currentTs . '.' . $lastVisitTs . '.' . $ecommerceLastOrderTs; - $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker = new \MatomoTracker(1, self::TEST_URL); $tracker->setUrl('http://somesite.com'); $url = $tracker->getUrlTrackPageView('test title'); - $url = preg_replace('/&r=\d+/', "", $url); + $url = (string) preg_replace('/&r=\d+/', "", $url); - $queryStr = parse_url($url, PHP_URL_QUERY); - parse_str($queryStr, $query); + $query = self::parseQueryParams($url); $this->assertEquals($testVisitorId, $query['_id']); $this->assertEquals($createTs, $query['_idts']); @@ -75,7 +104,18 @@ public function test_trackingWithPreMatomo4CookieSetsCorrectUrl() $this->assertEquals($expected, $url); } - public function test_setApiUrl() + public function testTrackingWithNumericCookieNameDoesNotFail(): void + { + // numeric cookie names are exposed as integer keys in $_COOKIE + $_COOKIE[12345] = 'some-value'; + $_COOKIE['_pk_cvar_1_f609'] = '{"1":["name","value"]}'; + + $tracker = new \MatomoTracker(1, self::TEST_URL); + + $this->assertSame(['name', 'value'], $tracker->getCustomVariable(1)); + } + + public function testSetApiUrl(): void { $newApiUrl = 'https://NEW-API-URL.com'; $tracker = new \MatomoTracker(1, self::TEST_URL); @@ -85,15 +125,49 @@ public function test_setApiUrl() $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); } + public function testUsageApiUrl(): void + { + $newApiUrl = 'https://NEW-API-URL.com'; + $tracker = new \MatomoTracker(1, $newApiUrl); + $url = $tracker->getUrlTrackPageView('test title'); + + $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); + } + + public function testGetBaseUrlThrowsWhenNoUrlConfigured(): void + { + $tracker = new TestableMatomoTracker(1); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('You must first set the Matomo Tracker URL'); + + $tracker->callGetBaseUrl(); + } + + public function testGetBaseUrlAppendsMatomoPhp(): void + { + $tracker = new TestableMatomoTracker(1, 'http://example.org/matomo/'); + $this->assertSame('http://example.org/matomo/matomo.php', $tracker->callGetBaseUrl()); + + $tracker = new TestableMatomoTracker(1, 'http://example.org/matomo.php'); + $this->assertSame('http://example.org/matomo.php', $tracker->callGetBaseUrl()); + + $tracker = new TestableMatomoTracker(1, 'http://example.org/proxy-matomo.php'); + $this->assertSame('http://example.org/proxy-matomo.php', $tracker->callGetBaseUrl()); + } + /** * @dataProvider getTestDataForIsUserAgentAIBot */ - public function test_isUserAgentAIBot($userAgent, $expected) + public function testIsUserAgentAIBot(string $userAgent, bool $expected): void { $this->assertSame($expected, \MatomoTracker::isUserAgentAIBot($userAgent)); } - public function getTestDataForIsUserAgentAIBot(): array + /** + * @return list + */ + public static function getTestDataForIsUserAgentAIBot(): array { return [ ['', false], @@ -111,14 +185,19 @@ public function getTestDataForIsUserAgentAIBot(): array ]; } + public function testIsUserAgentAIBotWithNull(): void + { + $this->assertFalse(\MatomoTracker::isUserAgentAIBot(null)); + } + /** * @dataProvider getTestDataForGetUrlTrackAIBot */ - public function test_getUrlTrackAIBot(?int $httpStatus, ?int $responseSizeBytes, ?int $serverTimeMs, ?string $source, string $expected) + public function testGetUrlTrackAIBot(?int $httpStatus, ?int $responseSizeBytes, ?int $serverTimeMs, ?string $source, string $expected): void { $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot'; - $tracker = new \MatomoTracker(1, $apiUrl = self::TEST_URL); + $tracker = new \MatomoTracker(1, self::TEST_URL); $tracker->setUrl('https://example.com/page'); $tracker->setVisitorId('abcdef01234517ab'); @@ -128,7 +207,10 @@ public function test_getUrlTrackAIBot(?int $httpStatus, ?int $responseSizeBytes, $this->assertEquals($expected, $actual); } - public function getTestDataForGetUrlTrackAIBot(): array + /** + * @return list + */ + public static function getTestDataForGetUrlTrackAIBot(): array { return [ [ @@ -157,7 +239,32 @@ public function getTestDataForGetUrlTrackAIBot(): array ]; } - private function normalizeTrackingUrl(string $url) + public function testDoTrackPageViewIfAIBotWithRegularUserAgentReturnsNull(): void + { + $tracker = $this->createTracker(); + $tracker->setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + + $this->assertNull($tracker->doTrackPageViewIfAIBot(200)); + $this->assertSame([], $tracker->capturedRequests); + } + + public function testDoTrackPageViewIfAIBotWithBotUserAgentTracks(): void + { + $tracker = $this->createTracker(); + $tracker->setUserAgent('compatible; ChatGPT-User/1.0; +https://openai.com/bot'); + + $response = $tracker->doTrackPageViewIfAIBot(200, 1024, 55, 'wordpress'); + + $this->assertSame('mock-response', $response); + $query = self::parseQueryParams($tracker->lastRequestUrl()); + $this->assertSame('1', $query['recMode']); + $this->assertSame('200', $query['http_status']); + $this->assertSame('1024', $query['bw_bytes']); + $this->assertSame('55', $query['pf_srv']); + $this->assertSame('wordpress', $query['source']); + } + + private function normalizeTrackingUrl(string $url): string { $nonDeterministicParams = [ 'r', @@ -165,18 +272,1632 @@ private function normalizeTrackingUrl(string $url) ]; foreach ($nonDeterministicParams as $param) { - $url = preg_replace('/&' . preg_quote($param) . '=[^&]+/', '&r=', $url); + $url = (string) preg_replace('/&' . preg_quote($param, '/') . '=[^&]+/', '&r=', $url); } return $url; } - public function testUsageApiUrl(): void + public function testDoTrackPageViewGeneratesNewPageviewId(): void { - $newApiUrl = 'https://NEW-API-URL.com'; - $tracker = new \MatomoTracker(1, $newApiUrl); - $url = $tracker->getUrlTrackPageView('test title'); + $tracker = $this->createTracker(); + $tracker->doTrackPageView('page one'); + $firstId = $tracker->getPageviewId(); - $this->assertSame(substr($url, 0, strlen($newApiUrl)), $newApiUrl); + $tracker->doTrackPageView('page two'); + $secondId = $tracker->getPageviewId(); + + $this->assertNotNull($firstId); + $this->assertNotNull($secondId); + $this->assertSame(6, strlen($firstId)); + $this->assertNotSame($firstId, $secondId); + + $query = self::parseQueryParams($tracker->lastRequestUrl()); + $this->assertSame($secondId, $query['pv_id']); + $this->assertSame('page two', $query['action_name']); + } + + public function testSetPageviewIdIsKeptAcrossPageViews(): void + { + $tracker = $this->createTracker(); + $tracker->setPageviewId('custom'); + $tracker->doTrackPageView('page one'); + $tracker->doTrackPageView('page two'); + + $this->assertSame('custom', $tracker->getPageviewId()); + } + + public function testGetUrlTrackPageViewWithoutTitle(): void + { + $tracker = $this->createTracker(); + $url = $tracker->getUrlTrackPageView(); + + $this->assertStringNotContainsString('action_name', $url); + } + + public function testGetUrlTrackEventRequiresCategoryAndAction(): void + { + $tracker = $this->createTracker(); + + try { + $tracker->getUrlTrackEvent('', 'action'); + $this->fail('Expected exception for empty category'); + } catch (Exception $e) { + $this->assertStringContainsString('Category', $e->getMessage()); + } + + $this->expectException(Exception::class); + $tracker->getUrlTrackEvent('category', ''); + } + + public function testGetUrlTrackEventDefaultsOmitNameAndValue(): void + { + $tracker = $this->createTracker(); + $url = $tracker->getUrlTrackEvent('cat', 'act'); + + $this->assertStringContainsString('&e_c=cat', $url); + $this->assertStringContainsString('&e_a=act', $url); + $this->assertStringContainsString('&ca=1', $url); + $this->assertStringNotContainsString('&e_n=', $url); + $this->assertStringNotContainsString('&e_v=', $url); + + // a plain page view is not a custom action + $this->assertStringNotContainsString('&ca=1', $tracker->getUrlTrackPageView('title')); + } + + public function testGetUrlTrackEventWithNameAndValues(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackEvent('cat', 'act', 'name', 0); + $this->assertStringContainsString('&e_n=name', $url); + $this->assertStringContainsString('&e_v=0', $url); + + $url = $tracker->getUrlTrackEvent('cat', 'act', 'name', 3.5); + $this->assertStringContainsString('&e_v=3.5', $url); + + // an empty name is not sent + $url = $tracker->getUrlTrackEvent('cat', 'act', '', 1); + $this->assertStringNotContainsString('&e_n=', $url); + } + + public function testDoTrackEventSendsRequest(): void + { + $tracker = $this->createTracker(); + $response = $tracker->doTrackEvent('cat', 'act', 'name', 2); + + $this->assertSame('mock-response', $response); + $this->assertCount(1, $tracker->capturedRequests); + } + + public function testGetUrlTrackContentImpression(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackContentImpression('name', 'piece', 'http://target.example'); + $query = self::parseQueryParams($url); + $this->assertSame('name', $query['c_n']); + $this->assertSame('piece', $query['c_p']); + $this->assertSame('http://target.example', $query['c_t']); + $this->assertSame('1', $query['ca']); + + $url = $tracker->getUrlTrackContentImpression('name', '', null); + $this->assertStringNotContainsString('&c_p=', $url); + $this->assertStringNotContainsString('&c_t=', $url); + + $this->expectException(Exception::class); + $tracker->getUrlTrackContentImpression('', 'piece', null); + } + + public function testGetUrlTrackContentInteraction(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackContentInteraction('click', 'name', 'piece', 'http://target.example'); + $query = self::parseQueryParams($url); + $this->assertSame('click', $query['c_i']); + $this->assertSame('name', $query['c_n']); + $this->assertSame('piece', $query['c_p']); + $this->assertSame('http://target.example', $query['c_t']); + $this->assertSame('1', $query['ca']); + + $url = $tracker->getUrlTrackContentInteraction('click', 'name', '', null); + $this->assertStringNotContainsString('&c_p=', $url); + $this->assertStringNotContainsString('&c_t=', $url); + } + + public function testGetUrlTrackContentInteractionRequiresInteractionAndName(): void + { + $tracker = $this->createTracker(); + + try { + $tracker->getUrlTrackContentInteraction('', 'name', 'piece', null); + $this->fail('Expected exception for empty interaction'); + } catch (Exception $e) { + $this->assertStringContainsString('interaction', $e->getMessage()); + } + + $this->expectException(Exception::class); + $tracker->getUrlTrackContentInteraction('click', '', 'piece', null); + } + + public function testDoTrackContentImpressionAndInteractionSendRequests(): void + { + $tracker = $this->createTracker(); + $tracker->doTrackContentImpression('name'); + $tracker->doTrackContentInteraction('click', 'name'); + + $this->assertCount(2, $tracker->capturedRequests); + } + + public function testGetUrlTrackSiteSearchOmitsCountByDefault(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackSiteSearch('keyword', ''); + $this->assertStringContainsString('&search=keyword', $url); + $this->assertStringNotContainsString('&search_cat=', $url); + $this->assertStringNotContainsString('&search_count=', $url); + } + + public function testGetUrlTrackSiteSearchWithCategoryAndZeroCount(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackSiteSearch('keyword', 'category', 0); + $this->assertStringContainsString('&search_cat=category', $url); + $this->assertStringContainsString('&search_count=0', $url); + + $url = $tracker->getUrlTrackSiteSearch('keyword', '', 12); + $this->assertStringContainsString('&search_count=12', $url); + } + + public function testDoTrackSiteSearchSendsRequest(): void + { + $tracker = $this->createTracker(); + $tracker->doTrackSiteSearch('keyword'); + + $this->assertStringContainsString('&search=keyword', $tracker->lastRequestUrl()); + } + + public function testGetUrlTrackGoal(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackGoal(42); + $this->assertStringContainsString('&idgoal=42', $url); + $this->assertStringNotContainsString('&revenue=', $url); + + $url = $tracker->getUrlTrackGoal(42, 3.5); + $this->assertStringContainsString('&revenue=3.5', $url); + } + + public function testDoTrackGoalSendsRequest(): void + { + $tracker = $this->createTracker(); + $tracker->doTrackGoal(7, 1.25); + + $this->assertStringContainsString('&idgoal=7', $tracker->lastRequestUrl()); + } + + public function testGetUrlTrackActionAndDoTrackAction(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackAction('http://example.org/file.zip', 'download'); + $this->assertStringContainsString('&download=' . urlencode('http://example.org/file.zip'), $url); + + $tracker->doTrackAction('http://example.org/out', 'link'); + $this->assertStringContainsString('&link=' . urlencode('http://example.org/out'), $tracker->lastRequestUrl()); + } + + public function testGetUrlTrackActionEncodesTheActionType(): void + { + $tracker = $this->createTracker(); + + // A crafted action type must be URL-encoded into a single parameter name and must not be + // able to inject an additional query-string parameter of its own. + $url = $tracker->getUrlTrackAction('http://example.org/file.zip', 'download&extra=1'); + + $this->assertStringContainsString('&' . urlencode('download&extra=1') . '=', $url); + $this->assertStringNotContainsString('&extra=1', $url); + } + + /** + * @return \MatomoTracker a tracker that always uses the stream transport (no cURL) + */ + private function createStreamTracker(string $apiUrl): \MatomoTracker + { + $tracker = new class (1, $apiUrl) extends \MatomoTracker { + protected function hasCurlSupport(): bool + { + return false; + } + }; + $tracker->setUrl('http://somesite.com'); + + return $tracker; + } + + public function testStreamTransportThrowsHostOnlyMessageOnFailure(): void + { + // Port 1 on loopback refuses the connection immediately, so the stream transport fails fast. + $tracker = $this->createStreamTracker('http://127.0.0.1:1/matomo.php'); + $tracker->setTokenAuth(str_repeat('a', 32)); + + try { + $tracker->doTrackPageView('secret title'); + $this->fail('Expected a RuntimeException from the failing stream request.'); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('127.0.0.1', $e->getMessage()); + // The query string (which carries token_auth and other PII) must never leak into the message. + $this->assertStringNotContainsString('token_auth', $e->getMessage()); + $this->assertStringNotContainsString('action_name', $e->getMessage()); + } + } + + public function testStreamTransportFailSafeReturnsFalseWhenExceptionsDisabled(): void + { + $tracker = $this->createStreamTracker('http://127.0.0.1:1/matomo.php'); + $tracker->setExceptionsEnabled(false); + + $this->assertFalse($tracker->doTrackPageView('some title')); + } + + public function testGetUrlTrackCrash(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackCrash('message', 'TypeError', 'category', 'stack', 'http://loc.example', 10, 20); + $query = self::parseQueryParams($url); + $this->assertSame('1', $query['ca']); + $this->assertSame('message', $query['cra']); + $this->assertSame('TypeError', $query['cra_tp']); + $this->assertSame('category', $query['cra_ct']); + $this->assertSame('stack', $query['cra_st']); + $this->assertSame('http://loc.example', $query['cra_ru']); + $this->assertSame('10', $query['cra_rl']); + $this->assertSame('20', $query['cra_rc']); + + $url = $tracker->getUrlTrackCrash('message'); + $this->assertStringNotContainsString('&cra_tp=', $url); + $this->assertStringNotContainsString('&cra_ct=', $url); + $this->assertStringNotContainsString('&cra_st=', $url); + $this->assertStringNotContainsString('&cra_ru=', $url); + $this->assertStringNotContainsString('&cra_rl=', $url); + $this->assertStringNotContainsString('&cra_rc=', $url); + } + + public function testDoTrackCrashAndPhpThrowable(): void + { + $tracker = $this->createTracker(); + + $tracker->doTrackCrash('crashed'); + $this->assertStringContainsString('&cra=crashed', $tracker->lastRequestUrl()); + + $throwable = new \RuntimeException('something broke'); + $tracker->doTrackPhpThrowable($throwable, 'category'); + + $query = self::parseQueryParams($tracker->lastRequestUrl()); + $this->assertSame('something broke', $query['cra']); + $this->assertSame('RuntimeException', $query['cra_tp']); + $this->assertSame('category', $query['cra_ct']); + $this->assertSame(__FILE__, $query['cra_ru']); + } + + public function testDoPing(): void + { + $tracker = $this->createTracker(); + $tracker->doPing(); + + $this->assertStringContainsString('&ping=1', $tracker->lastRequestUrl()); + } + + public function testAddEcommerceItemRequiresSku(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('You must specify a SKU'); + + $tracker->addEcommerceItem(''); + } + + public function testEcommerceOrderWithItems(): void + { + $tracker = $this->createTracker(); + $tracker->addEcommerceItem('SKU1', 'Product 1', ['cat1', 'cat2'], '9,99', 2); + $tracker->addEcommerceItem('SKU2'); + + $tracker->doTrackEcommerceOrder('order-1', 20.5, 18.0, 1.5, 0.5, 0.25); + + $query = self::parseQueryParams($tracker->lastRequestUrl()); + $this->assertSame('0', $query['idgoal']); + $this->assertSame('order-1', $query['ec_id']); + $this->assertSame('20.5', $query['revenue']); + $this->assertSame('18', $query['ec_st']); + $this->assertSame('1.5', $query['ec_tx']); + $this->assertSame('0.5', $query['ec_sh']); + $this->assertSame('0.25', $query['ec_dt']); + + $this->assertIsString($query['ec_items']); + $items = json_decode($query['ec_items'], true); + $this->assertSame([ + ['SKU1', 'Product 1', ['cat1', 'cat2'], '9.99', 2], + ['SKU2', '', '', '0', 1], + ], $items); + + // items are cleared after the order was tracked + $this->assertSame([], $tracker->ecommerceItems); + } + + public function testGetUrlTrackEcommerceOrderRequiresOrderId(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('orderId'); + + $tracker->getUrlTrackEcommerceOrder('', 10.0); + } + + public function testEcommerceOrderAcceptsIntegerOrderId(): void + { + $tracker = $this->createTracker(); + $url = $tracker->getUrlTrackEcommerceOrder(12345, 10.0); + + $this->assertStringContainsString('&ec_id=12345', $url); + } + + public function testDoTrackEcommerceCartUpdate(): void + { + $tracker = $this->createTracker(); + $tracker->addEcommerceItem('SKU1'); + $tracker->doTrackEcommerceCartUpdate(10.0); + + $url = $tracker->lastRequestUrl(); + $this->assertStringContainsString('&idgoal=0', $url); + $this->assertStringContainsString('&revenue=10', $url); + $this->assertStringNotContainsString('&ec_id=', $url); + } + + public function testGetUrlTrackEcommerceCartUpdateWithZeroTotal(): void + { + $tracker = $this->createTracker(); + $url = $tracker->getUrlTrackEcommerceCartUpdate(0.0); + + // grandTotal is required, so an explicit zero total is sent as revenue=0 + $this->assertStringContainsString('&idgoal=0', $url); + $this->assertStringContainsString('&revenue=0', $url); + } + + public function testGoalRevenueOmittedByDefaultButZeroIsSent(): void + { + $tracker = $this->createTracker(); + + // no revenue argument -> revenue omitted (Matomo uses the goal's configured revenue) + $this->assertStringNotContainsString('&revenue=', $tracker->getUrlTrackGoal(1)); + + // explicit 0.0 -> revenue=0 is sent (distinct from "unset") + $this->assertStringContainsString('&revenue=0', $tracker->getUrlTrackGoal(1, 0.0)); + + // a real value is sent as-is + $this->assertStringContainsString('&revenue=12.5', $tracker->getUrlTrackGoal(1, 12.5)); + } + + public function testEcommerceOptionalAmountsOmittedByDefaultButZeroIsSent(): void + { + $tracker = $this->createTracker(); + + // subtotal/tax/shipping/discount omitted when not provided + $url = $tracker->getUrlTrackEcommerceOrder('order-1', 10.0); + $this->assertStringNotContainsString('&ec_st=', $url); + $this->assertStringNotContainsString('&ec_tx=', $url); + + // explicit zeros are sent + $url = $tracker->getUrlTrackEcommerceOrder('order-2', 10.0, 0.0, 0.0, 0.0, 0.0); + $this->assertStringContainsString('&ec_st=0', $url); + $this->assertStringContainsString('&ec_tx=0', $url); + $this->assertStringContainsString('&ec_sh=0', $url); + $this->assertStringContainsString('&ec_dt=0', $url); + } + + public function testSetEcommerceView(): void + { + $tracker = $this->createTracker(); + + $tracker->setEcommerceView('SKU1', 'Product', 'category', 9.99); + $this->assertSame( + ['_pkc' => 'category', '_pkp' => '9.99', '_pks' => 'SKU1', '_pkn' => 'Product'], + $tracker->ecommerceView + ); + + $tracker->setEcommerceView('SKU1', 'Product', ['cat1', 'cat2']); + $this->assertSame('["cat1","cat2"]', $tracker->ecommerceView['_pkc']); + + // category-only page: product sku/name are not recorded + $tracker->setEcommerceView('', '', 'category'); + $this->assertSame(['_pkc' => 'category'], $tracker->ecommerceView); + + // ecommerce view parameters end up in the tracking URL and are reset afterwards + $tracker->setEcommerceView('SKU1', 'Product', 'category'); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&_pkc=category', $url); + $this->assertStringContainsString('&_pks=SKU1', $url); + $this->assertStringContainsString('&_pkn=Product', $url); + $this->assertSame([], $tracker->ecommerceView); + } + + public function testSetAttributionInfo(): void + { + $tracker = $this->createTracker(); + $tracker->setAttributionInfo('["campaign","keyword",1234,"http://referrer.example"]'); + + $this->assertSame('["campaign","keyword",1234,"http:\/\/referrer.example"]', $tracker->getAttributionInfo()); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('campaign', $query['_rcn']); + $this->assertSame('keyword', $query['_rck']); + $this->assertSame('1234', $query['_refts']); + $this->assertSame('http://referrer.example', $query['_ref']); + } + + public function testUrlValuesAreEncodedAgainstInjection(): void + { + $tracker = $this->createTracker(); + + // _refts comes from (attacker-controlled) attribution JSON and must be encoded + $tracker->setAttributionInfo('["c","k","1&new_visit=1&cid=deadbeefdeadbeef","r"]'); + $tracker->customData = 'x&idsite=999'; + $tracker->setPageCharset('utf-8&foo=bar'); + + $url = $tracker->getUrlTrackPageView('title'); + $query = self::parseQueryParams($url); + + // injected params must land inside the encoded value, not as separate parameters + $this->assertSame('1&new_visit=1&cid=deadbeefdeadbeef', $query['_refts']); + $this->assertSame('x&idsite=999', $query['data']); + $this->assertSame('utf-8&foo=bar', $query['cs']); + $this->assertArrayNotHasKey('new_visit', $query); + $this->assertSame('1', $query['idsite']); // built-in idsite is untouched + $this->assertArrayNotHasKey('foo', $query); + } + + public function testSetAttributionInfoThrowsOnInvalidJsonWithoutLeakingPayload(): void + { + $tracker = $this->createTracker(); + $payload = 'not-json-with-secret@example.com'; + + try { + $tracker->setAttributionInfo($payload); + $this->fail('Expected an exception'); + } catch (Exception $e) { + $this->assertStringContainsString('JSON encoded string', $e->getMessage()); + // the (potentially PII-bearing) payload must not appear in the message + $this->assertStringNotContainsString($payload, $e->getMessage()); + } + } + + public function testGetAttributionInfoFromCookie(): void + { + $_COOKIE['_pk_ref_1_f609'] = '["campaign","keyword"]'; + + $tracker = $this->createTracker(); + $this->assertSame('["campaign","keyword"]', $tracker->getAttributionInfo()); + } + + public function testGetAttributionInfoWithoutCookieReturnsFalse(): void + { + $tracker = $this->createTracker(); + $this->assertFalse($tracker->getAttributionInfo()); + } + + public function testCustomVariables(): void + { + $tracker = $this->createTracker(); + + $tracker->setCustomVariable(1, 'visit-name', 'visit-value'); + $tracker->setCustomVariable(1, 'page-name', 'page-value', 'page'); + $tracker->setCustomVariable(1, 'event-name', 'event-value', 'event'); + + $this->assertSame(['visit-name', 'visit-value'], $tracker->getCustomVariable(1)); + $this->assertSame(['page-name', 'page-value'], $tracker->getCustomVariable(1, 'page')); + $this->assertSame(['event-name', 'event-value'], $tracker->getCustomVariable(1, 'event')); + $this->assertFalse($tracker->getCustomVariable(2, 'page')); + $this->assertFalse($tracker->getCustomVariable(2, 'event')); + $this->assertFalse($tracker->getCustomVariable(2)); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('{"1":["visit-name","visit-value"]}', $query['_cvar']); + $this->assertSame('{"1":["page-name","page-value"]}', $query['cvar']); + $this->assertSame('{"1":["event-name","event-value"]}', $query['e_cvar']); + + // page and event scoped variables are reset after the request, visit scope is kept + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&cvar=', $url); + $this->assertStringNotContainsString('&e_cvar=', $url); + $this->assertStringContainsString('&_cvar=', $url); + + $tracker->clearCustomVariables(); + $this->assertFalse($tracker->getCustomVariable(1)); + } + + public function testSetCustomVariableThrowsOnInvalidScope(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Invalid 'scope' parameter value"); + + $tracker->setCustomVariable(1, 'name', 'value', 'invalid'); + } + + public function testGetCustomVariableThrowsOnInvalidScope(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + + $tracker->getCustomVariable(1, 'invalid'); + } + + public function testGetCustomVariableFromCookie(): void + { + $_COOKIE['_pk_cvar_1_f609'] = '{"2":["cookie-name","cookie-value"]}'; + + $tracker = $this->createTracker(); + $this->assertSame(['cookie-name', 'cookie-value'], $tracker->getCustomVariable(2)); + $this->assertFalse($tracker->getCustomVariable(3)); + } + + /** + * @dataProvider getTestDataForCustomVariablesFromCookie + * @param array $expected + */ + public function testGetCustomVariablesFromCookieFiltersInvalidData(string $cookieValue, array $expected): void + { + $_COOKIE['_pk_cvar_1_f609'] = $cookieValue; + + $tracker = $this->createTracker(); + $this->assertSame($expected, $tracker->callGetCustomVariablesFromCookie()); + } + + /** + * @return list}> + */ + public static function getTestDataForCustomVariablesFromCookie(): array + { + return [ + ['', []], + ['not-json', []], + ['"a string"', []], + ['{"1":"not-a-pair"}', []], + ['{"1":["only-one"]}', []], + ['{"1":["name","value"],"2":"broken"}', [1 => ['name', 'value']]], + ['{"1":["name",5]}', [1 => ['name', '5']]], + ]; + } + + public function testCustomDimensions(): void + { + $tracker = $this->createTracker(); + $tracker->setCustomDimension(2, 'value'); + + $this->assertSame('value', $tracker->getCustomDimension(2)); + $this->assertNull($tracker->getCustomDimension(3)); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&dimension2=value', $url); + + // dimensions are reset after a request + $this->assertNull($tracker->getCustomDimension(2)); + + $tracker->setCustomDimension(2, 'value'); + $tracker->clearCustomDimensions(); + $this->assertNull($tracker->getCustomDimension(2)); + } + + public function testCustomTrackingParameters(): void + { + $tracker = $this->createTracker(); + $tracker->setCustomTrackingParameter('bw_bytes', '1024'); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&bw_bytes=1024', $url); + + // custom parameters are reset after a request + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&bw_bytes=', $url); + + // dimensionX parameters are mapped to custom dimensions + $tracker->setCustomTrackingParameter('dimension3', 'dim-value'); + $this->assertSame('dim-value', $tracker->getCustomDimension(3)); + + $tracker->setCustomTrackingParameter('bw_bytes', '1024'); + $tracker->clearCustomTrackingParameters(); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&bw_bytes=', $url); + } + + public function testCustomTrackingParameterAcceptsArrayValue(): void + { + $tracker = $this->createTracker(); + // array values are serialized like the JS tracker does, via http_build_query + $tracker->setCustomTrackingParameter('forms', [['name' => 'a'], ['name' => 'b']]); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('forms%5B0%5D%5Bname%5D=a', $url); + $this->assertStringContainsString('forms%5B1%5D%5Bname%5D=b', $url); + } + + public function testSetDebugTrackingParameterOverridesBuiltInParameter(): void + { + $tracker = $this->createTracker(); + // inject an intentionally invalid idsite to exercise server-side validation + $tracker->setDebugTrackingParameter('idsite', 'not-a-number'); + $tracker->setDebugTrackingParameter('_cvar', '{"1":[["bad"],"v"]}'); + + $url = $tracker->getUrlTrackPageView('title'); + // appended last so it wins over the built-in idsite=1 + $this->assertStringContainsString('&idsite=not-a-number', $url); + $this->assertStringContainsString('&_cvar=' . urlencode('{"1":[["bad"],"v"]}'), $url); + $query = self::parseQueryParams($url); + $this->assertSame('not-a-number', $query['idsite']); + + // debug parameters are cleared after a request + $this->assertStringNotContainsString('not-a-number', $tracker->getUrlTrackPageView('title')); + } + + public function testVisitorIdHandling(): void + { + $tracker = $this->createTracker(); + + $randomId = $tracker->getVisitorId(); + $this->assertSame(16, strlen($randomId)); + + $tracker->setVisitorId('abcdef0123456789'); + $this->assertSame('abcdef0123456789', $tracker->getVisitorId()); + + $tracker->setNewVisitorId(); + $newId = $tracker->getVisitorId(); + $this->assertSame(16, strlen($newId)); + $this->assertNotSame('abcdef0123456789', $newId); + } + + public function testSetVisitorIdThrowsOnInvalidValue(): void + { + $tracker = $this->createTracker(); + + try { + $tracker->setVisitorId('too-short'); + $this->fail('Expected exception for invalid length'); + } catch (Exception $e) { + $this->assertStringContainsString('16', $e->getMessage()); + } + + $this->expectException(Exception::class); + $tracker->setVisitorId('zzzzzzzzzzzzzzzz'); + } + + public function testForcedVisitorIdIsUsedAsCid(): void + { + $tracker = $this->createTracker(); + $tracker->setVisitorId('abcdef0123456789'); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('abcdef0123456789', $query['cid']); + $this->assertArrayNotHasKey('_id', $query); + } + + public function testLoadVisitorIdCookie(): void + { + $tracker = $this->createTracker(); + $this->assertFalse($tracker->callLoadVisitorIdCookie()); + + $_COOKIE['_pk_id_1_f609'] = 'too-short.123'; + $this->assertFalse($tracker->callLoadVisitorIdCookie()); + + // a 16-char but non-hex id (e.g. containing injection chars) is rejected + $_COOKIE['_pk_id_1_f609'] = '&x=1&y=2&z=3&w=4.1'; + $this->assertFalse($tracker->callLoadVisitorIdCookie()); + + $_COOKIE['_pk_id_1_f609'] = 'abcdef0123456789.1583291045'; + $this->assertTrue($tracker->callLoadVisitorIdCookie()); + $this->assertSame('abcdef0123456789', $tracker->getVisitorId()); + $this->assertSame(1583291045, $tracker->createTs); + } + + public function testLoadVisitorIdCookieWithoutCreationTsKeepsCurrentOne(): void + { + $tracker = $this->createTracker(); + $createTsBefore = $tracker->createTs; + + $_COOKIE['_pk_id_1_f609'] = 'abcdef0123456789'; + $this->assertTrue($tracker->callLoadVisitorIdCookie()); + $this->assertSame($createTsBefore, $tracker->createTs); + } + + public function testUserIdHandling(): void + { + $tracker = $this->createTracker(); + $this->assertNull($tracker->getUserId()); + + $tracker->setUserId('user@example.org'); + $this->assertSame('user@example.org', $tracker->getUserId()); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('user@example.org', $query['uid']); + + // null de-assigns a previously set user id + $tracker->setUserId(null); + $this->assertNull($tracker->getUserId()); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&uid=', $url); + } + + public function testSetUserIdThrowsOnEmptyString(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('User ID cannot be empty'); + + $tracker->setUserId(''); + } + + public function testGetUserIdHashed(): void + { + $this->assertSame(substr(sha1('user@example.org'), 0, 16), \MatomoTracker::getUserIdHashed('user@example.org')); + } + + public function testUserAgentAndBrowserLanguage(): void + { + $tracker = $this->createTracker(); + $this->assertNull($tracker->getUserAgent()); + + $tracker->setUserAgent('My Agent'); + $this->assertSame('My Agent', $tracker->getUserAgent()); + + $tracker->setBrowserLanguage('de-de'); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertSame('My Agent', $options[CURLOPT_USERAGENT]); + $this->assertSame(['Accept-Language: de-de'], $options[CURLOPT_HTTPHEADER]); + } + + public function testIpHandling(): void + { + $tracker = $this->createTracker(); + $this->assertNull($tracker->getIp()); + + $tracker->setIp('130.54.2.1'); + $this->assertSame('130.54.2.1', $tracker->getIp()); + + // cip is only added when a token_auth is set + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&cip=', $url); + + $tracker->setTokenAuth('0123456789abcdef0123456789abcdef'); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&cip=130.54.2.1', $url); + } + + public function testGeoLocationParameters(): void + { + $tracker = $this->createTracker(); + $tracker->setCountry('de'); + $tracker->setRegion('Hessen'); + $tracker->setCity('Frankfurt'); + $tracker->setLatitude(50.11); + $tracker->setLongitude(8.68); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('de', $query['country']); + $this->assertSame('Hessen', $query['region']); + $this->assertSame('Frankfurt', $query['city']); + $this->assertSame('50.11', $query['lat']); + $this->assertSame('8.68', $query['long']); + } + + public function testZeroCoordinatesAreSent(): void + { + $tracker = $this->createTracker(); + $tracker->setLatitude(0.0); + $tracker->setLongitude(0.0); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('0', $query['lat']); + $this->assertSame('0', $query['long']); + } + + public function testBrowserAttributes(): void + { + $tracker = $this->createTracker(); + $tracker->setResolution(1920, 1080); + $tracker->setBrowserHasCookies(true); + $tracker->setLocalTime('04:05:06'); + $tracker->setPlugins(true, false, true); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('1920x1080', $query['res']); + $this->assertSame('1', $query['cookie']); + $this->assertSame('4', $query['h']); + $this->assertSame('5', $query['m']); + $this->assertSame('6', $query['s']); + $this->assertSame('1', $query['fla']); + $this->assertSame('0', $query['java']); + $this->assertSame('1', $query['qt']); + $this->assertSame('0', $query['realp']); + $this->assertSame('0', $query['pdf']); + $this->assertSame('0', $query['wma']); + $this->assertSame('0', $query['ag']); + } + + public function testPageCharset(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&cs=', $url); + + $tracker->setPageCharset('iso-8859-1'); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&cs=iso-8859-1', $url); + + $tracker->setPageCharset(); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&cs=', $url); + } + + public function testUrlReferrer(): void + { + $tracker = $this->createTracker(); + $tracker->setUrlReferrer('http://referrer.example'); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('http://referrer.example', $query['urlref']); + + // the deprecated setUrlReferer() forwards to setUrlReferrer() + $tracker->setUrlReferer('http://other.example'); + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('http://other.example', $query['urlref']); + + // null unsets the referrer (renders as an empty urlref) + $tracker->setUrlReferrer(null); + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('', $query['urlref']); + } + + public function testSetGenerationTimeIsANoOp(): void + { + $tracker = $this->createTracker(); + $this->assertSame($tracker, $tracker->setGenerationTime(500)); + } + + public function testPerformanceTimings(): void + { + $tracker = $this->createTracker(); + + // without a pageview id no performance timings are added + $tracker->setPerformanceTimings(1, 2, 3, 4, 5, 6); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&pf_net=', $url); + + $tracker->setPageviewId('abc123'); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&pf_net=1', $url); + $this->assertStringContainsString('&pf_srv=2', $url); + $this->assertStringContainsString('&pf_tfr=3', $url); + $this->assertStringContainsString('&pf_dm1=4', $url); + $this->assertStringContainsString('&pf_dm2=5', $url); + $this->assertStringContainsString('&pf_onl=6', $url); + + // timings are cleared after they were tracked once + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&pf_net=', $url); + + $tracker->setPerformanceTimings(1, 2, 3, 4, 5, 6); + $tracker->clearPerformanceTimings(); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&pf_net=', $url); + } + + public function testForceVisitDateTime(): void + { + $tracker = $this->createTracker(); + $tracker->setForceVisitDateTime('2020-01-02 03:04:05'); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('2020-01-02 03:04:05', $query['cdt']); + + $this->assertSame(strtotime('2020-01-02 03:04:05'), $tracker->callGetTimestamp()); + } + + public function testGetTimestampFallsBackToCurrentTimeOnInvalidDateTime(): void + { + $tracker = $this->createTracker(); + $tracker->setForceVisitDateTime('not a datetime'); + + $this->assertEqualsWithDelta(time(), $tracker->callGetTimestamp(), 5); + + $tracker = $this->createTracker(); + $this->assertEqualsWithDelta(time(), $tracker->callGetTimestamp(), 5); + } + + public function testForceNewVisitIsOnlySentOnce(): void + { + $tracker = $this->createTracker(); + $tracker->setForceNewVisit(); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&new_visit=1', $url); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&new_visit=1', $url); + } + + public function testSetIdSite(): void + { + $tracker = $this->createTracker(); + $tracker->setIdSite(42); + + $this->assertStringContainsString('idsite=42', $tracker->getUrlTrackPageView('title')); + } + + public function testDebugStringAppend(): void + { + $tracker = $this->createTracker(); + $tracker->setDebugStringAppend('debug=1'); + + $this->assertStringContainsString('&debug=1', $tracker->getUrlTrackPageView('title')); + } + + public function testDisableSendImageResponse(): void + { + $tracker = $this->createTracker(); + + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringNotContainsString('&send_image=0', $url); + + $tracker->disableSendImageResponse(); + $url = $tracker->getUrlTrackPageView('title'); + $this->assertStringContainsString('&send_image=0', $url); + } + + public function testClientHintsFromStrings(): void + { + $tracker = $this->createTracker(); + $tracker->setClientHints( + 'model', + 'Windows', + '14.0.0', + '"Chromium"; v="110.0.1", "Google Chrome"; v="110.0.2"', + '110.0.1', + '"Desktop", "XR"' + ); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertIsString($query['uadata']); + $this->assertSame( + [ + 'model' => 'model', + 'platform' => 'Windows', + 'platformVersion' => '14.0.0', + 'uaFullVersion' => '110.0.1', + 'fullVersionList' => [ + ['brand' => 'Chromium', 'version' => '110.0.1'], + ['brand' => 'Google Chrome', 'version' => '110.0.2'], + ], + 'formFactors' => ['Desktop', 'XR'], + ], + json_decode($query['uadata'], true) + ); + } + + public function testClientHintsFromArrays(): void + { + $tracker = $this->createTracker(); + $fullVersionList = [['brand' => 'Chromium', 'version' => '110.0.1']]; + $tracker->setClientHints('', 'Linux', '', $fullVersionList, '', ['Desktop']); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertIsString($query['uadata']); + $this->assertSame( + [ + 'platform' => 'Linux', + 'fullVersionList' => $fullVersionList, + 'formFactors' => ['Desktop'], + ], + json_decode($query['uadata'], true) + ); + } + + public function testEmptyClientHintsAreNotSent(): void + { + $tracker = $this->createTracker(); + $tracker->setClientHints(); + + $this->assertStringNotContainsString('&uadata=', $tracker->getUrlTrackPageView('title')); + } + + public function testClientHintsFromServerVariables(): void + { + $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] = '"macOS"'; + $_SERVER['HTTP_SEC_CH_UA_PLATFORM_VERSION'] = '"13.1.0"'; + + $tracker = $this->createTracker(); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertIsString($query['uadata']); + $this->assertSame( + ['platform' => '"macOS"', 'platformVersion' => '"13.1.0"'], + json_decode($query['uadata'], true) + ); + } + + public function testConstructorReadsServerVariables(): void + { + $_SERVER['HTTP_REFERER'] = 'http://referrer.example'; + $_SERVER['REMOTE_ADDR'] = '10.11.12.13'; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr-fr'; + $_SERVER['HTTP_USER_AGENT'] = 'Test Agent'; + + $tracker = new TestableMatomoTracker(1, self::TEST_URL); + + $this->assertSame('10.11.12.13', $tracker->getIp()); + $this->assertSame('fr-fr', $tracker->acceptLanguage); + $this->assertSame('Test Agent', $tracker->getUserAgent()); + + $query = self::parseQueryParams($tracker->getUrlTrackPageView('title')); + $this->assertSame('http://referrer.example', $query['urlref']); + } + + public function testBulkTrackingStoresRequestsAndResetsState(): void + { + $tracker = $this->createTracker(); + $tracker->enableBulkTracking(); + $tracker->setUserAgent('Bulk Agent'); + $tracker->setBrowserLanguage('en-us'); + + $this->assertTrue($tracker->doTrackPageView('title')); + $this->assertCount(1, $tracker->storedTrackingActions); + $this->assertStringContainsString('&ua=' . urlencode('Bulk Agent'), $tracker->storedTrackingActions[0]); + $this->assertStringContainsString('&lang=' . urlencode('en-us'), $tracker->storedTrackingActions[0]); + + // user agent, language and client hints are reset after storing a bulk request + $this->assertNull($tracker->getUserAgent()); + $this->assertNull($tracker->acceptLanguage); + $this->assertSame([], $tracker->clientHints); + + $this->assertTrue($tracker->doTrackEvent('cat', 'act')); + $this->assertCount(2, $tracker->storedTrackingActions); + } + + public function testDoBulkTrackSendsAllStoredRequests(): void + { + $tracker = $this->createTracker(); + $tracker->enableBulkTracking(); + $tracker->setTokenAuth('0123456789abcdef0123456789abcdef'); + $tracker->doTrackPageView('page one'); + $tracker->doTrackPageView('page two'); + + $response = $tracker->doBulkTrack(); + + $this->assertSame('mock-response', $response); + $this->assertSame([], $tracker->storedTrackingActions); + + $this->assertCount(1, $tracker->capturedRequests); + $request = $tracker->capturedRequests[0]; + $this->assertSame('http://mymatomo.com/matomo.php', $request['url']); + $this->assertSame('POST', $request['method']); + $this->assertTrue($request['force']); + // bulk requests use the more generous bulk timeout + $this->assertGreaterThanOrEqual(\MatomoTracker::DEFAULT_BULK_REQUEST_TIMEOUT, $request['timeout']); + + $this->assertIsString($request['data']); + $data = json_decode($request['data'], true); + $this->assertIsArray($data); + $this->assertSame('0123456789abcdef0123456789abcdef', $data['token_auth']); + $this->assertIsArray($data['requests']); + $this->assertCount(2, $data['requests']); + } + + public function testDoBulkTrackRetainsBatchOnFailureAndRestoresTimeout(): void + { + $tracker = $this->createTracker(); + $tracker->mockResponse = false; // simulate a failed send + $tracker->enableBulkTracking(); + $tracker->doTrackPageView('page'); + $originalTimeout = $tracker->getRequestTimeout(); + + $this->assertFalse($tracker->doBulkTrack()); + // the batch is kept so the caller can retry, and the (temporarily raised) timeout is restored + $this->assertCount(1, $tracker->storedTrackingActions); + $this->assertSame($originalTimeout, $tracker->getRequestTimeout()); + } + + public function testTokenAuthRequestIsSentAsPost(): void + { + // capture the transport method after sendRequest() has applied its token/method handling + $captured = new class (1, 'http://matomo.example/matomo.php') extends \MatomoTracker { + public string $capturedMethod = ''; + + protected function prepareCurlOptions(string $url, string $method, ?string $data, bool $forcePostUrlEncoded): array + { + $this->capturedMethod = $method; + throw new \RuntimeException('stop-before-network'); + } + }; + $captured->disableCookieSupport(); + $captured->setTokenAuth('0123456789abcdef0123456789abcdef'); + + try { + $captured->doTrackPageView('page'); + $this->fail('expected the network short-circuit'); + } catch (\RuntimeException $e) { + $this->assertSame('stop-before-network', $e->getMessage()); + } + + // with a token and no explicit request method, the request must be POSTed so Matomo + // reads token_auth from the body instead of ignoring a GET body + $this->assertSame('POST', $captured->capturedMethod); + } + + /** + * The URL/body carry token_auth and PII, so they must be redacted from stack traces not only + * in sendRequest() but also in the transport option builders they are forwarded to (otherwise + * a throw one frame down would put them straight back into the trace). + * + * @return array + */ + public static function sensitiveParameterProvider(): array + { + return [ + ['sendRequest', 'url'], + ['sendRequest', 'data'], + ['prepareCurlOptions', 'url'], + ['prepareCurlOptions', 'data'], + ['prepareStreamOptions', 'data'], + ]; + } + + /** + * @dataProvider sensitiveParameterProvider + */ + public function testRequestUrlAndBodyAreMarkedSensitive(string $method, string $param): void + { + $reflection = new \ReflectionMethod(\MatomoTracker::class, $method); + foreach ($reflection->getParameters() as $p) { + if ($p->getName() === $param) { + $this->assertNotEmpty( + $p->getAttributes(\SensitiveParameter::class), + "$method(\$$param) must be marked #[\\SensitiveParameter]" + ); + return; + } + } + $this->fail("Parameter \$$param not found on $method()"); + } + + public function testSetCurlOptionsMergesHttpHeadersInsteadOfReplacingThem(): void + { + $tracker = $this->createTracker(); + $tracker->setCurlOptions([CURLOPT_HTTPHEADER => ['X-Custom: 1']]); + + // A POST/bulk-style request whose Content-Type must survive the caller's extra header. + $options = $tracker->callPrepareCurlOptions('http://example.org/', 'POST', 'foo=bar', true); + $headers = $options[CURLOPT_HTTPHEADER]; + + $this->assertIsArray($headers); + $this->assertContains('X-Custom: 1', $headers); + $this->assertContains('Content-Type: application/x-www-form-urlencoded', $headers); + $this->assertContains('Accept-Language: ', $headers); + } + + public function testStreamOptionsIgnoreHttpErrors(): void + { + $tracker = $this->createTracker(); + $options = $tracker->callPrepareStreamOptions('GET', null, false); + $this->assertTrue($options['http']['ignore_errors']); + } + + public function testDoBulkTrackThrowsWithoutStoredRequests(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + + $tracker->doBulkTrack(); + } + + public function testDisableBulkTracking(): void + { + $tracker = $this->createTracker(); + $tracker->enableBulkTracking(); + $tracker->disableBulkTracking(); + + $this->assertSame('mock-response', $tracker->doTrackPageView('title')); + $this->assertSame([], $tracker->storedTrackingActions); + } + + public function testRequestTimeoutAccessors(): void + { + $tracker = $this->createTracker(); + + $this->assertSame(5, $tracker->getRequestTimeout()); + $tracker->setRequestTimeout(10); + $this->assertSame(10, $tracker->getRequestTimeout()); + + $this->assertSame(2, $tracker->getRequestConnectTimeout()); + $tracker->setRequestConnectTimeout(5); + $this->assertSame(5, $tracker->getRequestConnectTimeout()); + } + + public function testRequestTimeoutThrowsOnNegativeValue(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $tracker->setRequestTimeout(-1); + } + + public function testRequestConnectTimeoutThrowsOnNegativeValue(): void + { + $tracker = $this->createTracker(); + + $this->expectException(Exception::class); + $tracker->setRequestConnectTimeout(-1); + } + + public function testPrepareCurlOptions(): void + { + $tracker = $this->createTracker(); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertSame('http://example.org', $options[CURLOPT_URL]); + $this->assertSame('', $options[CURLOPT_USERAGENT]); + $this->assertTrue($options[CURLOPT_FOLLOWLOCATION]); + $this->assertArrayNotHasKey(CURLOPT_POST, $options); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'POST', null, false); + $this->assertTrue($options[CURLOPT_POST]); + $this->assertArrayNotHasKey(CURLOPT_FOLLOWLOCATION, $options); + + // url encoded post data + $options = $tracker->callPrepareCurlOptions('http://example.org', 'POST', 'a=b', true); + $this->assertSame('a=b', $options[CURLOPT_POSTFIELDS]); + $this->assertIsArray($options[CURLOPT_HTTPHEADER]); + $this->assertContains('Content-Type: application/x-www-form-urlencoded', $options[CURLOPT_HTTPHEADER]); + + // json post data + $options = $tracker->callPrepareCurlOptions('http://example.org', 'POST', '{"requests":[]}', false); + $this->assertSame('{"requests":[]}', $options[CURLOPT_POSTFIELDS]); + $this->assertIsArray($options[CURLOPT_HTTPHEADER]); + $this->assertContains('Content-Type: application/json', $options[CURLOPT_HTTPHEADER]); + $this->assertContains('Expect:', $options[CURLOPT_HTTPHEADER]); + } + + public function testPrepareCurlOptionsWithProxyAndCookies(): void + { + $tracker = $this->createTracker(); + $tracker->setProxy('proxy.example', 3128); + $tracker->setOutgoingTrackerCookie('name', 'value'); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertSame('proxy.example:3128', $options[CURLOPT_PROXY]); + $this->assertSame('name=value', $options[CURLOPT_COOKIE]); + + // outgoing cookies are cleared once they were added to a request + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertArrayNotHasKey(CURLOPT_COOKIE, $options); + } + + public function testSetCurlOptionsExtendAndOverrideDefaults(): void + { + $tracker = $this->createTracker(); + $tracker->setCurlOptions([ + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, // extends the defaults + CURLOPT_TIMEOUT => 1, // overrides the built-in timeout + ]); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertSame(CURL_IPRESOLVE_V4, $options[CURLOPT_IPRESOLVE]); + $this->assertSame(1, $options[CURLOPT_TIMEOUT]); + } + + private function makeFailingTracker(): \MatomoTracker + { + // A closed local port gives a fast, deterministic connection failure without external I/O. + $tracker = new \MatomoTracker(1, 'http://127.0.0.1:1/matomo.php'); + $tracker->disableCookieSupport(); + $tracker->setRequestConnectTimeout(1); + $tracker->setRequestTimeout(1); + + return $tracker; + } + + public function testFailedRequestThrowsByDefault(): void + { + $tracker = $this->makeFailingTracker(); + + $this->expectException(\RuntimeException::class); + $tracker->doTrackPageView('title'); + } + + public function testFailedRequestReturnsFalseWhenExceptionsDisabled(): void + { + $tracker = $this->makeFailingTracker(); + $tracker->setExceptionsEnabled(false); + + $this->assertFalse($tracker->doTrackPageView('title')); + } + + public function testPrepareStreamOptions(): void + { + $tracker = $this->createTracker(); + $tracker->setUserAgent('Stream Agent'); + $tracker->setBrowserLanguage('en-gb'); + + $options = $tracker->callPrepareStreamOptions('GET', null, false); + $this->assertSame('GET', $options['http']['method']); + $this->assertSame('Stream Agent', $options['http']['user_agent']); + $this->assertSame("Accept-Language: en-gb\r\n", $options['http']['header']); + + $options = $tracker->callPrepareStreamOptions('POST', 'a=b', true); + $this->assertIsString($options['http']['header']); + $this->assertStringContainsString('Content-Type: application/x-www-form-urlencoded', $options['http']['header']); + $this->assertSame('a=b', $options['http']['content']); + + $options = $tracker->callPrepareStreamOptions('POST', '{"requests":[]}', false); + $this->assertIsString($options['http']['header']); + $this->assertStringContainsString('Content-Type: application/json', $options['http']['header']); + $this->assertSame('{"requests":[]}', $options['http']['content']); + } + + public function testPrepareStreamOptionsWithProxyAndCookies(): void + { + $tracker = $this->createTracker(); + $tracker->setProxy('proxy.example'); + $tracker->setOutgoingTrackerCookie('name', 'value'); + + $options = $tracker->callPrepareStreamOptions('GET', null, false); + $this->assertSame('proxy.example:80', $options['http']['proxy']); + $this->assertIsString($options['http']['header']); + $this->assertStringContainsString('Cookie: name=value', $options['http']['header']); + } + + public function testOutgoingTrackerCookieCanBeRemoved(): void + { + $tracker = $this->createTracker(); + $tracker->setOutgoingTrackerCookie('name', 'value'); + $tracker->setOutgoingTrackerCookie('name', null); + + $this->assertSame([], $tracker->outgoingTrackerCookies); + } + + public function testOutgoingCookiesAreJoinedWithSemicolon(): void + { + $tracker = $this->createTracker(); + $tracker->setOutgoingTrackerCookie('a', '1'); + $tracker->setOutgoingTrackerCookie('b', '2'); + + $options = $tracker->callPrepareCurlOptions('http://example.org', 'GET', null, false); + $this->assertSame('a=1; b=2', $options[CURLOPT_COOKIE]); + } + + public function testParseIncomingCookies(): void + { + $tracker = $this->createTracker(); + + $tracker->callParseIncomingCookies([ + 'Content-Type: text/plain', + 'Set-Cookie: first=value1; path=/; HttpOnly', + 'Set-Cookie: second=value2; path=/', + 12345, + ]); + + // multiple Set-Cookie headers all accumulate (previously only the last survived) + $this->assertSame('value1', $tracker->getIncomingTrackerCookie('first')); + $this->assertSame('value2', $tracker->getIncomingTrackerCookie('second')); + $this->assertFalse($tracker->getIncomingTrackerCookie('missing')); + + $tracker->callParseIncomingCookies([]); + $this->assertFalse($tracker->getIncomingTrackerCookie('first')); + } + + public function testFirstPartyCookiesAreSet(): void + { + $tracker = $this->createTracker(); + $tracker->setCustomVariable(1, 'name', 'value'); + $tracker->setAttributionInfo('["campaign","keyword"]'); + $tracker->callSetFirstPartyCookies(); + + $cookieNames = array_column($tracker->capturedCookies, 'name'); + $this->assertSame(['ref', 'ses', 'id', 'cvar'], $cookieNames); + + $this->assertSame('["campaign","keyword"]', $tracker->capturedCookies[0]['value']); + $this->assertSame('*', $tracker->capturedCookies[1]['value']); + $this->assertStringContainsString($tracker->getVisitorId() . '.', $tracker->capturedCookies[2]['value']); + $this->assertSame('{"1":["name","value"]}', $tracker->capturedCookies[3]['value']); + } + + public function testDisableCookieSupport(): void + { + $_COOKIE['_pk_id_1_f609'] = 'abcdef0123456789.1583291045'; + + $tracker = $this->createTracker(); + $tracker->disableCookieSupport(); + + $this->assertFalse($tracker->callGetCookieMatchingName('id')); + + $tracker->callSetFirstPartyCookies(); + $this->assertSame([], $tracker->capturedCookies); + } + + public function testDeleteCookies(): void + { + $tracker = $this->createTracker(); + $tracker->deleteCookies(); + + $this->assertCount(4, $tracker->capturedCookies); + $this->assertSame(['id', 'ses', 'cvar', 'ref'], array_column($tracker->capturedCookies, 'name')); + foreach ($tracker->capturedCookies as $cookie) { + $this->assertSame('', $cookie['value']); + $this->assertSame(-86400, $cookie['ttl']); + } + } + + public function testSetCookieBuildsHeader(): void + { + $tracker = $this->createTracker(); + $tracker->captureCookies = false; + $tracker->enableCookies('example.com', '/path', true, true, 'Lax'); + + // in a CLI environment headers can not actually be sent, this only must not fail + $tracker->deleteCookies(); + + $this->assertSame([], $tracker->capturedCookies); + } + + public function testEnableCookiesInfluencesCookieName(): void + { + $tracker = $this->createTracker(); + $defaultName = $tracker->callGetCookieName('id'); + $this->assertMatchesRegularExpression('/^_pk_id\.1\.[0-9a-f]{4}$/', $defaultName); + + $tracker->enableCookies('example.com', '/path'); + $nameWithDomain = $tracker->callGetCookieName('id'); + $this->assertMatchesRegularExpression('/^_pk_id\.1\.[0-9a-f]{4}$/', $nameWithDomain); + $this->assertNotSame($defaultName, $nameWithDomain); + } + + /** + * @dataProvider getTestDataForDomainFixup + */ + public function testDomainFixup(string $domain, string $expected): void + { + $this->assertSame($expected, TestableMatomoTracker::callDomainFixup($domain)); + } + + /** + * @return list + */ + public static function getTestDataForDomainFixup(): array + { + return [ + ['', ''], + ['example.com', 'example.com'], + ['example.com.', 'example.com'], + ['*.example.com', '.example.com'], + ]; + } + + /** + * @dataProvider getTestDataForToStringValue + */ + public function testToStringValue(mixed $value, string $expected): void + { + $this->assertSame($expected, TestableMatomoTracker::callToStringValue($value)); + } + + /** + * @return list + */ + public static function getTestDataForToStringValue(): array + { + return [ + ['string', 'string'], + [5, '5'], + [1.5, '1.5'], + [true, '1'], + [false, ''], + [null, ''], + [['array'], ''], + [new \stdClass(), ''], + ]; + } + + public function testGetCookieMatchingNameReturnsFalseWhenNotFound(): void + { + $tracker = $this->createTracker(); + $this->assertFalse($tracker->callGetCookieMatchingName('id')); + } + + public function testGetCurrentScheme(): void + { + unset($_SERVER['HTTPS']); + $this->assertSame('http', TestableMatomoTracker::callGetCurrentScheme()); + + $_SERVER['HTTPS'] = 'on'; + $this->assertSame('https', TestableMatomoTracker::callGetCurrentScheme()); + } + + public function testGetCurrentHost(): void + { + unset($_SERVER['HTTP_HOST']); + $this->assertSame('unknown', TestableMatomoTracker::callGetCurrentHost()); + + $_SERVER['HTTP_HOST'] = 'matomo.example'; + $this->assertSame('matomo.example', TestableMatomoTracker::callGetCurrentHost()); + } + + public function testGetCurrentScriptName(): void + { + unset($_SERVER['PATH_INFO'], $_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']); + $this->assertSame('/', TestableMatomoTracker::callGetCurrentScriptName()); + + $_SERVER['SCRIPT_NAME'] = 'script.php'; + $this->assertSame('/script.php', TestableMatomoTracker::callGetCurrentScriptName()); + + $_SERVER['REQUEST_URI'] = '/dir/page.php?query=1'; + $this->assertSame('/dir/page.php', TestableMatomoTracker::callGetCurrentScriptName()); + + $_SERVER['REQUEST_URI'] = '/dir/other.php'; + $this->assertSame('/dir/other.php', TestableMatomoTracker::callGetCurrentScriptName()); + + $_SERVER['PATH_INFO'] = '/path/info'; + $this->assertSame('/path/info', TestableMatomoTracker::callGetCurrentScriptName()); + } + + public function testGetCurrentQueryStringAndUrl(): void + { + unset($_SERVER['QUERY_STRING']); + $this->assertSame('', TestableMatomoTracker::callGetCurrentQueryString()); + + $_SERVER['QUERY_STRING'] = 'a=b&c=d'; + $this->assertSame('?a=b&c=d', TestableMatomoTracker::callGetCurrentQueryString()); + + $_SERVER['HTTPS'] = 'on'; + $_SERVER['HTTP_HOST'] = 'matomo.example'; + $_SERVER['PATH_INFO'] = '/page'; + $this->assertSame('https://matomo.example/page?a=b&c=d', TestableMatomoTracker::callGetCurrentUrl()); + } + + public function testHelperFunctions(): void + { + \MatomoTracker::$URL = self::TEST_URL; + + $url = \Matomo_getUrlTrackPageView(5, 'my title'); + $this->assertStringContainsString('idsite=5', $url); + $this->assertStringContainsString('&action_name=my+title', $url); + + $url = \Matomo_getUrlTrackGoal(5, 3, 1.5); + $this->assertStringContainsString('idsite=5', $url); + $this->assertStringContainsString('&idgoal=3', $url); + $this->assertStringContainsString('&revenue=1.5', $url); + } + + public function testPiwikCompatibilityShim(): void + { + \MatomoTracker::$URL = self::TEST_URL; + + $tracker = new \PiwikTracker(1, self::TEST_URL); + $this->assertInstanceOf(\MatomoTracker::class, $tracker); + + $url = \Piwik_getUrlTrackPageView(5, 'my title'); + $this->assertStringContainsString('idsite=5', $url); + + $url = \Piwik_getUrlTrackGoal(5, 3, 1.5); + $this->assertStringContainsString('&idgoal=3', $url); } -} \ No newline at end of file +} diff --git a/tests/Unit/TestableMatomoTracker.php b/tests/Unit/TestableMatomoTracker.php new file mode 100644 index 0000000..77d0261 --- /dev/null +++ b/tests/Unit/TestableMatomoTracker.php @@ -0,0 +1,177 @@ + + */ + public array $capturedRequests = []; + + public string|bool $mockResponse = 'mock-response'; + + /** + * @var list + */ + public array $capturedCookies = []; + + public bool $captureCookies = true; + + protected function sendRequest(string $url, string $method = 'GET', ?string $data = null, bool $force = false): string|bool + { + if ($this->doBulkRequests && !$force) { + return parent::sendRequest($url, $method, $data, $force); + } + + $this->capturedRequests[] = [ + 'url' => $url, + 'method' => $method, + 'data' => $data, + 'force' => $force, + 'timeout' => $this->requestTimeout, + ]; + + return $this->mockResponse; + } + + protected function setCookie(string $cookieName, string $cookieValue, int $cookieTTL): self + { + if (!$this->captureCookies) { + return parent::setCookie($cookieName, $cookieValue, $cookieTTL); + } + + $this->capturedCookies[] = ['name' => $cookieName, 'value' => $cookieValue, 'ttl' => $cookieTTL]; + + return $this; + } + + public function lastRequestUrl(): string + { + $last = end($this->capturedRequests); + + return $last === false ? '' : $last['url']; + } + + /** + * @return array + */ + public function callPrepareCurlOptions(string $url, string $method, ?string $data, bool $forcePostUrlEncoded): array + { + return $this->prepareCurlOptions($url, $method, $data, $forcePostUrlEncoded); + } + + /** + * @return array{http: array} + */ + public function callPrepareStreamOptions(string $method, ?string $data, bool $forcePostUrlEncoded): array + { + return $this->prepareStreamOptions($method, $data, $forcePostUrlEncoded); + } + + /** + * @param array $headers + */ + public function callParseIncomingCookies(array $headers): void + { + $this->parseIncomingCookies($headers); + } + + public function callGetTimestamp(): int + { + return $this->getTimestamp(); + } + + public function callGetBaseUrl(): string + { + return $this->getBaseUrl(); + } + + public function callGetRequest(int $idSite): string + { + return $this->getRequest($idSite); + } + + public function callGetCookieMatchingName(string $name): string|false + { + return $this->getCookieMatchingName($name); + } + + public function callGetCookieName(string $name): string + { + return $this->getCookieName($name); + } + + public function callLoadVisitorIdCookie(): bool + { + return $this->loadVisitorIdCookie(); + } + + public function callSetFirstPartyCookies(): void + { + $this->setFirstPartyCookies(); + } + + /** + * @return array + */ + public function callGetCustomVariablesFromCookie(): array + { + return $this->getCustomVariablesFromCookie(); + } + + public static function callDomainFixup(string $domain): string + { + return self::domainFixup($domain); + } + + public static function callToStringValue(mixed $value): string + { + return self::toStringValue($value); + } + + public static function callGetCurrentScheme(): string + { + return self::getCurrentScheme(); + } + + public static function callGetCurrentHost(): string + { + return self::getCurrentHost(); + } + + public static function callGetCurrentScriptName(): string + { + return self::getCurrentScriptName(); + } + + public static function callGetCurrentQueryString(): string + { + return self::getCurrentQueryString(); + } + + public static function callGetCurrentUrl(): string + { + return self::getCurrentUrl(); + } +} From b3e57c4c32cfa179e59ccc50a2c083686a20d1b9 Mon Sep 17 00:00:00 2001 From: eldk Date: Mon, 27 Jul 2026 16:18:51 +0200 Subject: [PATCH 113/115] Detect Google-GeminiNotebook as an AI bot (#153) * Apply Google-GeminiNotebook update * Detect Google-GeminiNotebook user agent --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- MatomoTracker.php | 1 + tests/Unit/MatomoTrackerTest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/MatomoTracker.php b/MatomoTracker.php index 26f5a28..f77dcbc 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -40,6 +40,7 @@ class MatomoTracker 'Gemini-Deep-Research', 'Claude-User', 'Perplexity-User', + 'Google-GeminiNotebook', 'Google-NotebookLM', ]; diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index 670fe19..de2057a 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -181,6 +181,8 @@ public static function getTestDataForIsUserAgentAIBot(): array ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Gemini-Deep-Research; +https://gemini.google/overview/deep-research/) Chrome/135.0.0.0 Safari/537.36', true], ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +Claude-User@anthropic.com)', true], ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)', true], + ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 (compatible; Google-GeminiNotebook; +https://developers.google.com/crawling/docs/crawlers-fetchers/google-gemininotebook)', true], + ['Google-NotebookLM/1.0', true], ['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36; Devin/1.0; +devin.ai', false], ]; } From cf976bf0c248c5d979d1c3b873eb0ae8a543cea2 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Mon, 27 Jul 2026 17:18:26 +0200 Subject: [PATCH 114/115] Detect the tracked page URL from REQUEST_URI instead of PATH_INFO (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Detect the tracked page URL from REQUEST_URI, not PATH_INFO (#141) getCurrentScriptName() builds the auto-detected page URL (the path between host and query string). It preferred $_SERVER['PATH_INFO'], which only holds the trailing path-info segment — so with front- controller / path-info routing (e.g. /dir1/page handled by dir1/index.php) the tracker recorded a truncated '/page' instead of '/dir1/page'. REQUEST_URI already contains the full requested path (PATH_INFO is always just a suffix of it), so use it as the source and drop PATH_INFO entirely; SCRIPT_NAME stays as the fallback when REQUEST_URI is absent. This also aligns the primary source with Matomo core's Url helper. Reported in #141. * Drop the ticket number from an inline test comment Keep issue references in the CHANGELOG and git history, not in code. --- CHANGELOG.md | 1 + MatomoTracker.php | 26 ++++++++++++++------------ tests/Unit/MatomoTrackerTest.php | 14 +++++++++++--- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b558746..743d797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Attention: this is a major release with breaking changes. - Event and content tracking requests now send `&ca=1` (custom action), so Matomo no longer falls back to recording them as page views if the handling plugin is disabled (#80). - The `cip` (override IP) tracking parameter is now URL-encoded like every other value (#151). - No longer calls the deprecated `curl_close()` (it was already a no-op on the supported PHP versions) (#149). +- Auto-detection of the tracked page URL now uses `REQUEST_URI` as the source instead of `PATH_INFO`. With front-controller / path-info routing (e.g. `/dir1/page` handled by `dir1/index.php`), `PATH_INFO` only holds the trailing `/page`, so the tracker previously recorded a truncated URL; it now records the full requested path. `PATH_INFO` is no longer used at all (`SCRIPT_NAME` remains the fallback when `REQUEST_URI` is unavailable) (#141). ### Added - PHPStan static analysis at max level (`phpstan.neon.dist`) and the Matomo coding standard via PHP_CodeSniffer (`phpcs.xml.dist`), both enforced for every pull request through GitHub Actions. diff --git a/MatomoTracker.php b/MatomoTracker.php index f77dcbc..591ba59 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2621,24 +2621,26 @@ protected function getCookieMatchingName(string $name): string|false } /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "/dir1/dir2/index.php" + * Returns the path portion of the URL the visitor requested (everything between the host and + * the query string). For "http://example.org/dir1/dir2/index.php?param1=value1" this returns + * "/dir1/dir2/index.php"; for a front-controller URL such as "http://example.org/dir1/page" + * (where "/page" is handled by dir1/index.php) it returns "/dir1/page". + * + * The full request path is taken from REQUEST_URI. PATH_INFO is deliberately not used: it only + * holds the trailing path-info segment (e.g. "/page"), so it would drop the directory/script + * prefix and yield a truncated URL. SCRIPT_NAME is the fallback when REQUEST_URI is unavailable. * * @ignore */ protected static function getCurrentScriptName(): string { $url = ''; - if (!empty($_SERVER['PATH_INFO'])) { - $url = self::toStringValue($_SERVER['PATH_INFO']); - } else { - if (!empty($_SERVER['REQUEST_URI'])) { - $requestUri = self::toStringValue($_SERVER['REQUEST_URI']); - if (($pos = strpos($requestUri, '?')) !== false) { - $url = substr($requestUri, 0, $pos); - } else { - $url = $requestUri; - } + if (!empty($_SERVER['REQUEST_URI'])) { + $requestUri = self::toStringValue($_SERVER['REQUEST_URI']); + if (($pos = strpos($requestUri, '?')) !== false) { + $url = substr($requestUri, 0, $pos); + } else { + $url = $requestUri; } } if (empty($url) && isset($_SERVER['SCRIPT_NAME'])) { diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index de2057a..e971e1d 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -1848,17 +1848,24 @@ public function testGetCurrentScriptName(): void unset($_SERVER['PATH_INFO'], $_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']); $this->assertSame('/', TestableMatomoTracker::callGetCurrentScriptName()); + // SCRIPT_NAME is only the fallback when REQUEST_URI is unavailable. $_SERVER['SCRIPT_NAME'] = 'script.php'; $this->assertSame('/script.php', TestableMatomoTracker::callGetCurrentScriptName()); + // REQUEST_URI is the primary source; the query string is stripped. $_SERVER['REQUEST_URI'] = '/dir/page.php?query=1'; $this->assertSame('/dir/page.php', TestableMatomoTracker::callGetCurrentScriptName()); $_SERVER['REQUEST_URI'] = '/dir/other.php'; $this->assertSame('/dir/other.php', TestableMatomoTracker::callGetCurrentScriptName()); - $_SERVER['PATH_INFO'] = '/path/info'; - $this->assertSame('/path/info', TestableMatomoTracker::callGetCurrentScriptName()); + // Front-controller / path-info routing: with a request for /dir1/page handled by + // dir1/index.php, PATH_INFO is only "/page". The full requested path must still be tracked, + // so REQUEST_URI wins and PATH_INFO is ignored (previously it truncated the URL to "/page"). + $_SERVER['REQUEST_URI'] = '/dir1/page'; + $_SERVER['PATH_INFO'] = '/page'; + $_SERVER['SCRIPT_NAME'] = '/dir1/index.php'; + $this->assertSame('/dir1/page', TestableMatomoTracker::callGetCurrentScriptName()); } public function testGetCurrentQueryStringAndUrl(): void @@ -1871,7 +1878,8 @@ public function testGetCurrentQueryStringAndUrl(): void $_SERVER['HTTPS'] = 'on'; $_SERVER['HTTP_HOST'] = 'matomo.example'; - $_SERVER['PATH_INFO'] = '/page'; + unset($_SERVER['PATH_INFO']); + $_SERVER['REQUEST_URI'] = '/page'; $this->assertSame('https://matomo.example/page?a=b&c=d', TestableMatomoTracker::callGetCurrentUrl()); } From 4818aa947dac9216b43fe479988b7df96119d049 Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Mon, 3 Aug 2026 15:09:53 +0200 Subject: [PATCH 115/115] Avoid PHP 8.5 deprecation notice when loading the tracker (#156) PHP 8.5 deprecated the predefined locally scoped $http_response_header variable, and emits the notice at compile time. The stream transport's fallback read therefore triggered it merely by loading MatomoTracker.php, even on 8.5 where http_get_last_response_headers() is used instead - so it could neither be suppressed nor avoided by the function_exists() guard, and tools turning diagnostics into exceptions (e.g. Psalm) aborted while autoloading the class. Assigning the variable before reading it silences the diagnostic and keeps the PHP < 8.5 fallback working. Fixes #155 --- CHANGELOG.md | 5 +++++ MatomoTracker.php | 10 ++++++++-- tests/Unit/MatomoTrackerTest.php | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 743d797..6eef919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ This is the Developer Changelog for Matomo PHP Tracker. All breaking changes or new features are listed below. +## Matomo PHP Tracker 4.0.1 + +### Fixed +- Loading `MatomoTracker.php` no longer emits a deprecation notice for the predefined `$http_response_header` variable on PHP 8.5. PHP reports it at compile time, so it was emitted on every include (#155). + ## Matomo PHP Tracker 4.0.0 Attention: this is a major release with breaking changes. diff --git a/MatomoTracker.php b/MatomoTracker.php index 591ba59..92a43e6 100644 --- a/MatomoTracker.php +++ b/MatomoTracker.php @@ -2400,6 +2400,12 @@ protected function sendRequest(#[\SensitiveParameter] string $url, string $metho $stream_options = $this->prepareStreamOptions($method, $data, $forcePostUrlEncoded); $ctx = stream_context_create($stream_options); + + // $http_response_header must be assigned before the fallback read below: PHP 8.5 + // deprecated the predefined variable and reports it at compile time, so the read would + // otherwise emit a notice merely by loading this file. PHP still overwrites the value. + $http_response_header = []; + $response = @file_get_contents($url, false, $ctx); if ($response === false && $this->exceptionsEnabled) { // Only include the host (never the query string, which carries token_auth/PII) in the message. @@ -2414,8 +2420,8 @@ protected function sendRequest(#[\SensitiveParameter] string $url, string $metho $responseHeaders = $headers; } } elseif ($response !== false) { - // PHP populates $http_response_header in the local scope whenever an HTTP response - // was received; the $response !== false guard guarantees that is the case here. + // PHP < 8.5 has no http_get_last_response_headers() and populates the local + // variable instead, which it only does when a response was actually received. $responseHeaders = $http_response_header; } diff --git a/tests/Unit/MatomoTrackerTest.php b/tests/Unit/MatomoTrackerTest.php index e971e1d..152eb72 100644 --- a/tests/Unit/MatomoTrackerTest.php +++ b/tests/Unit/MatomoTrackerTest.php @@ -546,6 +546,29 @@ public function testStreamTransportFailSafeReturnsFalseWhenExceptionsDisabled(): $this->assertFalse($tracker->doTrackPageView('some title')); } + /** + * Loading the tracker must not emit any notice, as tools that turn those into exceptions + * (e.g. Psalm) would abort while autoloading the class. This is what the `$http_response_header` + * assignment in `sendRequest()` guards, so that assignment must stay above the read following it. + */ + public function testLoadingTheTrackerEmitsNoDeprecationNotice(): void + { + // -n ignores the environment's php.ini, so that unrelated startup diagnostics (e.g. a + // dangling extension line) cannot fail this test + $command = escapeshellarg(PHP_BINARY) + . ' -n -d error_reporting=-1 -d display_errors=1 -d log_errors=0 -r ' + . escapeshellarg('include ' . var_export(dirname(__DIR__, 2) . '/MatomoTracker.php', true) . '; echo \'loaded\';') + . ' 2>&1'; + + $output = []; + $exitCode = -1; + exec($command, $output, $exitCode); + + // expecting the marker rather than just no output also catches a child that never ran + $this->assertSame('loaded', trim(implode("\n", $output)), 'Loading the tracker must not emit any notice'); + $this->assertSame(0, $exitCode); + } + public function testGetUrlTrackCrash(): void { $tracker = $this->createTracker();