PageRenderTime 48ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Cookie.php

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