3 * Stores cookies and passes them between HTTP requests
\r
9 * This source file is subject to BSD 3-Clause License that is bundled
\r
10 * with this package in the file LICENSE and available at the URL
\r
11 * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
\r
14 * @package HTTP_Request2
\r
15 * @author Alexey Borzov <avb@php.net>
\r
16 * @copyright 2008-2014 Alexey Borzov <avb@php.net>
\r
17 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
\r
18 * @link http://pear.php.net/package/HTTP_Request2
\r
21 /** Class representing a HTTP request message */
\r
22 require_once 'HTTP/Request2.php';
\r
25 * Stores cookies and passes them between HTTP requests
\r
28 * @package HTTP_Request2
\r
29 * @author Alexey Borzov <avb@php.net>
\r
30 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
\r
31 * @version Release: @package_version@
\r
32 * @link http://pear.php.net/package/HTTP_Request2
\r
34 class HTTP_Request2_CookieJar implements Serializable
\r
37 * Array of stored cookies
\r
39 * The array is indexed by domain, path and cookie name
\r
42 * some_cookie => cookie data
\r
44 * other_cookie => cookie data
\r
50 protected $cookies = array();
\r
53 * Whether session cookies should be serialized when serializing the jar
\r
56 protected $serializeSession = false;
\r
59 * Whether Public Suffix List should be used for domain matching
\r
62 protected $useList = true;
\r
65 * Array with Public Suffix List data
\r
67 * @link http://publicsuffix.org/
\r
69 protected static $psl = array();
\r
72 * Class constructor, sets various options
\r
74 * @param bool $serializeSessionCookies Controls serializing session cookies,
\r
75 * see {@link serializeSessionCookies()}
\r
76 * @param bool $usePublicSuffixList Controls using Public Suffix List,
\r
77 * see {@link usePublicSuffixList()}
\r
79 public function __construct(
\r
80 $serializeSessionCookies = false, $usePublicSuffixList = true
\r
82 $this->serializeSessionCookies($serializeSessionCookies);
\r
83 $this->usePublicSuffixList($usePublicSuffixList);
\r
87 * Returns current time formatted in ISO-8601 at UTC timezone
\r
91 protected function now()
\r
93 $dt = new DateTime();
\r
94 $dt->setTimezone(new DateTimeZone('UTC'));
\r
95 return $dt->format(DateTime::ISO8601);
\r
99 * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
\r
101 * The checks are as follows:
\r
102 * - cookie array should contain 'name' and 'value' fields;
\r
103 * - name and value should not contain disallowed symbols;
\r
104 * - 'expires' should be either empty parseable by DateTime;
\r
105 * - 'domain' and 'path' should be either not empty or an URL where
\r
106 * cookie was set should be provided.
\r
107 * - if $setter is provided, then document at that URL should be allowed
\r
108 * to set a cookie for that 'domain'. If $setter is not provided,
\r
109 * then no domain checks will be made.
\r
111 * 'expires' field will be converted to ISO8601 format from COOKIE format,
\r
112 * 'domain' and 'path' will be set from setter URL if empty.
\r
114 * @param array $cookie cookie data, as returned by
\r
115 * {@link HTTP_Request2_Response::getCookies()}
\r
116 * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
\r
118 * @return array Updated cookie array
\r
119 * @throws HTTP_Request2_LogicException
\r
120 * @throws HTTP_Request2_MessageException
\r
122 protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
\r
124 if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
\r
125 throw new HTTP_Request2_LogicException(
\r
126 "Cookie array should contain 'name' and 'value' fields",
\r
127 HTTP_Request2_Exception::MISSING_VALUE
\r
130 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
\r
131 throw new HTTP_Request2_LogicException(
\r
132 "Invalid cookie name: '{$cookie['name']}'",
\r
133 HTTP_Request2_Exception::INVALID_ARGUMENT
\r
136 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
\r
137 throw new HTTP_Request2_LogicException(
\r
138 "Invalid cookie value: '{$cookie['value']}'",
\r
139 HTTP_Request2_Exception::INVALID_ARGUMENT
\r
142 $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
\r
144 // Need ISO-8601 date @ UTC timezone
\r
145 if (!empty($cookie['expires'])
\r
146 && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
\r
149 $dt = new DateTime($cookie['expires']);
\r
150 $dt->setTimezone(new DateTimeZone('UTC'));
\r
151 $cookie['expires'] = $dt->format(DateTime::ISO8601);
\r
152 } catch (Exception $e) {
\r
153 throw new HTTP_Request2_LogicException($e->getMessage());
\r
157 if (empty($cookie['domain']) || empty($cookie['path'])) {
\r
159 throw new HTTP_Request2_LogicException(
\r
160 'Cookie misses domain and/or path component, cookie setter URL needed',
\r
161 HTTP_Request2_Exception::MISSING_VALUE
\r
164 if (empty($cookie['domain'])) {
\r
165 if ($host = $setter->getHost()) {
\r
166 $cookie['domain'] = $host;
\r
168 throw new HTTP_Request2_LogicException(
\r
169 'Setter URL does not contain host part, can\'t set cookie domain',
\r
170 HTTP_Request2_Exception::MISSING_VALUE
\r
174 if (empty($cookie['path'])) {
\r
175 $path = $setter->getPath();
\r
176 $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
\r
180 if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
\r
181 throw new HTTP_Request2_MessageException(
\r
182 "Domain " . $setter->getHost() . " cannot set cookies for "
\r
183 . $cookie['domain']
\r
191 * Stores a cookie in the jar
\r
193 * @param array $cookie cookie data, as returned by
\r
194 * {@link HTTP_Request2_Response::getCookies()}
\r
195 * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
\r
197 * @throws HTTP_Request2_Exception
\r
199 public function store(array $cookie, Net_URL2 $setter = null)
\r
201 $cookie = $this->checkAndUpdateFields($cookie, $setter);
\r
203 if (strlen($cookie['value'])
\r
204 && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
\r
206 if (!isset($this->cookies[$cookie['domain']])) {
\r
207 $this->cookies[$cookie['domain']] = array();
\r
209 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
\r
210 $this->cookies[$cookie['domain']][$cookie['path']] = array();
\r
212 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
\r
214 } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
\r
215 unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
\r
220 * Adds cookies set in HTTP response to the jar
\r
222 * @param HTTP_Request2_Response $response HTTP response message
\r
223 * @param Net_URL2 $setter original request URL, needed for
\r
224 * setting default domain/path
\r
226 public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
\r
228 foreach ($response->getCookies() as $cookie) {
\r
229 $this->store($cookie, $setter);
\r
234 * Returns all cookies matching a given request URL
\r
236 * The following checks are made:
\r
237 * - cookie domain should match request host
\r
238 * - cookie path should be a prefix for request path
\r
239 * - 'secure' cookies will only be sent for HTTPS requests
\r
241 * @param Net_URL2 $url Request url
\r
242 * @param bool $asString Whether to return cookies as string for "Cookie: " header
\r
244 * @return array|string Matching cookies
\r
246 public function getMatching(Net_URL2 $url, $asString = false)
\r
248 $host = $url->getHost();
\r
249 $path = $url->getPath();
\r
250 $secure = 0 == strcasecmp($url->getScheme(), 'https');
\r
252 $matched = $ret = array();
\r
253 foreach (array_keys($this->cookies) as $domain) {
\r
254 if ($this->domainMatch($host, $domain)) {
\r
255 foreach (array_keys($this->cookies[$domain]) as $cPath) {
\r
256 if (0 === strpos($path, $cPath)) {
\r
257 foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
\r
258 if (!$cookie['secure'] || $secure) {
\r
259 $matched[$name][strlen($cookie['path'])] = $cookie;
\r
266 foreach ($matched as $cookies) {
\r
268 $ret = array_merge($ret, $cookies);
\r
274 foreach ($ret as $c) {
\r
275 $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
\r
282 * Returns all cookies stored in a jar
\r
286 public function getAll()
\r
288 $cookies = array();
\r
289 foreach (array_keys($this->cookies) as $domain) {
\r
290 foreach (array_keys($this->cookies[$domain]) as $path) {
\r
291 foreach ($this->cookies[$domain][$path] as $name => $cookie) {
\r
292 $cookies[] = $cookie;
\r
300 * Sets whether session cookies should be serialized when serializing the jar
\r
302 * @param boolean $serialize serialize?
\r
304 public function serializeSessionCookies($serialize)
\r
306 $this->serializeSession = (bool)$serialize;
\r
310 * Sets whether Public Suffix List should be used for restricting cookie-setting
\r
312 * Without PSL {@link domainMatch()} will only prevent setting cookies for
\r
313 * top-level domains like '.com' or '.org'. However, it will not prevent
\r
314 * setting a cookie for '.co.uk' even though only third-level registrations
\r
315 * are possible in .uk domain.
\r
317 * With the List it is possible to find the highest level at which a domain
\r
318 * may be registered for a particular top-level domain and consequently
\r
319 * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
\r
320 * Firefox, Chrome and Opera browsers to restrict cookie setting.
\r
322 * Note that PSL is licensed differently to HTTP_Request2 package (refer to
\r
323 * the license information in public-suffix-list.php), so you can disable
\r
324 * its use if this is an issue for you.
\r
326 * @param boolean $useList use the list?
\r
328 * @link http://publicsuffix.org/learn/
\r
330 public function usePublicSuffixList($useList)
\r
332 $this->useList = (bool)$useList;
\r
336 * Returns string representation of object
\r
340 * @see Serializable::serialize()
\r
342 public function serialize()
\r
344 $cookies = $this->getAll();
\r
345 if (!$this->serializeSession) {
\r
346 for ($i = count($cookies) - 1; $i >= 0; $i--) {
\r
347 if (empty($cookies[$i]['expires'])) {
\r
348 unset($cookies[$i]);
\r
352 return serialize(array(
\r
353 'cookies' => $cookies,
\r
354 'serializeSession' => $this->serializeSession,
\r
355 'useList' => $this->useList
\r
360 * Constructs the object from serialized string
\r
362 * @param string $serialized string representation
\r
364 * @see Serializable::unserialize()
\r
366 public function unserialize($serialized)
\r
368 $data = unserialize($serialized);
\r
369 $now = $this->now();
\r
370 $this->serializeSessionCookies($data['serializeSession']);
\r
371 $this->usePublicSuffixList($data['useList']);
\r
372 foreach ($data['cookies'] as $cookie) {
\r
373 if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
\r
376 if (!isset($this->cookies[$cookie['domain']])) {
\r
377 $this->cookies[$cookie['domain']] = array();
\r
379 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
\r
380 $this->cookies[$cookie['domain']][$cookie['path']] = array();
\r
382 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
\r
387 * Checks whether a cookie domain matches a request host.
\r
389 * The method is used by {@link store()} to check for whether a document
\r
390 * at given URL can set a cookie with a given domain attribute and by
\r
391 * {@link getMatching()} to find cookies matching the request URL.
\r
393 * @param string $requestHost request host
\r
394 * @param string $cookieDomain cookie domain
\r
396 * @return bool match success
\r
398 public function domainMatch($requestHost, $cookieDomain)
\r
400 if ($requestHost == $cookieDomain) {
\r
403 // IP address, we require exact match
\r
404 if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
\r
407 if ('.' != $cookieDomain[0]) {
\r
408 $cookieDomain = '.' . $cookieDomain;
\r
410 // prevents setting cookies for '.com' and similar domains
\r
411 if (!$this->useList && substr_count($cookieDomain, '.') < 2
\r
412 || $this->useList && !self::getRegisteredDomain($cookieDomain)
\r
416 return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
\r
420 * Removes subdomains to get the registered domain (the first after top-level)
\r
422 * The method will check Public Suffix List to find out where top-level
\r
423 * domain ends and registered domain starts. It will remove domain parts
\r
424 * to the left of registered one.
\r
426 * @param string $domain domain name
\r
428 * @return string|bool registered domain, will return false if $domain is
\r
429 * either invalid or a TLD itself
\r
431 public static function getRegisteredDomain($domain)
\r
433 $domainParts = explode('.', ltrim($domain, '.'));
\r
435 // load the list if needed
\r
436 if (empty(self::$psl)) {
\r
437 $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
\r
438 if (0 === strpos($path, '@' . 'data_dir@')) {
\r
440 dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
\r
441 . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'
\r
444 self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
\r
447 if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
\r
448 // known TLD, invalid domain name
\r
453 if (!strpos($result, '.')) {
\r
454 // fallback to checking that domain "has at least two dots"
\r
455 if (2 > ($count = count($domainParts))) {
\r
458 return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
\r
464 * Recursive helper method for {@link getRegisteredDomain()}
\r
466 * @param array $domainParts remaining domain parts
\r
467 * @param mixed $listNode node in {@link HTTP_Request2_CookieJar::$psl} to check
\r
469 * @return string|null concatenated domain parts, null in case of error
\r
471 protected static function checkDomainsList(array $domainParts, $listNode)
\r
473 $sub = array_pop($domainParts);
\r
476 if (!is_array($listNode) || is_null($sub)
\r
477 || array_key_exists('!' . $sub, $listNode)
\r
481 } elseif (array_key_exists($sub, $listNode)) {
\r
482 $result = self::checkDomainsList($domainParts, $listNode[$sub]);
\r
484 } elseif (array_key_exists('*', $listNode)) {
\r
485 $result = self::checkDomainsList($domainParts, $listNode['*']);
\r
491 return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
\r