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