]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/HTTP/Request2/CookieJar.php
PEAR::HTTP_Request2 updated to 2.2.1
[quix0rs-gnu-social.git] / extlib / HTTP / Request2 / CookieJar.php
1 <?php\r
2 /**\r
3  * Stores cookies and passes them between HTTP requests\r
4  *\r
5  * PHP version 5\r
6  *\r
7  * LICENSE\r
8  *\r
9  * This source file is subject to BSD 3-Clause License that is bundled\r
10  * with this package in the file LICENSE and available at the URL\r
11  * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE\r
12  *\r
13  * @category  HTTP\r
14  * @package   HTTP_Request2\r
15  * @author    Alexey Borzov <avb@php.net>\r
16  * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
17  * @license   http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
18  * @link      http://pear.php.net/package/HTTP_Request2\r
19  */\r
20 \r
21 /** Class representing a HTTP request message */\r
22 require_once 'HTTP/Request2.php';\r
23 \r
24 /**\r
25  * Stores cookies and passes them between HTTP requests\r
26  *\r
27  * @category HTTP\r
28  * @package  HTTP_Request2\r
29  * @author   Alexey Borzov <avb@php.net>\r
30  * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
31  * @version  Release: @package_version@\r
32  * @link     http://pear.php.net/package/HTTP_Request2\r
33  */\r
34 class HTTP_Request2_CookieJar implements Serializable\r
35 {\r
36     /**\r
37      * Array of stored cookies\r
38      *\r
39      * The array is indexed by domain, path and cookie name\r
40      *   .example.com\r
41      *     /\r
42      *       some_cookie => cookie data\r
43      *     /subdir\r
44      *       other_cookie => cookie data\r
45      *   .example.org\r
46      *     ...\r
47      *\r
48      * @var array\r
49      */\r
50     protected $cookies = array();\r
51 \r
52     /**\r
53      * Whether session cookies should be serialized when serializing the jar\r
54      * @var bool\r
55      */\r
56     protected $serializeSession = false;\r
57 \r
58     /**\r
59      * Whether Public Suffix List should be used for domain matching\r
60      * @var bool\r
61      */\r
62     protected $useList = true;\r
63 \r
64     /**\r
65      * Array with Public Suffix List data\r
66      * @var  array\r
67      * @link http://publicsuffix.org/\r
68      */\r
69     protected static $psl = array();\r
70 \r
71     /**\r
72      * Class constructor, sets various options\r
73      *\r
74      * @param bool $serializeSessionCookies Controls serializing session cookies,\r
75      *                                      see {@link serializeSessionCookies()}\r
76      * @param bool $usePublicSuffixList     Controls using Public Suffix List,\r
77      *                                      see {@link usePublicSuffixList()}\r
78      */\r
79     public function __construct(\r
80         $serializeSessionCookies = false, $usePublicSuffixList = true\r
81     ) {\r
82         $this->serializeSessionCookies($serializeSessionCookies);\r
83         $this->usePublicSuffixList($usePublicSuffixList);\r
84     }\r
85 \r
86     /**\r
87      * Returns current time formatted in ISO-8601 at UTC timezone\r
88      *\r
89      * @return string\r
90      */\r
91     protected function now()\r
92     {\r
93         $dt = new DateTime();\r
94         $dt->setTimezone(new DateTimeZone('UTC'));\r
95         return $dt->format(DateTime::ISO8601);\r
96     }\r
97 \r
98     /**\r
99      * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields\r
100      *\r
101      * The checks are as follows:\r
102      *   - cookie array should contain 'name' and 'value' fields;\r
103      *   - name and value should not contain disallowed symbols;\r
104      *   - 'expires' should be either empty parseable by DateTime;\r
105      *   - 'domain' and 'path' should be either not empty or an URL where\r
106      *     cookie was set should be provided.\r
107      *   - if $setter is provided, then document at that URL should be allowed\r
108      *     to set a cookie for that 'domain'. If $setter is not provided,\r
109      *     then no domain checks will be made.\r
110      *\r
111      * 'expires' field will be converted to ISO8601 format from COOKIE format,\r
112      * 'domain' and 'path' will be set from setter URL if empty.\r
113      *\r
114      * @param array    $cookie cookie data, as returned by\r
115      *                         {@link HTTP_Request2_Response::getCookies()}\r
116      * @param Net_URL2 $setter URL of the document that sent Set-Cookie header\r
117      *\r
118      * @return   array    Updated cookie array\r
119      * @throws   HTTP_Request2_LogicException\r
120      * @throws   HTTP_Request2_MessageException\r
121      */\r
122     protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)\r
123     {\r
124         if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {\r
125             throw new HTTP_Request2_LogicException(\r
126                 "Cookie array should contain 'name' and 'value' fields",\r
127                 HTTP_Request2_Exception::MISSING_VALUE\r
128             );\r
129         }\r
130         if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {\r
131             throw new HTTP_Request2_LogicException(\r
132                 "Invalid cookie name: '{$cookie['name']}'",\r
133                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
134             );\r
135         }\r
136         if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {\r
137             throw new HTTP_Request2_LogicException(\r
138                 "Invalid cookie value: '{$cookie['value']}'",\r
139                 HTTP_Request2_Exception::INVALID_ARGUMENT\r
140             );\r
141         }\r
142         $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);\r
143 \r
144         // Need ISO-8601 date @ UTC timezone\r
145         if (!empty($cookie['expires'])\r
146             && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])\r
147         ) {\r
148             try {\r
149                 $dt = new DateTime($cookie['expires']);\r
150                 $dt->setTimezone(new DateTimeZone('UTC'));\r
151                 $cookie['expires'] = $dt->format(DateTime::ISO8601);\r
152             } catch (Exception $e) {\r
153                 throw new HTTP_Request2_LogicException($e->getMessage());\r
154             }\r
155         }\r
156 \r
157         if (empty($cookie['domain']) || empty($cookie['path'])) {\r
158             if (!$setter) {\r
159                 throw new HTTP_Request2_LogicException(\r
160                     'Cookie misses domain and/or path component, cookie setter URL needed',\r
161                     HTTP_Request2_Exception::MISSING_VALUE\r
162                 );\r
163             }\r
164             if (empty($cookie['domain'])) {\r
165                 if ($host = $setter->getHost()) {\r
166                     $cookie['domain'] = $host;\r
167                 } else {\r
168                     throw new HTTP_Request2_LogicException(\r
169                         'Setter URL does not contain host part, can\'t set cookie domain',\r
170                         HTTP_Request2_Exception::MISSING_VALUE\r
171                     );\r
172                 }\r
173             }\r
174             if (empty($cookie['path'])) {\r
175                 $path = $setter->getPath();\r
176                 $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);\r
177             }\r
178         }\r
179 \r
180         if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {\r
181             throw new HTTP_Request2_MessageException(\r
182                 "Domain " . $setter->getHost() . " cannot set cookies for "\r
183                 . $cookie['domain']\r
184             );\r
185         }\r
186 \r
187         return $cookie;\r
188     }\r
189 \r
190     /**\r
191      * Stores a cookie in the jar\r
192      *\r
193      * @param array    $cookie cookie data, as returned by\r
194      *                         {@link HTTP_Request2_Response::getCookies()}\r
195      * @param Net_URL2 $setter URL of the document that sent Set-Cookie header\r
196      *\r
197      * @throws   HTTP_Request2_Exception\r
198      */\r
199     public function store(array $cookie, Net_URL2 $setter = null)\r
200     {\r
201         $cookie = $this->checkAndUpdateFields($cookie, $setter);\r
202 \r
203         if (strlen($cookie['value'])\r
204             && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())\r
205         ) {\r
206             if (!isset($this->cookies[$cookie['domain']])) {\r
207                 $this->cookies[$cookie['domain']] = array();\r
208             }\r
209             if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {\r
210                 $this->cookies[$cookie['domain']][$cookie['path']] = array();\r
211             }\r
212             $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;\r
213 \r
214         } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {\r
215             unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);\r
216         }\r
217     }\r
218 \r
219     /**\r
220      * Adds cookies set in HTTP response to the jar\r
221      *\r
222      * @param HTTP_Request2_Response $response HTTP response message\r
223      * @param Net_URL2               $setter   original request URL, needed for\r
224      *                               setting default domain/path\r
225      */\r
226     public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)\r
227     {\r
228         foreach ($response->getCookies() as $cookie) {\r
229             $this->store($cookie, $setter);\r
230         }\r
231     }\r
232 \r
233     /**\r
234      * Returns all cookies matching a given request URL\r
235      *\r
236      * The following checks are made:\r
237      *   - cookie domain should match request host\r
238      *   - cookie path should be a prefix for request path\r
239      *   - 'secure' cookies will only be sent for HTTPS requests\r
240      *\r
241      * @param Net_URL2 $url      Request url\r
242      * @param bool     $asString Whether to return cookies as string for "Cookie: " header\r
243      *\r
244      * @return array|string Matching cookies\r
245      */\r
246     public function getMatching(Net_URL2 $url, $asString = false)\r
247     {\r
248         $host   = $url->getHost();\r
249         $path   = $url->getPath();\r
250         $secure = 0 == strcasecmp($url->getScheme(), 'https');\r
251 \r
252         $matched = $ret = array();\r
253         foreach (array_keys($this->cookies) as $domain) {\r
254             if ($this->domainMatch($host, $domain)) {\r
255                 foreach (array_keys($this->cookies[$domain]) as $cPath) {\r
256                     if (0 === strpos($path, $cPath)) {\r
257                         foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {\r
258                             if (!$cookie['secure'] || $secure) {\r
259                                 $matched[$name][strlen($cookie['path'])] = $cookie;\r
260                             }\r
261                         }\r
262                     }\r
263                 }\r
264             }\r
265         }\r
266         foreach ($matched as $cookies) {\r
267             krsort($cookies);\r
268             $ret = array_merge($ret, $cookies);\r
269         }\r
270         if (!$asString) {\r
271             return $ret;\r
272         } else {\r
273             $str = '';\r
274             foreach ($ret as $c) {\r
275                 $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];\r
276             }\r
277             return $str;\r
278         }\r
279     }\r
280 \r
281     /**\r
282      * Returns all cookies stored in a jar\r
283      *\r
284      * @return array\r
285      */\r
286     public function getAll()\r
287     {\r
288         $cookies = array();\r
289         foreach (array_keys($this->cookies) as $domain) {\r
290             foreach (array_keys($this->cookies[$domain]) as $path) {\r
291                 foreach ($this->cookies[$domain][$path] as $name => $cookie) {\r
292                     $cookies[] = $cookie;\r
293                 }\r
294             }\r
295         }\r
296         return $cookies;\r
297     }\r
298 \r
299     /**\r
300      * Sets whether session cookies should be serialized when serializing the jar\r
301      *\r
302      * @param boolean $serialize serialize?\r
303      */\r
304     public function serializeSessionCookies($serialize)\r
305     {\r
306         $this->serializeSession = (bool)$serialize;\r
307     }\r
308 \r
309     /**\r
310      * Sets whether Public Suffix List should be used for restricting cookie-setting\r
311      *\r
312      * Without PSL {@link domainMatch()} will only prevent setting cookies for\r
313      * top-level domains like '.com' or '.org'. However, it will not prevent\r
314      * setting a cookie for '.co.uk' even though only third-level registrations\r
315      * are possible in .uk domain.\r
316      *\r
317      * With the List it is possible to find the highest level at which a domain\r
318      * may be registered for a particular top-level domain and consequently\r
319      * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by\r
320      * Firefox, Chrome and Opera browsers to restrict cookie setting.\r
321      *\r
322      * Note that PSL is licensed differently to HTTP_Request2 package (refer to\r
323      * the license information in public-suffix-list.php), so you can disable\r
324      * its use if this is an issue for you.\r
325      *\r
326      * @param boolean $useList use the list?\r
327      *\r
328      * @link     http://publicsuffix.org/learn/\r
329      */\r
330     public function usePublicSuffixList($useList)\r
331     {\r
332         $this->useList = (bool)$useList;\r
333     }\r
334 \r
335     /**\r
336      * Returns string representation of object\r
337      *\r
338      * @return string\r
339      *\r
340      * @see    Serializable::serialize()\r
341      */\r
342     public function serialize()\r
343     {\r
344         $cookies = $this->getAll();\r
345         if (!$this->serializeSession) {\r
346             for ($i = count($cookies) - 1; $i >= 0; $i--) {\r
347                 if (empty($cookies[$i]['expires'])) {\r
348                     unset($cookies[$i]);\r
349                 }\r
350             }\r
351         }\r
352         return serialize(array(\r
353             'cookies'          => $cookies,\r
354             'serializeSession' => $this->serializeSession,\r
355             'useList'          => $this->useList\r
356         ));\r
357     }\r
358 \r
359     /**\r
360      * Constructs the object from serialized string\r
361      *\r
362      * @param string $serialized string representation\r
363      *\r
364      * @see   Serializable::unserialize()\r
365      */\r
366     public function unserialize($serialized)\r
367     {\r
368         $data = unserialize($serialized);\r
369         $now  = $this->now();\r
370         $this->serializeSessionCookies($data['serializeSession']);\r
371         $this->usePublicSuffixList($data['useList']);\r
372         foreach ($data['cookies'] as $cookie) {\r
373             if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {\r
374                 continue;\r
375             }\r
376             if (!isset($this->cookies[$cookie['domain']])) {\r
377                 $this->cookies[$cookie['domain']] = array();\r
378             }\r
379             if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {\r
380                 $this->cookies[$cookie['domain']][$cookie['path']] = array();\r
381             }\r
382             $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;\r
383         }\r
384     }\r
385 \r
386     /**\r
387      * Checks whether a cookie domain matches a request host.\r
388      *\r
389      * The method is used by {@link store()} to check for whether a document\r
390      * at given URL can set a cookie with a given domain attribute and by\r
391      * {@link getMatching()} to find cookies matching the request URL.\r
392      *\r
393      * @param string $requestHost  request host\r
394      * @param string $cookieDomain cookie domain\r
395      *\r
396      * @return   bool    match success\r
397      */\r
398     public function domainMatch($requestHost, $cookieDomain)\r
399     {\r
400         if ($requestHost == $cookieDomain) {\r
401             return true;\r
402         }\r
403         // IP address, we require exact match\r
404         if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {\r
405             return false;\r
406         }\r
407         if ('.' != $cookieDomain[0]) {\r
408             $cookieDomain = '.' . $cookieDomain;\r
409         }\r
410         // prevents setting cookies for '.com' and similar domains\r
411         if (!$this->useList && substr_count($cookieDomain, '.') < 2\r
412             || $this->useList && !self::getRegisteredDomain($cookieDomain)\r
413         ) {\r
414             return false;\r
415         }\r
416         return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;\r
417     }\r
418 \r
419     /**\r
420      * Removes subdomains to get the registered domain (the first after top-level)\r
421      *\r
422      * The method will check Public Suffix List to find out where top-level\r
423      * domain ends and registered domain starts. It will remove domain parts\r
424      * to the left of registered one.\r
425      *\r
426      * @param string $domain domain name\r
427      *\r
428      * @return string|bool   registered domain, will return false if $domain is\r
429      *                       either invalid or a TLD itself\r
430      */\r
431     public static function getRegisteredDomain($domain)\r
432     {\r
433         $domainParts = explode('.', ltrim($domain, '.'));\r
434 \r
435         // load the list if needed\r
436         if (empty(self::$psl)) {\r
437             $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';\r
438             if (0 === strpos($path, '@' . 'data_dir@')) {\r
439                 $path = realpath(\r
440                     dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'\r
441                     . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'\r
442                 );\r
443             }\r
444             self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';\r
445         }\r
446 \r
447         if (!($result = self::checkDomainsList($domainParts, self::$psl))) {\r
448             // known TLD, invalid domain name\r
449             return false;\r
450         }\r
451 \r
452         // unknown TLD\r
453         if (!strpos($result, '.')) {\r
454             // fallback to checking that domain "has at least two dots"\r
455             if (2 > ($count = count($domainParts))) {\r
456                 return false;\r
457             }\r
458             return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];\r
459         }\r
460         return $result;\r
461     }\r
462 \r
463     /**\r
464      * Recursive helper method for {@link getRegisteredDomain()}\r
465      *\r
466      * @param array $domainParts remaining domain parts\r
467      * @param mixed $listNode    node in {@link HTTP_Request2_CookieJar::$psl} to check\r
468      *\r
469      * @return string|null   concatenated domain parts, null in case of error\r
470      */\r
471     protected static function checkDomainsList(array $domainParts, $listNode)\r
472     {\r
473         $sub    = array_pop($domainParts);\r
474         $result = null;\r
475 \r
476         if (!is_array($listNode) || is_null($sub)\r
477             || array_key_exists('!' . $sub, $listNode)\r
478         ) {\r
479             return $sub;\r
480 \r
481         } elseif (array_key_exists($sub, $listNode)) {\r
482             $result = self::checkDomainsList($domainParts, $listNode[$sub]);\r
483 \r
484         } elseif (array_key_exists('*', $listNode)) {\r
485             $result = self::checkDomainsList($domainParts, $listNode['*']);\r
486 \r
487         } else {\r
488             return $sub;\r
489         }\r
490 \r
491         return (strlen($result) > 0) ? ($result . '.' . $sub) : null;\r
492     }\r
493 }\r
494 ?>