ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

Cookie.php (13033B)


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