/wp-includes/Requests/Cookie.php

  1. <?php 
  2. /** 
  3. * Cookie storage object 
  4. * 
  5. * @package Requests 
  6. * @subpackage Cookies 
  7. */ 
  8.  
  9. /** 
  10. * Cookie storage object 
  11. * 
  12. * @package Requests 
  13. * @subpackage Cookies 
  14. */ 
  15. class Requests_Cookie { 
  16. /** 
  17. * Cookie name. 
  18. * 
  19. * @var string 
  20. */ 
  21. public $name; 
  22.  
  23. /** 
  24. * Cookie value. 
  25. * 
  26. * @var string 
  27. */ 
  28. public $value; 
  29.  
  30. /** 
  31. * Cookie attributes 
  32. * 
  33. * Valid keys are (currently) path, domain, expires, max-age, secure and 
  34. * httponly. 
  35. * 
  36. * @var Requests_Utility_CaseInsensitiveDictionary|array Array-like object 
  37. */ 
  38. public $attributes = array(); 
  39.  
  40. /** 
  41. * Cookie flags 
  42. * 
  43. * Valid keys are (currently) creation, last-access, persistent and 
  44. * host-only. 
  45. * 
  46. * @var array 
  47. */ 
  48. public $flags = array(); 
  49.  
  50. /** 
  51. * Reference time for relative calculations 
  52. * 
  53. * This is used in place of `time()` when calculating Max-Age expiration and 
  54. * checking time validity. 
  55. * 
  56. * @var int 
  57. */ 
  58. public $reference_time = 0; 
  59.  
  60. /** 
  61. * Create a new cookie object 
  62. * 
  63. * @param string $name 
  64. * @param string $value 
  65. * @param array|Requests_Utility_CaseInsensitiveDictionary $attributes Associative array of attribute data 
  66. */ 
  67. public function __construct($name, $value, $attributes = array(), $flags = array(), $reference_time = null) { 
  68. $this->name = $name; 
  69. $this->value = $value; 
  70. $this->attributes = $attributes; 
  71. $default_flags = array( 
  72. 'creation' => time(),  
  73. 'last-access' => time(),  
  74. 'persistent' => false,  
  75. 'host-only' => true,  
  76. ); 
  77. $this->flags = array_merge($default_flags, $flags); 
  78.  
  79. $this->reference_time = time(); 
  80. if ($reference_time !== null) { 
  81. $this->reference_time = $reference_time; 
  82.  
  83. $this->normalize(); 
  84.  
  85. /** 
  86. * Check if a cookie is expired. 
  87. * 
  88. * Checks the age against $this->reference_time to determine if the cookie 
  89. * is expired. 
  90. * 
  91. * @return boolean True if expired, false if time is valid. 
  92. */ 
  93. public function is_expired() { 
  94. // RFC6265, s. 4.1.2.2: 
  95. // If a cookie has both the Max-Age and the Expires attribute, the Max- 
  96. // Age attribute has precedence and controls the expiration date of the 
  97. // cookie. 
  98. if (isset($this->attributes['max-age'])) { 
  99. $max_age = $this->attributes['max-age']; 
  100. return $max_age < $this->reference_time; 
  101.  
  102. if (isset($this->attributes['expires'])) { 
  103. $expires = $this->attributes['expires']; 
  104. return $expires < $this->reference_time; 
  105.  
  106. return false; 
  107.  
  108. /** 
  109. * Check if a cookie is valid for a given URI 
  110. * 
  111. * @param Requests_IRI $uri URI to check 
  112. * @return boolean Whether the cookie is valid for the given URI 
  113. */ 
  114. public function uri_matches(Requests_IRI $uri) { 
  115. if (!$this->domain_matches($uri->host)) { 
  116. return false; 
  117.  
  118. if (!$this->path_matches($uri->path)) { 
  119. return false; 
  120.  
  121. return empty($this->attributes['secure']) || $uri->scheme === 'https'; 
  122.  
  123. /** 
  124. * Check if a cookie is valid for a given domain 
  125. * 
  126. * @param string $string Domain to check 
  127. * @return boolean Whether the cookie is valid for the given domain 
  128. */ 
  129. public function domain_matches($string) { 
  130. if (!isset($this->attributes['domain'])) { 
  131. // Cookies created manually; cookies created by Requests will set 
  132. // the domain to the requested domain 
  133. return true; 
  134.  
  135. $domain_string = $this->attributes['domain']; 
  136. if ($domain_string === $string) { 
  137. // The domain string and the string are identical. 
  138. return true; 
  139.  
  140. // If the cookie is marked as host-only and we don't have an exact 
  141. // match, reject the cookie 
  142. if ($this->flags['host-only'] === true) { 
  143. return false; 
  144.  
  145. if (strlen($string) <= strlen($domain_string)) { 
  146. // For obvious reasons, the string cannot be a suffix if the domain 
  147. // is shorter than the domain string 
  148. return false; 
  149.  
  150. if (substr($string, -1 * strlen($domain_string)) !== $domain_string) { 
  151. // The domain string should be a suffix of the string. 
  152. return false; 
  153.  
  154. $prefix = substr($string, 0, strlen($string) - strlen($domain_string)); 
  155. if (substr($prefix, -1) !== '.') { 
  156. // The last character of the string that is not included in the 
  157. // domain string should be a %x2E (".") character. 
  158. return false; 
  159.  
  160. // The string should be a host name (i.e., not an IP address). 
  161. return !preg_match('#^(.+\.)\d{1, 3}\.\d{1, 3}\.\d{1, 3}\.\d{1, 3}$#', $string); 
  162.  
  163. /** 
  164. * Check if a cookie is valid for a given path 
  165. * 
  166. * From the path-match check in RFC 6265 section 5.1.4 
  167. * 
  168. * @param string $request_path Path to check 
  169. * @return boolean Whether the cookie is valid for the given path 
  170. */ 
  171. public function path_matches($request_path) { 
  172. if (empty($request_path)) { 
  173. // Normalize empty path to root 
  174. $request_path = '/'; 
  175.  
  176. if (!isset($this->attributes['path'])) { 
  177. // Cookies created manually; cookies created by Requests will set 
  178. // the path to the requested path 
  179. return true; 
  180.  
  181. $cookie_path = $this->attributes['path']; 
  182.  
  183. if ($cookie_path === $request_path) { 
  184. // The cookie-path and the request-path are identical. 
  185. return true; 
  186.  
  187. if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) { 
  188. if (substr($cookie_path, -1) === '/') { 
  189. // The cookie-path is a prefix of the request-path, and the last 
  190. // character of the cookie-path is %x2F ("/"). 
  191. return true; 
  192.  
  193. if (substr($request_path, strlen($cookie_path), 1) === '/') { 
  194. // The cookie-path is a prefix of the request-path, and the 
  195. // first character of the request-path that is not included in 
  196. // the cookie-path is a %x2F ("/") character. 
  197. return true; 
  198.  
  199. return false; 
  200.  
  201. /** 
  202. * Normalize cookie and attributes 
  203. * 
  204. * @return boolean Whether the cookie was successfully normalized 
  205. */ 
  206. public function normalize() { 
  207. foreach ($this->attributes as $key => $value) { 
  208. $orig_value = $value; 
  209. $value = $this->normalize_attribute($key, $value); 
  210. if ($value === null) { 
  211. unset($this->attributes[$key]); 
  212. continue; 
  213.  
  214. if ($value !== $orig_value) { 
  215. $this->attributes[$key] = $value; 
  216.  
  217. return true; 
  218.  
  219. /** 
  220. * Parse an individual cookie attribute 
  221. * 
  222. * Handles parsing individual attributes from the cookie values. 
  223. * 
  224. * @param string $name Attribute name 
  225. * @param string|boolean $value Attribute value (string value, or true if empty/flag) 
  226. * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped) 
  227. */ 
  228. protected function normalize_attribute($name, $value) { 
  229. switch (strtolower($name)) { 
  230. case 'expires': 
  231. // Expiration parsing, as per RFC 6265 section 5.2.1 
  232. if (is_int($value)) { 
  233. return $value; 
  234.  
  235. $expiry_time = strtotime($value); 
  236. if ($expiry_time === false) { 
  237. return null; 
  238.  
  239. return $expiry_time; 
  240.  
  241. case 'max-age': 
  242. // Expiration parsing, as per RFC 6265 section 5.2.2 
  243. if (is_int($value)) { 
  244. return $value; 
  245.  
  246. // Check that we have a valid age 
  247. if (!preg_match('/^-?\d+$/', $value)) { 
  248. return null; 
  249.  
  250. $delta_seconds = (int) $value; 
  251. if ($delta_seconds <= 0) { 
  252. $expiry_time = 0; 
  253. else { 
  254. $expiry_time = $this->reference_time + $delta_seconds; 
  255.  
  256. return $expiry_time; 
  257.  
  258. case 'domain': 
  259. // Domain normalization, as per RFC 6265 section 5.2.3 
  260. if ($value[0] === '.') { 
  261. $value = substr($value, 1); 
  262.  
  263. return $value; 
  264.  
  265. default: 
  266. return $value; 
  267.  
  268. /** 
  269. * Format a cookie for a Cookie header 
  270. * 
  271. * This is used when sending cookies to a server. 
  272. * 
  273. * @return string Cookie formatted for Cookie header 
  274. */ 
  275. public function format_for_header() { 
  276. return sprintf('%s=%s', $this->name, $this->value); 
  277.  
  278. /** 
  279. * Format a cookie for a Cookie header 
  280. * 
  281. * @codeCoverageIgnore 
  282. * @deprecated Use {@see Requests_Cookie::format_for_header} 
  283. * @return string 
  284. */ 
  285. public function formatForHeader() { 
  286. return $this->format_for_header(); 
  287.  
  288. /** 
  289. * Format a cookie for a Set-Cookie header 
  290. * 
  291. * This is used when sending cookies to clients. This isn't really 
  292. * applicable to client-side usage, but might be handy for debugging. 
  293. * 
  294. * @return string Cookie formatted for Set-Cookie header 
  295. */ 
  296. public function format_for_set_cookie() { 
  297. $header_value = $this->format_for_header(); 
  298. if (!empty($this->attributes)) { 
  299. $parts = array(); 
  300. foreach ($this->attributes as $key => $value) { 
  301. // Ignore non-associative attributes 
  302. if (is_numeric($key)) { 
  303. $parts[] = $value; 
  304. else { 
  305. $parts[] = sprintf('%s=%s', $key, $value); 
  306.  
  307. $header_value .= '; ' . implode('; ', $parts); 
  308. return $header_value; 
  309.  
  310. /** 
  311. * Format a cookie for a Set-Cookie header 
  312. * 
  313. * @codeCoverageIgnore 
  314. * @deprecated Use {@see Requests_Cookie::format_for_set_cookie} 
  315. * @return string 
  316. */ 
  317. public function formatForSetCookie() { 
  318. return $this->format_for_set_cookie(); 
  319.  
  320. /** 
  321. * Get the cookie value 
  322. * 
  323. * Attributes and other data can be accessed via methods. 
  324. */ 
  325. public function __toString() { 
  326. return $this->value; 
  327.  
  328. /** 
  329. * Parse a cookie string into a cookie object 
  330. * 
  331. * Based on Mozilla's parsing code in Firefox and related projects, which 
  332. * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265 
  333. * specifies some of this handling, but not in a thorough manner. 
  334. * 
  335. * @param string Cookie header value (from a Set-Cookie header) 
  336. * @return Requests_Cookie Parsed cookie object 
  337. */ 
  338. public static function parse($string, $name = '', $reference_time = null) { 
  339. $parts = explode(';', $string); 
  340. $kvparts = array_shift($parts); 
  341.  
  342. if (!empty($name)) { 
  343. $value = $string; 
  344. elseif (strpos($kvparts, '=') === false) { 
  345. // Some sites might only have a value without the equals separator. 
  346. // Deviate from RFC 6265 and pretend it was actually a blank name 
  347. // (`=foo`) 
  348. // 
  349. // https://bugzilla.mozilla.org/show_bug.cgi?id=169091 
  350. $name = ''; 
  351. $value = $kvparts; 
  352. else { 
  353. list($name, $value) = explode('=', $kvparts, 2); 
  354. $name = trim($name); 
  355. $value = trim($value); 
  356.  
  357. // Attribute key are handled case-insensitively 
  358. $attributes = new Requests_Utility_CaseInsensitiveDictionary(); 
  359.  
  360. if (!empty($parts)) { 
  361. foreach ($parts as $part) { 
  362. if (strpos($part, '=') === false) { 
  363. $part_key = $part; 
  364. $part_value = true; 
  365. else { 
  366. list($part_key, $part_value) = explode('=', $part, 2); 
  367. $part_value = trim($part_value); 
  368.  
  369. $part_key = trim($part_key); 
  370. $attributes[$part_key] = $part_value; 
  371.  
  372. return new Requests_Cookie($name, $value, $attributes, array(), $reference_time); 
  373.  
  374. /** 
  375. * Parse all Set-Cookie headers from request headers 
  376. * 
  377. * @param Requests_Response_Headers $headers Headers to parse from 
  378. * @param Requests_IRI|null $origin URI for comparing cookie origins 
  379. * @param int|null $time Reference time for expiration calculation 
  380. * @return array 
  381. */ 
  382. public static function parse_from_headers(Requests_Response_Headers $headers, Requests_IRI $origin = null, $time = null) { 
  383. $cookie_headers = $headers->getValues('Set-Cookie'); 
  384. if (empty($cookie_headers)) { 
  385. return array(); 
  386.  
  387. $cookies = array(); 
  388. foreach ($cookie_headers as $header) { 
  389. $parsed = self::parse($header, '', $time); 
  390.  
  391. // Default domain/path attributes 
  392. if (empty($parsed->attributes['domain']) && !empty($origin)) { 
  393. $parsed->attributes['domain'] = $origin->host; 
  394. $parsed->flags['host-only'] = true; 
  395. else { 
  396. $parsed->flags['host-only'] = false; 
  397.  
  398. $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/'); 
  399. if (!$path_is_valid && !empty($origin)) { 
  400. $path = $origin->path; 
  401.  
  402. // Default path normalization as per RFC 6265 section 5.1.4 
  403. if (substr($path, 0, 1) !== '/') { 
  404. // If the uri-path is empty or if the first character of 
  405. // the uri-path is not a %x2F ("/") character, output 
  406. // %x2F ("/") and skip the remaining steps. 
  407. $path = '/'; 
  408. elseif (substr_count($path, '/') === 1) { 
  409. // If the uri-path contains no more than one %x2F ("/") 
  410. // character, output %x2F ("/") and skip the remaining 
  411. // step. 
  412. $path = '/'; 
  413. else { 
  414. // Output the characters of the uri-path from the first 
  415. // character up to, but not including, the right-most 
  416. // %x2F ("/"). 
  417. $path = substr($path, 0, strrpos($path, '/')); 
  418. $parsed->attributes['path'] = $path; 
  419.  
  420. // Reject invalid cookie domains 
  421. if (!empty($origin) && !$parsed->domain_matches($origin->host)) { 
  422. continue; 
  423.  
  424. $cookies[$parsed->name] = $parsed; 
  425.  
  426. return $cookies; 
  427.  
  428. /** 
  429. * Parse all Set-Cookie headers from request headers 
  430. * 
  431. * @codeCoverageIgnore 
  432. * @deprecated Use {@see Requests_Cookie::parse_from_headers} 
  433. * @return string 
  434. */ 
  435. public static function parseFromHeaders(Requests_Response_Headers $headers) { 
  436. return self::parse_from_headers($headers); 
.