PageRenderTime 40ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/core/Cookie.php

https://github.com/quarkness/piwik
PHP | 396 lines | 189 code | 41 blank | 166 comment | 21 complexity | 5577b067722606450ab5b27611aecbc1 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * Simple class to handle the cookies:
  14. * - read a cookie values
  15. * - edit an existing cookie and save it
  16. * - create a new cookie, set values, expiration date, etc. and save it
  17. *
  18. * @package Piwik
  19. */
  20. class Piwik_Cookie
  21. {
  22. /**
  23. * Don't create a cookie bigger than 1k
  24. */
  25. const MAX_COOKIE_SIZE = 1024;
  26. /**
  27. * The name of the cookie
  28. */
  29. protected $name = null;
  30. /**
  31. * The expire time for the cookie (expressed in UNIX Timestamp)
  32. */
  33. protected $expire = null;
  34. /**
  35. * Restrict cookie path
  36. */
  37. protected $path = '';
  38. /**
  39. * Restrict cookie to a domain (or subdomains)
  40. */
  41. protected $domain = '';
  42. /**
  43. * If true, cookie should only be transmitted over secure HTTPS
  44. */
  45. protected $secure = false;
  46. /**
  47. * If true, cookie will only be made available via the HTTP protocol.
  48. * Note: not well supported by browsers.
  49. */
  50. protected $httponly = false;
  51. /**
  52. * The content of the cookie
  53. */
  54. protected $value = array();
  55. /**
  56. * The character used to separate the tuple name=value in the cookie
  57. */
  58. const VALUE_SEPARATOR = ':';
  59. /**
  60. * Instantiate a new Cookie object and tries to load the cookie content if the cookie
  61. * exists already.
  62. *
  63. * @param string $cookieName cookie Name
  64. * @param int $expire The timestamp after which the cookie will expire, eg time() + 86400; use 0 (int zero) to expire cookie at end of browser session
  65. * @param string $path The path on the server in which the cookie will be available on.
  66. * @param string $keyStore Will be used to store several bits of data (eg. one array per website)
  67. */
  68. public function __construct( $cookieName, $expire = null, $path = null, $keyStore = false)
  69. {
  70. $this->name = $cookieName;
  71. $this->path = $path;
  72. $this->expire = $expire;
  73. if(is_null($expire)
  74. || !is_numeric($expire)
  75. || $expire < 0)
  76. {
  77. $this->expire = $this->getDefaultExpire();
  78. }
  79. $this->keyStore = $keyStore;
  80. if($this->isCookieFound())
  81. {
  82. $this->loadContentFromCookie();
  83. }
  84. }
  85. /**
  86. * Returns true if the visitor already has the cookie.
  87. *
  88. * @return bool
  89. */
  90. public function isCookieFound()
  91. {
  92. return isset($_COOKIE[$this->name]);
  93. }
  94. /**
  95. * Returns the default expiry time, 2 years
  96. *
  97. * @return int Timestamp in 2 years
  98. */
  99. protected function getDefaultExpire()
  100. {
  101. return time() + 86400*365*2;
  102. }
  103. /**
  104. * setcookie() replacement -- we don't use the built-in function because
  105. * it is buggy for some PHP versions.
  106. *
  107. * @link http://php.net/setcookie
  108. *
  109. * @param string $Name Name of cookie
  110. * @param string $Value Value of cookie
  111. * @param int $Expires Time the cookie expires
  112. * @param string $Path
  113. * @param string $Domain
  114. * @param bool $Secure
  115. * @param bool $HTTPOnly
  116. */
  117. protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false)
  118. {
  119. if (!empty($Domain))
  120. {
  121. // Fix the domain to accept domains with and without 'www.'.
  122. if (!strncasecmp($Domain, 'www.', 4))
  123. {
  124. $Domain = substr($Domain, 4);
  125. }
  126. $Domain = '.' . $Domain;
  127. // Remove port information.
  128. $Port = strpos($Domain, ':');
  129. if ($Port !== false) $Domain = substr($Domain, 0, $Port);
  130. }
  131. $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value)
  132. . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT')
  133. . (empty($Path) ? '' : '; path=' . $Path)
  134. . (empty($Domain) ? '' : '; domain=' . $Domain)
  135. . (!$Secure ? '' : '; secure')
  136. . (!$HTTPOnly ? '' : '; HttpOnly');
  137. header($header, false);
  138. }
  139. /**
  140. * We set the privacy policy header
  141. */
  142. protected function setP3PHeader()
  143. {
  144. header("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'");
  145. }
  146. /**
  147. * Delete the cookie
  148. */
  149. public function delete()
  150. {
  151. $this->setP3PHeader();
  152. $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain);
  153. }
  154. /**
  155. * Saves the cookie (set the Cookie header).
  156. * You have to call this method before sending any text to the browser or you would get the
  157. * "Header already sent" error.
  158. */
  159. public function save()
  160. {
  161. $cookieString = $this->generateContentString();
  162. if(strlen($cookieString) > self::MAX_COOKIE_SIZE)
  163. {
  164. // If the cookie was going to be too large, instead, delete existing cookie and start afresh
  165. // This will result in slightly less accuracy in the case
  166. // where someone visits more than dozen websites tracked by the same Piwik
  167. // This will usually be the Piwik super user itself checking all his websites regularly
  168. $this->delete();
  169. return;
  170. }
  171. $this->setP3PHeader();
  172. $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly);
  173. }
  174. /**
  175. * Extract signed content from string: content VALUE_SEPARATOR '_=' signature
  176. *
  177. * @param string $content
  178. * @return string|false Content or false if unsigned
  179. */
  180. private function extractSignedContent($content)
  181. {
  182. $signature = substr($content, -40);
  183. if(substr($content, -43, 3) == self::VALUE_SEPARATOR . '_=' &&
  184. $signature == sha1(substr($content, 0, -40) . Piwik_Common::getSalt()))
  185. {
  186. // strip trailing: VALUE_SEPARATOR '_=' signature"
  187. return substr($content, 0, -43);
  188. }
  189. return false;
  190. }
  191. /**
  192. * Load the cookie content into a php array.
  193. * Parses the cookie string to extract the different variables.
  194. * Unserialize the array when necessary.
  195. * Decode the non numeric values that were base64 encoded.
  196. */
  197. protected function loadContentFromCookie()
  198. {
  199. $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]);
  200. if($cookieStr === false)
  201. {
  202. return;
  203. }
  204. $values = explode( self::VALUE_SEPARATOR, $cookieStr);
  205. foreach($values as $nameValue)
  206. {
  207. $equalPos = strpos($nameValue, '=');
  208. $varName = substr($nameValue,0,$equalPos);
  209. $varValue = substr($nameValue,$equalPos+1);
  210. // no numeric value are base64 encoded so we need to decode them
  211. if(!is_numeric($varValue))
  212. {
  213. $tmpValue = base64_decode($varValue);
  214. $varValue = safe_unserialize($tmpValue);
  215. // discard entire cookie
  216. // note: this assumes we never serialize a boolean
  217. if($varValue === false && $tmpValue !== 'b:0;')
  218. {
  219. $this->value = array();
  220. unset($_COOKIE[$this->name]);
  221. break;
  222. }
  223. }
  224. $this->value[$varName] = $varValue;
  225. }
  226. }
  227. /**
  228. * Returns the string to save in the cookie from the $this->value array of values.
  229. * It goes through the array and generates the cookie content string.
  230. *
  231. * @return string Cookie content
  232. */
  233. protected function generateContentString()
  234. {
  235. $cookieStr = '';
  236. foreach($this->value as $name=>$value)
  237. {
  238. if(!is_numeric($value))
  239. {
  240. $value = base64_encode(safe_serialize($value));
  241. }
  242. $cookieStr .= "$name=$value" . self::VALUE_SEPARATOR;
  243. }
  244. if(!empty($cookieStr))
  245. {
  246. $cookieStr .= '_=';
  247. // sign cookie
  248. $signature = sha1($cookieStr . Piwik_Common::getSalt());
  249. return $cookieStr . $signature;
  250. }
  251. return '';
  252. }
  253. /**
  254. * Set cookie domain
  255. *
  256. * @param string $domain
  257. */
  258. public function setDomain($domain)
  259. {
  260. $this->domain = $domain;
  261. }
  262. /**
  263. * Set secure flag
  264. *
  265. * @param bool $secure
  266. */
  267. public function setSecure($secure)
  268. {
  269. $this->secure = $secure;
  270. }
  271. /**
  272. * Set HTTP only
  273. *
  274. * @param bool $httponly
  275. */
  276. public function setHttpOnly($httponly)
  277. {
  278. $this->httponly = $httponly;
  279. }
  280. /**
  281. * Registers a new name => value association in the cookie.
  282. *
  283. * Registering new values is optimal if the value is a numeric value.
  284. * If the value is a string, it will be saved as a base64 encoded string.
  285. * If the value is an array, it will be saved as a serialized and base64 encoded
  286. * string which is not very good in terms of bytes usage.
  287. * You should save arrays only when you are sure about their maximum data size.
  288. * A cookie has to stay small and its size shouldn't increase over time!
  289. *
  290. * @param string Name of the value to save; the name will be used to retrieve this value
  291. * @param string|array|numeric Value to save. If null, entry will be deleted from cookie.
  292. */
  293. public function set( $name, $value )
  294. {
  295. $name = self::escapeValue($name);
  296. // Delete value if $value === null
  297. if(is_null($value))
  298. {
  299. if($this->keyStore === false)
  300. {
  301. unset($this->value[$name]);
  302. return;
  303. }
  304. unset($this->value[$this->keyStore][$name]);
  305. return;
  306. }
  307. if($this->keyStore === false)
  308. {
  309. $this->value[$name] = $value;
  310. return;
  311. }
  312. $this->value[$this->keyStore][$name] = $value;
  313. }
  314. /**
  315. * Returns the value defined by $name from the cookie.
  316. *
  317. * @param string|integer Index name of the value to return
  318. * @return mixed The value if found, false if the value is not found
  319. */
  320. public function get( $name )
  321. {
  322. $name = self::escapeValue($name);
  323. if($this->keyStore === false)
  324. {
  325. return isset($this->value[$name])
  326. ? self::escapeValue($this->value[$name])
  327. : false;
  328. }
  329. return isset($this->value[$this->keyStore][$name])
  330. ? self::escapeValue($this->value[$this->keyStore][$name])
  331. : false;
  332. }
  333. /**
  334. * Returns an easy to read cookie dump
  335. *
  336. * @return string The cookie dump
  337. */
  338. public function __toString()
  339. {
  340. $str = 'COOKIE '.$this->name.', rows count: '.count($this->value). ', cookie size = '.strlen($this->generateContentString())." bytes\n";
  341. $str .= var_export($this->value, $return = true);
  342. return $str;
  343. }
  344. /**
  345. * Escape values from the cookie before sending them back to the client
  346. * (when using the get() method).
  347. *
  348. * @param string $value Value to be escaped
  349. * @return mixed The value once cleaned.
  350. */
  351. static protected function escapeValue( $value )
  352. {
  353. return Piwik_Common::sanitizeInputValues($value);
  354. }
  355. }