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