diff options
author | mattab <matthieu.aubry@gmail.com> | 2013-10-24 08:36:09 +0400 |
---|---|---|
committer | mattab <matthieu.aubry@gmail.com> | 2013-10-24 08:36:09 +0400 |
commit | 87e938344233b997855af9c438f7b355e3784d34 (patch) | |
tree | d59eceaeff112fbef302fb9f8e21b67efb3f6ab6 /libs | |
parent | 03805c84c3a4774de52104b6b10ba833d661da9c (diff) | |
parent | 6e78c7bcb16929ab1f9ee0cbfc6b3dbcf4ed0404 (diff) |
Merge branch 'master' into cevents
Diffstat (limited to 'libs')
-rw-r--r-- | libs/PiwikTracker/PiwikTracker.php | 257 |
1 files changed, 206 insertions, 51 deletions
diff --git a/libs/PiwikTracker/PiwikTracker.php b/libs/PiwikTracker/PiwikTracker.php index 8456b6687a..8dbee694b3 100644 --- a/libs/PiwikTracker/PiwikTracker.php +++ b/libs/PiwikTracker/PiwikTracker.php @@ -61,6 +61,11 @@ class PiwikTracker const DEFAULT_CHARSET_PARAMETER_VALUES = 'utf-8'; /** + * See piwik.js + */ + const FIRST_PARTY_COOKIES_PREFIX = '_pk_'; + + /** * Builds a PiwikTracker object, used to track visits, pages and Goal conversions * for a specific website, by using the Piwik Tracking API. * @@ -70,14 +75,12 @@ class PiwikTracker */ function __construct($idSite, $apiUrl = '') { - $this->cookieSupport = true; $this->userAgent = false; $this->localHour = false; $this->localMinute = false; $this->localSecond = false; $this->hasCookies = false; $this->plugins = false; - $this->visitorCustomVar = false; $this->pageCustomVar = false; $this->eventCustomVar = false; $this->customData = false; @@ -88,7 +91,6 @@ class PiwikTracker $this->ecommerceItems = array(); $this->generationTime = false; - $this->requestCookie = ''; $this->idSite = $idSite; $this->urlReferrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false; $this->pageCharset = self::DEFAULT_CHARSET_PARAMETER_VALUES; @@ -99,12 +101,38 @@ class PiwikTracker if (!empty($apiUrl)) { self::$URL = $apiUrl; } + + // Life of the visitor cookie (in sec) + $this->configVisitorCookieTimeout = 63072000; // 2 years + // 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->forcedVisitorId = false; + $this->cookieVisitorId = false; + $this->randomVisitorId = false; + $this->setNewVisitorId(); + $this->configCookiesDisabled = false; + $this->configCookiePath = '/'; + $this->configCookieDomain = ''; + + $this->currentTs = time(); + $this->createTs = $this->currentTs; + $this->visitCount = false; + $this->currentVisitTs = false; + $this->lastVisitTs = false; + $this->lastEcommerceOrderTs = false; + // Allow debug while blocking the request $this->requestTimeout = 600; $this->doBulkRequests = false; $this->storedTrackingActions = array(); + + $this->visitorCustomVar = $this->getCustomVariablesFromCookie(); } /** @@ -166,6 +194,9 @@ class PiwikTracker * This must be a JSON encoded string that would typically be fetched from the JS API: * piwikTracker.getAttributionInfo() and that you have JSON encoded via JSON2.stringify() * + * If you call enableCookies() then these referral attribution values will be set + * to the 'ref' first party cookie storing referral information. + * * @param string $jsonEncoded JSON encoded array containing Attribution info * @throws Exception * @see function getAttributionInfo() in https://github.com/piwik/piwik/blob/master/js/piwik.js @@ -229,15 +260,10 @@ class PiwikTracker if (!empty($this->visitorCustomVar[$id])) { return $this->visitorCustomVar[$id]; } - $customVariablesCookie = 'cvar.' . $this->idSite . '.'; - $cookie = $this->getCookieMatchingName($customVariablesCookie); - if (!$cookie) { - return false; - } + $cookieDecoded = $this->getCustomVariablesFromCookie(); if (!is_int($id)) { throw new Exception("Parameter to getCustomVariable should be an integer"); } - $cookieDecoded = json_decode($cookie, $assoc = true); if (!is_array($cookieDecoded) || !isset($cookieDecoded[$id]) || !is_array($cookieDecoded[$id]) @@ -253,8 +279,9 @@ class PiwikTracker */ public function setNewVisitorId() { - $this->visitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID); + $this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID); $this->forcedVisitorId = false; + $this->cookieVisitorId = false; } /** @@ -358,6 +385,45 @@ class PiwikTracker } /** + * Enable Cookie Creation - this will cause a first party VisitorId cookie to be set when the VisitorId is set or reset + * + * @param string $domain (optional) Set first-party cookie domain. Accepted values: example.com, *.example.com (same as .example.com) or subdomain.example.com + * @param string $path (optional) Set first-party cookie path + */ + public function enableCookies( $domain = '', $path = '/' ) + { + $this->configCookiesDisabled = false; + $this->configCookieDomain = self::domainFixup($domain); + $this->configCookiePath = $path; + } + + /** + * Fix-up domain + */ + static protected function domainFixup($domain) + { + $dl = strlen($domain) - 1; + // remove trailing '.' + if ($domain{$dl} === '.') { + $domain = substr($domain, 0, $dl); + } + // remove leading '*' + if (substr($domain, 0, 2) === '*.') { + $domain = substr($domain, 1); + } + return $domain; + } + + /** + * Get cookie name with prefix and domain hash + */ + protected function getCookieName($cookieName) { + // NOTE: If the cookie name is changed, we must also update the method in piwik.js with the same name. + $hash = substr( sha1( ($this->configCookieDomain == '' ? self::getCurrentHost() : $this->configCookieDomain) . $this->configCookiePath ), 0, 4); + return self::FIRST_PARTY_COOKIES_PREFIX . $cookieName . '.' . $this->idSite . '.' . $hash; + } + + /** * Tracks a page view * * @param string $documentTitle Page title as it will appear in the Actions > Page titles report @@ -389,8 +455,8 @@ class PiwikTracker * These are used to populate reports in Actions > Site Search. * * @param string $keyword Searched query on the site - * @param string $category Optional, Search engine category if applicable - * @param bool|int $countResults results displayed on the search result page. Used to track "zero result" keywords. + * @param string $category (optional) Search engine category if applicable + * @param bool|int $countResults (optional) results displayed on the search result page. Used to track "zero result" keywords. * * @return mixed Response or true if using bulk requests. */ @@ -808,19 +874,49 @@ class PiwikTracker { if (!empty($this->forcedVisitorId)) { return $this->forcedVisitorId; + } else if ($this->loadVisitorIdCookie()) { + return $this->cookieVisitorId; + } else { + return $this->randomVisitorId; } + } - $idCookieName = 'id.' . $this->idSite . '.'; - $idCookie = $this->getCookieMatchingName($idCookieName); - if ($idCookie !== false) { - $visitorId = substr($idCookie, 0, strpos($idCookie, '.')); - if (strlen($visitorId) == self::LENGTH_VISITOR_ID) { - return $visitorId; - } + /** + * Loads values from the VisitorId Cookie + * + * @return bool True if cookie exists and is valid, False otherwise + */ + protected function loadVisitorIdCookie() + { + $idCookie = $this->getCookieMatchingName('id'); + if ($idCookie === false) { + return false; } - return $this->visitorId; + $parts = explode('.', $idCookie); + if (strlen($parts[0]) != self::LENGTH_VISITOR_ID) { + return false; + } + $this->cookieVisitorId = $parts[0]; // provides backward compatibility since getVisitorId() didn't change any existing VisitorId value + $this->createTs = $parts[1]; + $this->visitCount = $parts[2]; + $this->currentVisitTs = $parts[3]; + $this->lastVisitTs = $parts[4]; + $this->lastEcommerceOrderTs = $parts[5]; + return true; } - + + /** + * Deletes all first party cookies from the client + */ + public function deleteCookies() + { + $expire = $this->currentTs - 86400; + $cookies = array('id', 'ses', 'cvar', 'ref'); + foreach($cookies as $cookie) { + $this->setCookie($cookie, '', $expire); + } + } + /** * Returns the currently assigned Attribution Information stored in a first party cookie. * @@ -833,8 +929,10 @@ class PiwikTracker */ public function getAttributionInfo() { - $attributionCookieName = 'ref.' . $this->idSite . '.'; - return $this->getCookieMatchingName($attributionCookieName); + if(!empty($this->attributionInfo)) { + return $this->attributionInfo; + } + return $this->getCookieMatchingName('ref'); } /** @@ -926,13 +1024,13 @@ class PiwikTracker } /** - * By default, PiwikTracker will read third party cookies - * from the response and sets them in the next request. + * By default, PiwikTracker will read first party cookies + * from the request and write updated cookies in the response (using setrawcookie). * This can be disabled by calling this function. */ public function disableCookieSupport() { - $this->cookieSupport = false; + $this->configCookiesDisabled = true; } /** @@ -974,9 +1072,6 @@ class PiwikTracker return true; } - if (!$this->cookieSupport) { - $this->requestCookie = ''; - } if (function_exists('curl_init')) { $options = array( CURLOPT_URL => $url, @@ -985,8 +1080,7 @@ class PiwikTracker CURLOPT_TIMEOUT => $this->requestTimeout, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array( - 'Accept-Language: ' . $this->acceptLanguage, - 'Cookie: ' . $this->requestCookie, + 'Accept-Language: ' . $this->acceptLanguage )); switch ($method) { @@ -1009,7 +1103,7 @@ class PiwikTracker ob_start(); $response = @curl_exec($ch); ob_end_clean(); - $header = $content = ''; + $content = ''; if (!empty($response)) { list($header, $content) = explode("\r\n\r\n", $response, $limitCount = 2); } @@ -1018,8 +1112,7 @@ class PiwikTracker 'http' => array( 'method' => $method, 'user_agent' => $this->userAgent, - 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n" . - "Cookie: " . $this->requestCookie . "\r\n", + 'header' => "Accept-Language: " . $this->acceptLanguage . "\r\n", 'timeout' => $this->requestTimeout, // PHP 5.2.1 ) ); @@ -1029,27 +1122,10 @@ class PiwikTracker $stream_options['http']['header'] .= "Content-Type: application/json \r\n"; $stream_options['http']['content'] = $data; } - $ctx = stream_context_create($stream_options); $response = file_get_contents($url, 0, $ctx); - $header = implode("\r\n", $http_response_header); $content = $response; } - // The cookie in the response will be set in the next request - preg_match_all('/^Set-Cookie: (.*?);/m', $header, $cookie); - if (!empty($cookie[1])) { - // in case several cookies returned, we keep only the latest one (ie. XDEBUG puts its cookie first in the list) - if (is_array($cookie[1])) { - $cookie = end($cookie[1]); - } else { - $cookie = $cookie[1]; - } - // XDEBUG is a PHP Debugger - if (strpos($cookie, 'XDEBUG') === false) { - $this->requestCookie = $cookie; - } - } - return $content; } @@ -1085,6 +1161,8 @@ class PiwikTracker */ protected function getRequest($idSite) { + $this->setFirstPartyCookies(); + $url = $this->getBaseUrl() . '?idsite=' . $idSite . '&rec=1' . @@ -1101,6 +1179,12 @@ class PiwikTracker (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') . ((!empty($this->token_auth) && !$this->doBulkRequests) ? '&token_auth=' . urlencode($this->token_auth) : '') . + // Values collected from cookie + '&_idts=' . $this->createTs . + '&_idvc=' . $this->visitCount . + (!empty($this->lastVisitTs) ? '&_viewts=' . $this->lastVisitTs : '' ) . + (!empty($this->lastEcommerceOrderTs) ? '&_ects=' . $this->lastEcommerceOrderTs : '' ) . + // These parameters are set by the JS, but optional when using API (!empty($this->plugins) ? $this->plugins : '') . (($this->localHour !== false && $this->localMinute !== false && $this->localSecond !== false) ? '&h=' . $this->localHour . '&m=' . $this->localMinute . '&s=' . $this->localSecond : '') . @@ -1156,6 +1240,11 @@ class PiwikTracker */ protected function getCookieMatchingName($name) { + if($this->configCookiesDisabled) { + return false; + } + $name = $this->getCookieName($name); + // Piwik cookie names use dots separators in piwik.js, // but PHP Replaces . with _ http://www.php.net/manual/en/language.variables.predefined.php#72571 $name = str_replace('.', '_', $name); @@ -1259,6 +1348,72 @@ class PiwikTracker . self::getCurrentScriptName() . self::getCurrentQueryString(); } + + /** + * Sets the first party cookies as would the piwik.js + * All cookies are supported: 'id' and 'ses' and 'ref' and 'cvar' cookies. + */ + protected function setFirstPartyCookies() + { + if ($this->configCookiesDisabled) { + return; + } + + if (empty($this->cookieVisitorId)) { + $this->loadVisitorIdCookie(); + } + + // Set the 'ses' cookie + if (!$this->getCookieMatchingName('ses')) { + // new session (new visit) + $this->visitCount++; + $this->lastVisitTs = $this->currentVisitTs; + + // Set the 'ref' cookie + $attributionInfo = $this->getAttributionInfo(); + if(!empty($attributionInfo)) { + $this->setCookie('ref', $attributionInfo, $this->configReferralCookieTimeout); + } + } + $this->setCookie('ses', '*', $this->configSessionCookieTimeout); + + // Set the 'id' cookie + $cookieValue = $this->getVisitorId() . '.' . $this->createTs . '.' . $this->visitCount . '.' . $this->currentTs . '.' . $this->lastVisitTs . '.' . $this->lastEcommerceOrderTs; + $this->setCookie('id', $cookieValue, $this->configVisitorCookieTimeout); + + // Set the 'cvar' cookie + $this->setCookie('cvar', json_encode($this->visitorCustomVar), $this->configSessionCookieTimeout); + + } + + /** + * 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. + * + * @param $cookieName + * @param $cookieValue + * @param $cookieTTL + */ + protected function setCookie($cookieName, $cookieValue, $cookieTTL) + { + $cookieExpire = $this->createTs + $cookieTTL; + if(!headers_sent()) { + setrawcookie($this->getCookieName($cookieName), $cookieValue, $cookieExpire, $this->configCookiePath, $this->configCookieDomain); + } + } + + /** + * @return bool|mixed + */ + protected function getCustomVariablesFromCookie() + { + $cookie = $this->getCookieMatchingName('cvar'); + if (!$cookie) { + return false; + } + return json_decode($cookie, $assoc = true); + } } /** |