]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/Requests/Requests/Cookie.php
Sync with current version of Requests
[Github/YOURLS.git] / includes / Requests / 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          * 
18          * @var string
19          */
20         public $name;
21
22         /**
23          * @var string
24          */
25         public $value;
26
27         /**
28          * Cookie attributes
29          * 
30          * Valid keys are (currently) path, domain, expires, max-age, secure and
31          * httponly.
32          *
33          * @var array
34          */
35         public $attributes = array();
36
37         /**
38          * Cookie flags
39          *
40          * Valid keys are (currently) creation, last-access, persistent and
41          * host-only.
42          *
43          * @var array
44          */
45         public $flags = array();
46
47         /**
48          * Create a new cookie object
49          *
50          * @param string $name
51          * @param string $value
52          * @param array $attributes Associative array of attribute data
53          */
54         public function __construct($name, $value, $attributes = array(), $flags = array()) {
55                 $this->name = $name;
56                 $this->value = $value;
57                 $this->attributes = $attributes;
58                 $default_flags = array(
59                         'creation' => time(),
60                         'last-access' => time(),
61                         'persistent' => false,
62                         'host-only' => true,
63                 );
64                 $this->flags = array_merge($default_flags, $flags);
65
66                 $this->normalize();
67         }
68
69         /**
70          * Check if a cookie is valid for a given URI
71          *
72          * @param Requests_IRI $uri URI to check
73          * @return boolean Whether the cookie is valid for the given URI
74          */
75         public function uriMatches(Requests_IRI $uri) {
76                 if (!$this->domainMatches($uri->host)) {
77                         return false;
78                 }
79
80                 if (!$this->pathMatches($uri->path)) {
81                         return false;
82                 }
83
84                 if (!empty($this->attributes['secure']) && $uri->scheme !== 'https') {
85                         return false;
86                 }
87
88                 return true;
89         }
90
91         /**
92          * Check if a cookie is valid for a given domain
93          *
94          * @param string $string Domain to check
95          * @return boolean Whether the cookie is valid for the given domain
96          */
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
101                         return true;
102                 }
103
104                 $domain_string = $this->attributes['domain'];
105                 if ($domain_string === $string) {
106                         // The domain string and the string are identical.
107                         return true;
108                 }
109
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) {
113                         return false;
114                 }
115
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
119                         return false;
120                 }
121
122                 if (substr($string, -1 * strlen($domain_string)) !== $domain_string) {
123                         // The domain string should be a suffix of the string.
124                         return false;
125                 }
126
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.
131                         return false;
132                 }
133
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).
136                         return false;
137                 }
138
139                 return true;
140         }
141
142         /**
143          * Check if a cookie is valid for a given path
144          *
145          * From the path-match check in RFC 6265 section 5.1.4
146          *
147          * @param string $request_path Path to check
148          * @return boolean Whether the cookie is valid for the given path
149          */
150         public function pathMatches($request_path) {
151                 if (empty($request_path)) {
152                         // Normalize empty path to root
153                         $request_path = '/';
154                 }
155
156                 if (!isset($this->attributes['path'])) {
157                         // Cookies created manually; cookies created by Requests will set
158                         // the path to the requested path
159                         return true;
160                 }
161
162                 $cookie_path = $this->attributes['path'];
163
164                 if ($cookie_path === $request_path) {
165                         // The cookie-path and the request-path are identical.
166                         return true;
167                 }
168
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 ("/").
173                                 return true;
174                         }
175
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.
180                                 return true;
181                         }
182                 }
183
184                 return false;
185         }
186
187         /**
188          * Normalize cookie and attributes
189          *
190          * @return boolean Whether the cookie was successfully normalized
191          */
192         public function normalize() {
193                 foreach ($this->attributes as $key => $value) {
194                         $orig_value = $value;
195                         switch ($key) {
196                                 case 'domain':
197                                         // Domain normalization, as per RFC 6265 section 5.2.3
198                                         if ($value[0] === '.') {
199                                                 $value = substr($value, 1);
200                                         }
201                                         break;
202                         }
203
204                         if ($value !== $orig_value) {
205                                 $this->attributes[$key] = $value;
206                         }
207                 }
208
209                 return true;
210         }
211
212         /**
213          * Format a cookie for a Cookie header
214          *
215          * This is used when sending cookies to a server.
216          *
217          * @return string Cookie formatted for Cookie header
218          */
219         public function formatForHeader() {
220                 return sprintf('%s=%s', $this->name, $this->value);
221         }
222
223         /**
224          * Format a cookie for a Set-Cookie header
225          *
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.
228          *
229          * @return string Cookie formatted for Set-Cookie header
230          */
231         public function formatForSetCookie() {
232                 $header_value = $this->formatForHeader();
233                 if (!empty($this->attributes)) {
234                         $parts = array();
235                         foreach ($this->attributes as $key => $value) {
236                                 // Ignore non-associative attributes
237                                 if (is_numeric($key)) {
238                                         $parts[] = $value;
239                                 }
240                                 else {
241                                         $parts[] = sprintf('%s=%s', $key, $value);
242                                 }
243                         }
244
245                         $header_value .= '; ' . implode('; ', $parts);
246                 }
247                 return $header_value;
248         }
249
250         /**
251          * Get the cookie value
252          *
253          * Attributes and other data can be accessed via methods.
254          */
255         public function __toString() {
256                 return $this->value;
257         }
258
259         /**
260          * Parse a cookie string into a cookie object
261          *
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.
265          *
266          * @param string Cookie header value (from a Set-Cookie header)
267          * @return Requests_Cookie Parsed cookie object
268          */
269         public static function parse($string, $name = '') {
270                 $parts = explode(';', $string);
271                 $kvparts = array_shift($parts);
272
273                 if (!empty($name)) {
274                         $value = $string;
275                 }
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
279                         // (`=foo`)
280                         //
281                         // https://bugzilla.mozilla.org/show_bug.cgi?id=169091
282                         $name = '';
283                         $value = $kvparts;
284                 }
285                 else {
286                         list($name, $value) = explode('=', $kvparts, 2);
287                 }
288                 $name = trim($name);
289                 $value = trim($value);
290
291                 // Attribute key are handled case-insensitively
292                 $attributes = new Requests_Utility_CaseInsensitiveDictionary();
293
294                 if (!empty($parts)) {
295                         foreach ($parts as $part) {
296                                 if (strpos($part, '=') === false) {
297                                         $part_key = $part;
298                                         $part_value = true;
299                                 }
300                                 else {
301                                         list($part_key, $part_value) = explode('=', $part, 2);
302                                         $part_value = trim($part_value);
303                                 }
304
305                                 $part_key = trim($part_key);
306                                 $attributes[$part_key] = $part_value;
307                         }
308                 }
309
310                 return new Requests_Cookie($name, $value, $attributes);
311         }
312
313         /**
314          * Parse all Set-Cookie headers from request headers
315          *
316          * @param Requests_Response_Headers $headers
317          * @return array
318          */
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)) {
322                         return array();
323                 }
324
325                 $cookies = array();
326                 foreach ($cookie_headers as $header) {
327                         $parsed = self::parse($header);
328
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;
333                         }
334                         else {
335                                 $parsed->flags['host-only'] = true;
336                         }
337
338                         $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/');
339                         if (!$path_is_valid && !empty($origin)) {
340                                 $path = $origin->path;
341
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.
347                                         $path = '/';
348                                 }
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
352                                         // step.
353                                         $path = '/';
354                                 }
355                                 else {
356                                         // Output the characters of the uri-path from the first
357                                         // character up to, but not including, the right-most
358                                         // %x2F ("/").
359                                         $path = substr($path, 0, strrpos($path, '/'));
360                                 }
361                                 $parsed->attributes['path'] = $path;
362                         }
363
364                         // Reject invalid cookie domains
365                         if (!$parsed->domainMatches($origin->host)) {
366                                 continue;
367                         }
368
369                         $cookies[$parsed->name] = $parsed;
370                 }
371
372                 return $cookies;
373         }
374 }