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