517 lines
19 KiB
PHP
517 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Stores cookies and passes them between HTTP requests
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* LICENSE:
|
|
*
|
|
* Copyright (c) 2008-2012, Alexey Borzov <avb@php.net>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * The names of the authors may not be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* @category HTTP
|
|
* @package HTTP_Request2
|
|
* @author Alexey Borzov <avb@php.net>
|
|
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
|
* @version SVN: $Id: CookieJar.php 324415 2012-03-21 10:50:50Z avb $
|
|
* @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-license.php New BSD 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;
|
|
}
|
|
}
|
|
?>
|