source: branches/version-2_13_0/data/module/HTTP/Request2/CookieJar.php @ 23125

Revision 23125, 19.0 KB checked in by kimoto, 11 years ago (diff)

#2275 PEAR更新
不要なrequire_onceの削除
レガシーなPEARモジュールは使わない
SearchReplace?.phpのパスが間違っているので修正

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