494 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			494 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * Stores cookies and passes them between HTTP requests
 | 
						|
 *
 | 
						|
 * PHP version 5
 | 
						|
 *
 | 
						|
 * LICENSE
 | 
						|
 *
 | 
						|
 * This source file is subject to BSD 3-Clause License that is bundled
 | 
						|
 * with this package in the file LICENSE and available at the URL
 | 
						|
 * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
 | 
						|
 *
 | 
						|
 * @category  HTTP
 | 
						|
 * @package   HTTP_Request2
 | 
						|
 * @author    Alexey Borzov <avb@php.net>
 | 
						|
 * @copyright 2008-2014 Alexey Borzov <avb@php.net>
 | 
						|
 * @license   http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
 | 
						|
 * @link      http://pear.php.net/package/HTTP_Request2
 | 
						|
 */
 | 
						|
 | 
						|
/** Class representing a HTTP request message */
 | 
						|
require_once 'HTTP/Request2.php';
 | 
						|
 | 
						|
/**
 | 
						|
 * Stores cookies and passes them between HTTP requests
 | 
						|
 *
 | 
						|
 * @category HTTP
 | 
						|
 * @package  HTTP_Request2
 | 
						|
 * @author   Alexey Borzov <avb@php.net>
 | 
						|
 * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
 | 
						|
 * @version  Release: @package_version@
 | 
						|
 * @link     http://pear.php.net/package/HTTP_Request2
 | 
						|
 */
 | 
						|
class HTTP_Request2_CookieJar implements Serializable
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * Array of stored cookies
 | 
						|
     *
 | 
						|
     * The array is indexed by domain, path and cookie name
 | 
						|
     *   .example.com
 | 
						|
     *     /
 | 
						|
     *       some_cookie => cookie data
 | 
						|
     *     /subdir
 | 
						|
     *       other_cookie => cookie data
 | 
						|
     *   .example.org
 | 
						|
     *     ...
 | 
						|
     *
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    protected $cookies = array();
 | 
						|
 | 
						|
    /**
 | 
						|
     * Whether session cookies should be serialized when serializing the jar
 | 
						|
     * @var bool
 | 
						|
     */
 | 
						|
    protected $serializeSession = false;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Whether Public Suffix List should be used for domain matching
 | 
						|
     * @var bool
 | 
						|
     */
 | 
						|
    protected $useList = true;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Array with Public Suffix List data
 | 
						|
     * @var  array
 | 
						|
     * @link http://publicsuffix.org/
 | 
						|
     */
 | 
						|
    protected static $psl = array();
 | 
						|
 | 
						|
    /**
 | 
						|
     * Class constructor, sets various options
 | 
						|
     *
 | 
						|
     * @param bool $serializeSessionCookies Controls serializing session cookies,
 | 
						|
     *                                      see {@link serializeSessionCookies()}
 | 
						|
     * @param bool $usePublicSuffixList     Controls using Public Suffix List,
 | 
						|
     *                                      see {@link usePublicSuffixList()}
 | 
						|
     */
 | 
						|
    public function __construct(
 | 
						|
        $serializeSessionCookies = false, $usePublicSuffixList = true
 | 
						|
    ) {
 | 
						|
        $this->serializeSessionCookies($serializeSessionCookies);
 | 
						|
        $this->usePublicSuffixList($usePublicSuffixList);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns current time formatted in ISO-8601 at UTC timezone
 | 
						|
     *
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    protected function now()
 | 
						|
    {
 | 
						|
        $dt = new DateTime();
 | 
						|
        $dt->setTimezone(new DateTimeZone('UTC'));
 | 
						|
        return $dt->format(DateTime::ISO8601);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
 | 
						|
     *
 | 
						|
     * The checks are as follows:
 | 
						|
     *   - cookie array should contain 'name' and 'value' fields;
 | 
						|
     *   - name and value should not contain disallowed symbols;
 | 
						|
     *   - 'expires' should be either empty parseable by DateTime;
 | 
						|
     *   - 'domain' and 'path' should be either not empty or an URL where
 | 
						|
     *     cookie was set should be provided.
 | 
						|
     *   - if $setter is provided, then document at that URL should be allowed
 | 
						|
     *     to set a cookie for that 'domain'. If $setter is not provided,
 | 
						|
     *     then no domain checks will be made.
 | 
						|
     *
 | 
						|
     * 'expires' field will be converted to ISO8601 format from COOKIE format,
 | 
						|
     * 'domain' and 'path' will be set from setter URL if empty.
 | 
						|
     *
 | 
						|
     * @param array    $cookie cookie data, as returned by
 | 
						|
     *                         {@link HTTP_Request2_Response::getCookies()}
 | 
						|
     * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
 | 
						|
     *
 | 
						|
     * @return   array    Updated cookie array
 | 
						|
     * @throws   HTTP_Request2_LogicException
 | 
						|
     * @throws   HTTP_Request2_MessageException
 | 
						|
     */
 | 
						|
    protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
 | 
						|
    {
 | 
						|
        if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
 | 
						|
            throw new HTTP_Request2_LogicException(
 | 
						|
                "Cookie array should contain 'name' and 'value' fields",
 | 
						|
                HTTP_Request2_Exception::MISSING_VALUE
 | 
						|
            );
 | 
						|
        }
 | 
						|
        if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
 | 
						|
            throw new HTTP_Request2_LogicException(
 | 
						|
                "Invalid cookie name: '{$cookie['name']}'",
 | 
						|
                HTTP_Request2_Exception::INVALID_ARGUMENT
 | 
						|
            );
 | 
						|
        }
 | 
						|
        if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
 | 
						|
            throw new HTTP_Request2_LogicException(
 | 
						|
                "Invalid cookie value: '{$cookie['value']}'",
 | 
						|
                HTTP_Request2_Exception::INVALID_ARGUMENT
 | 
						|
            );
 | 
						|
        }
 | 
						|
        $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
 | 
						|
 | 
						|
        // Need ISO-8601 date @ UTC timezone
 | 
						|
        if (!empty($cookie['expires'])
 | 
						|
            && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
 | 
						|
        ) {
 | 
						|
            try {
 | 
						|
                $dt = new DateTime($cookie['expires']);
 | 
						|
                $dt->setTimezone(new DateTimeZone('UTC'));
 | 
						|
                $cookie['expires'] = $dt->format(DateTime::ISO8601);
 | 
						|
            } catch (Exception $e) {
 | 
						|
                throw new HTTP_Request2_LogicException($e->getMessage());
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (empty($cookie['domain']) || empty($cookie['path'])) {
 | 
						|
            if (!$setter) {
 | 
						|
                throw new HTTP_Request2_LogicException(
 | 
						|
                    'Cookie misses domain and/or path component, cookie setter URL needed',
 | 
						|
                    HTTP_Request2_Exception::MISSING_VALUE
 | 
						|
                );
 | 
						|
            }
 | 
						|
            if (empty($cookie['domain'])) {
 | 
						|
                if ($host = $setter->getHost()) {
 | 
						|
                    $cookie['domain'] = $host;
 | 
						|
                } else {
 | 
						|
                    throw new HTTP_Request2_LogicException(
 | 
						|
                        'Setter URL does not contain host part, can\'t set cookie domain',
 | 
						|
                        HTTP_Request2_Exception::MISSING_VALUE
 | 
						|
                    );
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (empty($cookie['path'])) {
 | 
						|
                $path = $setter->getPath();
 | 
						|
                $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
 | 
						|
            throw new HTTP_Request2_MessageException(
 | 
						|
                "Domain " . $setter->getHost() . " cannot set cookies for "
 | 
						|
                . $cookie['domain']
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        return $cookie;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Stores a cookie in the jar
 | 
						|
     *
 | 
						|
     * @param array    $cookie cookie data, as returned by
 | 
						|
     *                         {@link HTTP_Request2_Response::getCookies()}
 | 
						|
     * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
 | 
						|
     *
 | 
						|
     * @throws   HTTP_Request2_Exception
 | 
						|
     */
 | 
						|
    public function store(array $cookie, Net_URL2 $setter = null)
 | 
						|
    {
 | 
						|
        $cookie = $this->checkAndUpdateFields($cookie, $setter);
 | 
						|
 | 
						|
        if (strlen($cookie['value'])
 | 
						|
            && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
 | 
						|
        ) {
 | 
						|
            if (!isset($this->cookies[$cookie['domain']])) {
 | 
						|
                $this->cookies[$cookie['domain']] = array();
 | 
						|
            }
 | 
						|
            if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
 | 
						|
                $this->cookies[$cookie['domain']][$cookie['path']] = array();
 | 
						|
            }
 | 
						|
            $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
 | 
						|
 | 
						|
        } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
 | 
						|
            unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds cookies set in HTTP response to the jar
 | 
						|
     *
 | 
						|
     * @param HTTP_Request2_Response $response HTTP response message
 | 
						|
     * @param Net_URL2               $setter   original request URL, needed for
 | 
						|
     *                               setting default domain/path
 | 
						|
     */
 | 
						|
    public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
 | 
						|
    {
 | 
						|
        foreach ($response->getCookies() as $cookie) {
 | 
						|
            $this->store($cookie, $setter);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns all cookies matching a given request URL
 | 
						|
     *
 | 
						|
     * The following checks are made:
 | 
						|
     *   - cookie domain should match request host
 | 
						|
     *   - cookie path should be a prefix for request path
 | 
						|
     *   - 'secure' cookies will only be sent for HTTPS requests
 | 
						|
     *
 | 
						|
     * @param Net_URL2 $url      Request url
 | 
						|
     * @param bool     $asString Whether to return cookies as string for "Cookie: " header
 | 
						|
     *
 | 
						|
     * @return array|string Matching cookies
 | 
						|
     */
 | 
						|
    public function getMatching(Net_URL2 $url, $asString = false)
 | 
						|
    {
 | 
						|
        $host   = $url->getHost();
 | 
						|
        $path   = $url->getPath();
 | 
						|
        $secure = 0 == strcasecmp($url->getScheme(), 'https');
 | 
						|
 | 
						|
        $matched = $ret = array();
 | 
						|
        foreach (array_keys($this->cookies) as $domain) {
 | 
						|
            if ($this->domainMatch($host, $domain)) {
 | 
						|
                foreach (array_keys($this->cookies[$domain]) as $cPath) {
 | 
						|
                    if (0 === strpos($path, $cPath)) {
 | 
						|
                        foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
 | 
						|
                            if (!$cookie['secure'] || $secure) {
 | 
						|
                                $matched[$name][strlen($cookie['path'])] = $cookie;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        foreach ($matched as $cookies) {
 | 
						|
            krsort($cookies);
 | 
						|
            $ret = array_merge($ret, $cookies);
 | 
						|
        }
 | 
						|
        if (!$asString) {
 | 
						|
            return $ret;
 | 
						|
        } else {
 | 
						|
            $str = '';
 | 
						|
            foreach ($ret as $c) {
 | 
						|
                $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
 | 
						|
            }
 | 
						|
            return $str;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns all cookies stored in a jar
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function getAll()
 | 
						|
    {
 | 
						|
        $cookies = array();
 | 
						|
        foreach (array_keys($this->cookies) as $domain) {
 | 
						|
            foreach (array_keys($this->cookies[$domain]) as $path) {
 | 
						|
                foreach ($this->cookies[$domain][$path] as $name => $cookie) {
 | 
						|
                    $cookies[] = $cookie;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return $cookies;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets whether session cookies should be serialized when serializing the jar
 | 
						|
     *
 | 
						|
     * @param boolean $serialize serialize?
 | 
						|
     */
 | 
						|
    public function serializeSessionCookies($serialize)
 | 
						|
    {
 | 
						|
        $this->serializeSession = (bool)$serialize;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets whether Public Suffix List should be used for restricting cookie-setting
 | 
						|
     *
 | 
						|
     * Without PSL {@link domainMatch()} will only prevent setting cookies for
 | 
						|
     * top-level domains like '.com' or '.org'. However, it will not prevent
 | 
						|
     * setting a cookie for '.co.uk' even though only third-level registrations
 | 
						|
     * are possible in .uk domain.
 | 
						|
     *
 | 
						|
     * With the List it is possible to find the highest level at which a domain
 | 
						|
     * may be registered for a particular top-level domain and consequently
 | 
						|
     * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
 | 
						|
     * Firefox, Chrome and Opera browsers to restrict cookie setting.
 | 
						|
     *
 | 
						|
     * Note that PSL is licensed differently to HTTP_Request2 package (refer to
 | 
						|
     * the license information in public-suffix-list.php), so you can disable
 | 
						|
     * its use if this is an issue for you.
 | 
						|
     *
 | 
						|
     * @param boolean $useList use the list?
 | 
						|
     *
 | 
						|
     * @link     http://publicsuffix.org/learn/
 | 
						|
     */
 | 
						|
    public function usePublicSuffixList($useList)
 | 
						|
    {
 | 
						|
        $this->useList = (bool)$useList;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns string representation of object
 | 
						|
     *
 | 
						|
     * @return string
 | 
						|
     *
 | 
						|
     * @see    Serializable::serialize()
 | 
						|
     */
 | 
						|
    public function serialize()
 | 
						|
    {
 | 
						|
        $cookies = $this->getAll();
 | 
						|
        if (!$this->serializeSession) {
 | 
						|
            for ($i = count($cookies) - 1; $i >= 0; $i--) {
 | 
						|
                if (empty($cookies[$i]['expires'])) {
 | 
						|
                    unset($cookies[$i]);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return serialize(array(
 | 
						|
            'cookies'          => $cookies,
 | 
						|
            'serializeSession' => $this->serializeSession,
 | 
						|
            'useList'          => $this->useList
 | 
						|
        ));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Constructs the object from serialized string
 | 
						|
     *
 | 
						|
     * @param string $serialized string representation
 | 
						|
     *
 | 
						|
     * @see   Serializable::unserialize()
 | 
						|
     */
 | 
						|
    public function unserialize($serialized)
 | 
						|
    {
 | 
						|
        $data = unserialize($serialized);
 | 
						|
        $now  = $this->now();
 | 
						|
        $this->serializeSessionCookies($data['serializeSession']);
 | 
						|
        $this->usePublicSuffixList($data['useList']);
 | 
						|
        foreach ($data['cookies'] as $cookie) {
 | 
						|
            if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            if (!isset($this->cookies[$cookie['domain']])) {
 | 
						|
                $this->cookies[$cookie['domain']] = array();
 | 
						|
            }
 | 
						|
            if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
 | 
						|
                $this->cookies[$cookie['domain']][$cookie['path']] = array();
 | 
						|
            }
 | 
						|
            $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Checks whether a cookie domain matches a request host.
 | 
						|
     *
 | 
						|
     * The method is used by {@link store()} to check for whether a document
 | 
						|
     * at given URL can set a cookie with a given domain attribute and by
 | 
						|
     * {@link getMatching()} to find cookies matching the request URL.
 | 
						|
     *
 | 
						|
     * @param string $requestHost  request host
 | 
						|
     * @param string $cookieDomain cookie domain
 | 
						|
     *
 | 
						|
     * @return   bool    match success
 | 
						|
     */
 | 
						|
    public function domainMatch($requestHost, $cookieDomain)
 | 
						|
    {
 | 
						|
        if ($requestHost == $cookieDomain) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        // IP address, we require exact match
 | 
						|
        if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        if ('.' != $cookieDomain[0]) {
 | 
						|
            $cookieDomain = '.' . $cookieDomain;
 | 
						|
        }
 | 
						|
        // prevents setting cookies for '.com' and similar domains
 | 
						|
        if (!$this->useList && substr_count($cookieDomain, '.') < 2
 | 
						|
            || $this->useList && !self::getRegisteredDomain($cookieDomain)
 | 
						|
        ) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Removes subdomains to get the registered domain (the first after top-level)
 | 
						|
     *
 | 
						|
     * The method will check Public Suffix List to find out where top-level
 | 
						|
     * domain ends and registered domain starts. It will remove domain parts
 | 
						|
     * to the left of registered one.
 | 
						|
     *
 | 
						|
     * @param string $domain domain name
 | 
						|
     *
 | 
						|
     * @return string|bool   registered domain, will return false if $domain is
 | 
						|
     *                       either invalid or a TLD itself
 | 
						|
     */
 | 
						|
    public static function getRegisteredDomain($domain)
 | 
						|
    {
 | 
						|
        $domainParts = explode('.', ltrim($domain, '.'));
 | 
						|
 | 
						|
        // load the list if needed
 | 
						|
        if (empty(self::$psl)) {
 | 
						|
            $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
 | 
						|
            if (0 === strpos($path, '@' . 'data_dir@')) {
 | 
						|
                $path = realpath(
 | 
						|
                    dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
 | 
						|
                    . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'
 | 
						|
                );
 | 
						|
            }
 | 
						|
            self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
 | 
						|
        }
 | 
						|
 | 
						|
        if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
 | 
						|
            // known TLD, invalid domain name
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        // unknown TLD
 | 
						|
        if (!strpos($result, '.')) {
 | 
						|
            // fallback to checking that domain "has at least two dots"
 | 
						|
            if (2 > ($count = count($domainParts))) {
 | 
						|
                return false;
 | 
						|
            }
 | 
						|
            return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
 | 
						|
        }
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Recursive helper method for {@link getRegisteredDomain()}
 | 
						|
     *
 | 
						|
     * @param array $domainParts remaining domain parts
 | 
						|
     * @param mixed $listNode    node in {@link HTTP_Request2_CookieJar::$psl} to check
 | 
						|
     *
 | 
						|
     * @return string|null   concatenated domain parts, null in case of error
 | 
						|
     */
 | 
						|
    protected static function checkDomainsList(array $domainParts, $listNode)
 | 
						|
    {
 | 
						|
        $sub    = array_pop($domainParts);
 | 
						|
        $result = null;
 | 
						|
 | 
						|
        if (!is_array($listNode) || is_null($sub)
 | 
						|
            || array_key_exists('!' . $sub, $listNode)
 | 
						|
        ) {
 | 
						|
            return $sub;
 | 
						|
 | 
						|
        } elseif (array_key_exists($sub, $listNode)) {
 | 
						|
            $result = self::checkDomainsList($domainParts, $listNode[$sub]);
 | 
						|
 | 
						|
        } elseif (array_key_exists('*', $listNode)) {
 | 
						|
            $result = self::checkDomainsList($domainParts, $listNode['*']);
 | 
						|
 | 
						|
        } else {
 | 
						|
            return $sub;
 | 
						|
        }
 | 
						|
 | 
						|
        return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
 | 
						|
    }
 | 
						|
}
 | 
						|
?>
 |