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