name = $cookieName; $this->path = $path; $this->expire = $expire; if (is_null($expire) || !is_numeric($expire) || $expire < 0 ) { $this->expire = $this->getDefaultExpire(); } $this->keyStore = $keyStore; if ($this->isCookieFound()) { $this->loadContentFromCookie(); } } /** * Returns true if the visitor already has the cookie. * * @return bool */ public function isCookieFound() { return isset($_COOKIE[$this->name]); } /** * Returns the default expiry time, 2 years * * @return int Timestamp in 2 years */ protected function getDefaultExpire() { return time() + 86400 * 365 * 2; } /** * setcookie() replacement -- we don't use the built-in function because * it is buggy for some PHP versions. * * @link http://php.net/setcookie * * @param string $Name Name of cookie * @param string $Value Value of cookie * @param int $Expires Time the cookie expires * @param string $Path * @param string $Domain * @param bool $Secure * @param bool $HTTPOnly */ protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false) { if (!empty($Domain)) { // Fix the domain to accept domains with and without 'www.'. if (!strncasecmp($Domain, 'www.', 4)) { $Domain = substr($Domain, 4); } $Domain = '.' . $Domain; // Remove port information. $Port = strpos($Domain, ':'); if ($Port !== false) $Domain = substr($Domain, 0, $Port); } $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value) . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT') . (empty($Path) ? '' : '; path=' . $Path) . (empty($Domain) ? '' : '; domain=' . $Domain) . (!$Secure ? '' : '; secure') . (!$HTTPOnly ? '' : '; HttpOnly'); Common::sendHeader($header, false); } /** * We set the privacy policy header */ protected function setP3PHeader() { Common::sendHeader("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'"); } /** * Delete the cookie */ public function delete() { $this->setP3PHeader(); $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain); } /** * Saves the cookie (set the Cookie header). * You have to call this method before sending any text to the browser or you would get the * "Header already sent" error. */ public function save() { $cookieString = $this->generateContentString(); if (strlen($cookieString) > self::MAX_COOKIE_SIZE) { // If the cookie was going to be too large, instead, delete existing cookie and start afresh $this->delete(); return; } $this->setP3PHeader(); $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly); } /** * Extract signed content from string: content VALUE_SEPARATOR '_=' signature * * @param string $content * @return string|bool Content or false if unsigned */ private function extractSignedContent($content) { $signature = substr($content, -40); if (substr($content, -43, 3) == self::VALUE_SEPARATOR . '_=' && $signature == sha1(substr($content, 0, -40) . SettingsPiwik::getSalt()) ) { // strip trailing: VALUE_SEPARATOR '_=' signature" return substr($content, 0, -43); } return false; } /** * Load the cookie content into a php array. * Parses the cookie string to extract the different variables. * Unserialize the array when necessary. * Decode the non numeric values that were base64 encoded. */ protected function loadContentFromCookie() { $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]); if ($cookieStr === false) { return; } $values = explode(self::VALUE_SEPARATOR, $cookieStr); foreach ($values as $nameValue) { $equalPos = strpos($nameValue, '='); $varName = substr($nameValue, 0, $equalPos); $varValue = substr($nameValue, $equalPos + 1); // no numeric value are base64 encoded so we need to decode them if (!is_numeric($varValue)) { $tmpValue = base64_decode($varValue); $varValue = safe_unserialize($tmpValue); // discard entire cookie // note: this assumes we never serialize a boolean if ($varValue === false && $tmpValue !== 'b:0;') { $this->value = array(); unset($_COOKIE[$this->name]); break; } } $this->value[$varName] = $varValue; } } /** * Returns the string to save in the cookie from the $this->value array of values. * It goes through the array and generates the cookie content string. * * @return string Cookie content */ protected function generateContentString() { $cookieStr = ''; foreach ($this->value as $name => $value) { if (!is_numeric($value)) { $value = base64_encode(safe_serialize($value)); } $cookieStr .= "$name=$value" . self::VALUE_SEPARATOR; } if (!empty($cookieStr)) { $cookieStr .= '_='; // sign cookie $signature = sha1($cookieStr . SettingsPiwik::getSalt()); return $cookieStr . $signature; } return ''; } /** * Set cookie domain * * @param string $domain */ public function setDomain($domain) { $this->domain = $domain; } /** * Set secure flag * * @param bool $secure */ public function setSecure($secure) { $this->secure = $secure; } /** * Set HTTP only * * @param bool $httponly */ public function setHttpOnly($httponly) { $this->httponly = $httponly; } /** * Registers a new name => value association in the cookie. * * Registering new values is optimal if the value is a numeric value. * If the value is a string, it will be saved as a base64 encoded string. * If the value is an array, it will be saved as a serialized and base64 encoded * string which is not very good in terms of bytes usage. * You should save arrays only when you are sure about their maximum data size. * A cookie has to stay small and its size shouldn't increase over time! * * @param string $name Name of the value to save; the name will be used to retrieve this value * @param string|array|number $value Value to save. If null, entry will be deleted from cookie. */ public function set($name, $value) { $name = self::escapeValue($name); // Delete value if $value === null if (is_null($value)) { if ($this->keyStore === false) { unset($this->value[$name]); return; } unset($this->value[$this->keyStore][$name]); return; } if ($this->keyStore === false) { $this->value[$name] = $value; return; } $this->value[$this->keyStore][$name] = $value; } /** * Returns the value defined by $name from the cookie. * * @param string|integer Index name of the value to return * @return mixed The value if found, false if the value is not found */ public function get($name) { $name = self::escapeValue($name); if (false === $this->keyStore) { if (isset($this->value[$name])) { return self::escapeValue($this->value[$name]); } return false; } if (isset($this->value[$this->keyStore][$name])) { return self::escapeValue($this->value[$this->keyStore][$name]); } return false; } /** * Returns an easy to read cookie dump * * @return string The cookie dump */ public function __toString() { $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, "; $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n"; $str .= var_export($this->value, $return = true); return $str; } /** * Escape values from the cookie before sending them back to the client * (when using the get() method). * * @param string $value Value to be escaped * @return mixed The value once cleaned. */ protected static function escapeValue($value) { return Common::sanitizeInputValues($value); } }