3 * Cookie storage object
10 * Cookie storage object
15 class Requests_Cookie {
30 * Valid keys are (currently) path, domain, expires, max-age, secure and
35 public $attributes = array();
40 * Valid keys are (currently) creation, last-access, persistent and
45 public $flags = array();
48 * Create a new cookie object
51 * @param string $value
52 * @param array $attributes Associative array of attribute data
54 public function __construct($name, $value, $attributes = array(), $flags = array()) {
56 $this->value = $value;
57 $this->attributes = $attributes;
58 $default_flags = array(
60 'last-access' => time(),
61 'persistent' => false,
64 $this->flags = array_merge($default_flags, $flags);
70 * Check if a cookie is valid for a given URI
72 * @param Requests_IRI $uri URI to check
73 * @return boolean Whether the cookie is valid for the given URI
75 public function uriMatches(Requests_IRI $uri) {
76 if (!$this->domainMatches($uri->host)) {
80 if (!$this->pathMatches($uri->path)) {
84 if (!empty($this->attributes['secure']) && $uri->scheme !== 'https') {
92 * Check if a cookie is valid for a given domain
94 * @param string $string Domain to check
95 * @return boolean Whether the cookie is valid for the given domain
97 public function domainMatches($string) {
98 if (!isset($this->attributes['domain'])) {
99 // Cookies created manually; cookies created by Requests will set
100 // the domain to the requested domain
104 $domain_string = $this->attributes['domain'];
105 if ($domain_string === $string) {
106 // The domain string and the string are identical.
110 // If the cookie is marked as host-only and we don't have an exact
111 // match, reject the cookie
112 if ($this->flags['host-only'] === true) {
116 if (strlen($string) <= strlen($domain_string)) {
117 // For obvious reasons, the string cannot be a suffix if the domain
118 // is shorter than the domain string
122 if (substr($string, -1 * strlen($domain_string)) !== $domain_string) {
123 // The domain string should be a suffix of the string.
127 $prefix = substr($string, 0, strlen($string) - strlen($domain_string));
128 if (substr($prefix, -1) !== '.') {
129 // The last character of the string that is not included in the
130 // domain string should be a %x2E (".") character.
134 if (preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string)) {
135 // The string should be a host name (i.e., not an IP address).
143 * Check if a cookie is valid for a given path
145 * From the path-match check in RFC 6265 section 5.1.4
147 * @param string $request_path Path to check
148 * @return boolean Whether the cookie is valid for the given path
150 public function pathMatches($request_path) {
151 if (empty($request_path)) {
152 // Normalize empty path to root
156 if (!isset($this->attributes['path'])) {
157 // Cookies created manually; cookies created by Requests will set
158 // the path to the requested path
162 $cookie_path = $this->attributes['path'];
164 if ($cookie_path === $request_path) {
165 // The cookie-path and the request-path are identical.
169 if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) {
170 if (substr($cookie_path, -1) === '/') {
171 // The cookie-path is a prefix of the request-path, and the last
172 // character of the cookie-path is %x2F ("/").
176 if (substr($request_path, strlen($cookie_path), 1) === '/') {
177 // The cookie-path is a prefix of the request-path, and the
178 // first character of the request-path that is not included in
179 // the cookie-path is a %x2F ("/") character.
188 * Normalize cookie and attributes
190 * @return boolean Whether the cookie was successfully normalized
192 public function normalize() {
193 foreach ($this->attributes as $key => $value) {
194 $orig_value = $value;
197 // Domain normalization, as per RFC 6265 section 5.2.3
198 if ($value[0] === '.') {
199 $value = substr($value, 1);
204 if ($value !== $orig_value) {
205 $this->attributes[$key] = $value;
213 * Format a cookie for a Cookie header
215 * This is used when sending cookies to a server.
217 * @return string Cookie formatted for Cookie header
219 public function formatForHeader() {
220 return sprintf('%s=%s', $this->name, $this->value);
224 * Format a cookie for a Set-Cookie header
226 * This is used when sending cookies to clients. This isn't really
227 * applicable to client-side usage, but might be handy for debugging.
229 * @return string Cookie formatted for Set-Cookie header
231 public function formatForSetCookie() {
232 $header_value = $this->formatForHeader();
233 if (!empty($this->attributes)) {
235 foreach ($this->attributes as $key => $value) {
236 // Ignore non-associative attributes
237 if (is_numeric($key)) {
241 $parts[] = sprintf('%s=%s', $key, $value);
245 $header_value .= '; ' . implode('; ', $parts);
247 return $header_value;
251 * Get the cookie value
253 * Attributes and other data can be accessed via methods.
255 public function __toString() {
260 * Parse a cookie string into a cookie object
262 * Based on Mozilla's parsing code in Firefox and related projects, which
263 * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265
264 * specifies some of this handling, but not in a thorough manner.
266 * @param string Cookie header value (from a Set-Cookie header)
267 * @return Requests_Cookie Parsed cookie object
269 public static function parse($string, $name = '') {
270 $parts = explode(';', $string);
271 $kvparts = array_shift($parts);
276 elseif (strpos($kvparts, '=') === false) {
277 // Some sites might only have a value without the equals separator.
278 // Deviate from RFC 6265 and pretend it was actually a blank name
281 // https://bugzilla.mozilla.org/show_bug.cgi?id=169091
286 list($name, $value) = explode('=', $kvparts, 2);
289 $value = trim($value);
291 // Attribute key are handled case-insensitively
292 $attributes = new Requests_Utility_CaseInsensitiveDictionary();
294 if (!empty($parts)) {
295 foreach ($parts as $part) {
296 if (strpos($part, '=') === false) {
301 list($part_key, $part_value) = explode('=', $part, 2);
302 $part_value = trim($part_value);
305 $part_key = trim($part_key);
306 $attributes[$part_key] = $part_value;
310 return new Requests_Cookie($name, $value, $attributes);
314 * Parse all Set-Cookie headers from request headers
316 * @param Requests_Response_Headers $headers
319 public static function parseFromHeaders(Requests_Response_Headers $headers, Requests_IRI $origin = null) {
320 $cookie_headers = $headers->getValues('Set-Cookie');
321 if (empty($cookie_headers)) {
326 foreach ($cookie_headers as $header) {
327 $parsed = self::parse($header);
329 // Default domain/path attributes
330 if (empty($parsed->attributes['domain']) && !empty($origin)) {
331 $parsed->attributes['domain'] = $origin->host;
332 $parsed->flags['host-only'] = false;
335 $parsed->flags['host-only'] = true;
338 $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/');
339 if (!$path_is_valid && !empty($origin)) {
340 $path = $origin->path;
342 // Default path normalization as per RFC 6265 section 5.1.4
343 if (substr($path, 0, 1) !== '/') {
344 // If the uri-path is empty or if the first character of
345 // the uri-path is not a %x2F ("/") character, output
346 // %x2F ("/") and skip the remaining steps.
349 elseif (substr_count($path, '/') === 1) {
350 // If the uri-path contains no more than one %x2F ("/")
351 // character, output %x2F ("/") and skip the remaining
356 // Output the characters of the uri-path from the first
357 // character up to, but not including, the right-most
359 $path = substr($path, 0, strrpos($path, '/'));
361 $parsed->attributes['path'] = $path;
364 // Reject invalid cookie domains
365 if (!$parsed->domainMatches($origin->host)) {
369 $cookies[$parsed->name] = $parsed;