3 * Stores cookies and passes them between HTTP requests
\r
9 * Copyright (c) 2008-2012, Alexey Borzov <avb@php.net>
\r
10 * All rights reserved.
\r
12 * Redistribution and use in source and binary forms, with or without
\r
13 * modification, are permitted provided that the following conditions
\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
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
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 324415 2012-03-21 10:50:50Z avb $
\r
41 * @link http://pear.php.net/package/HTTP_Request2
\r
44 /** Class representing a HTTP request message */
\r
45 require_once 'HTTP/Request2.php';
\r
48 * Stores cookies and passes them between HTTP requests
\r
51 * @package HTTP_Request2
\r
52 * @author Alexey Borzov <avb@php.net>
\r
53 * @license http://opensource.org/licenses/bsd-license.php New BSD License
\r
54 * @version Release: @package_version@
\r
55 * @link http://pear.php.net/package/HTTP_Request2
\r
57 class HTTP_Request2_CookieJar implements Serializable
\r
60 * Array of stored cookies
\r
62 * The array is indexed by domain, path and cookie name
\r
65 * some_cookie => cookie data
\r
67 * other_cookie => cookie data
\r
73 protected $cookies = array();
\r
76 * Whether session cookies should be serialized when serializing the jar
\r
79 protected $serializeSession = false;
\r
82 * Whether Public Suffix List should be used for domain matching
\r
85 protected $useList = true;
\r
88 * Array with Public Suffix List data
\r
90 * @link http://publicsuffix.org/
\r
92 protected static $psl = array();
\r
95 * Class constructor, sets various options
\r
97 * @param bool $serializeSessionCookies Controls serializing session cookies,
\r
98 * see {@link serializeSessionCookies()}
\r
99 * @param bool $usePublicSuffixList Controls using Public Suffix List,
\r
100 * see {@link usePublicSuffixList()}
\r
102 public function __construct(
\r
103 $serializeSessionCookies = false, $usePublicSuffixList = true
\r
105 $this->serializeSessionCookies($serializeSessionCookies);
\r
106 $this->usePublicSuffixList($usePublicSuffixList);
\r
110 * Returns current time formatted in ISO-8601 at UTC timezone
\r
114 protected function now()
\r
116 $dt = new DateTime();
\r
117 $dt->setTimezone(new DateTimeZone('UTC'));
\r
118 return $dt->format(DateTime::ISO8601);
\r
122 * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
\r
124 * The checks are as follows:
\r
125 * - cookie array should contain 'name' and 'value' fields;
\r
126 * - name and value should not contain disallowed symbols;
\r
127 * - 'expires' should be either empty parseable by DateTime;
\r
128 * - 'domain' and 'path' should be either not empty or an URL where
\r
129 * cookie was set should be provided.
\r
130 * - if $setter is provided, then document at that URL should be allowed
\r
131 * to set a cookie for that 'domain'. If $setter is not provided,
\r
132 * then no domain checks will be made.
\r
134 * 'expires' field will be converted to ISO8601 format from COOKIE format,
\r
135 * 'domain' and 'path' will be set from setter URL if empty.
\r
137 * @param array $cookie cookie data, as returned by
\r
138 * {@link HTTP_Request2_Response::getCookies()}
\r
139 * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
\r
141 * @return array Updated cookie array
\r
142 * @throws HTTP_Request2_LogicException
\r
143 * @throws HTTP_Request2_MessageException
\r
145 protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
\r
147 if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
\r
148 throw new HTTP_Request2_LogicException(
\r
149 "Cookie array should contain 'name' and 'value' fields",
\r
150 HTTP_Request2_Exception::MISSING_VALUE
\r
153 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
\r
154 throw new HTTP_Request2_LogicException(
\r
155 "Invalid cookie name: '{$cookie['name']}'",
\r
156 HTTP_Request2_Exception::INVALID_ARGUMENT
\r
159 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
\r
160 throw new HTTP_Request2_LogicException(
\r
161 "Invalid cookie value: '{$cookie['value']}'",
\r
162 HTTP_Request2_Exception::INVALID_ARGUMENT
\r
165 $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
\r
167 // Need ISO-8601 date @ UTC timezone
\r
168 if (!empty($cookie['expires'])
\r
169 && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
\r
172 $dt = new DateTime($cookie['expires']);
\r
173 $dt->setTimezone(new DateTimeZone('UTC'));
\r
174 $cookie['expires'] = $dt->format(DateTime::ISO8601);
\r
175 } catch (Exception $e) {
\r
176 throw new HTTP_Request2_LogicException($e->getMessage());
\r
180 if (empty($cookie['domain']) || empty($cookie['path'])) {
\r
182 throw new HTTP_Request2_LogicException(
\r
183 'Cookie misses domain and/or path component, cookie setter URL needed',
\r
184 HTTP_Request2_Exception::MISSING_VALUE
\r
187 if (empty($cookie['domain'])) {
\r
188 if ($host = $setter->getHost()) {
\r
189 $cookie['domain'] = $host;
\r
191 throw new HTTP_Request2_LogicException(
\r
192 'Setter URL does not contain host part, can\'t set cookie domain',
\r
193 HTTP_Request2_Exception::MISSING_VALUE
\r
197 if (empty($cookie['path'])) {
\r
198 $path = $setter->getPath();
\r
199 $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
\r
203 if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
\r
204 throw new HTTP_Request2_MessageException(
\r
205 "Domain " . $setter->getHost() . " cannot set cookies for "
\r
206 . $cookie['domain']
\r
214 * Stores a cookie in the jar
\r
216 * @param array $cookie cookie data, as returned by
\r
217 * {@link HTTP_Request2_Response::getCookies()}
\r
218 * @param Net_URL2 $setter URL of the document that sent Set-Cookie header
\r
220 * @throws HTTP_Request2_Exception
\r
222 public function store(array $cookie, Net_URL2 $setter = null)
\r
224 $cookie = $this->checkAndUpdateFields($cookie, $setter);
\r
226 if (strlen($cookie['value'])
\r
227 && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
\r
229 if (!isset($this->cookies[$cookie['domain']])) {
\r
230 $this->cookies[$cookie['domain']] = array();
\r
232 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
\r
233 $this->cookies[$cookie['domain']][$cookie['path']] = array();
\r
235 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
\r
237 } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
\r
238 unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
\r
243 * Adds cookies set in HTTP response to the jar
\r
245 * @param HTTP_Request2_Response $response HTTP response message
\r
246 * @param Net_URL2 $setter original request URL, needed for
\r
247 * setting default domain/path
\r
249 public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
\r
251 foreach ($response->getCookies() as $cookie) {
\r
252 $this->store($cookie, $setter);
\r
257 * Returns all cookies matching a given request URL
\r
259 * The following checks are made:
\r
260 * - cookie domain should match request host
\r
261 * - cookie path should be a prefix for request path
\r
262 * - 'secure' cookies will only be sent for HTTPS requests
\r
264 * @param Net_URL2 $url Request url
\r
265 * @param bool $asString Whether to return cookies as string for "Cookie: " header
\r
267 * @return array|string Matching cookies
\r
269 public function getMatching(Net_URL2 $url, $asString = false)
\r
271 $host = $url->getHost();
\r
272 $path = $url->getPath();
\r
273 $secure = 0 == strcasecmp($url->getScheme(), 'https');
\r
275 $matched = $ret = array();
\r
276 foreach (array_keys($this->cookies) as $domain) {
\r
277 if ($this->domainMatch($host, $domain)) {
\r
278 foreach (array_keys($this->cookies[$domain]) as $cPath) {
\r
279 if (0 === strpos($path, $cPath)) {
\r
280 foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
\r
281 if (!$cookie['secure'] || $secure) {
\r
282 $matched[$name][strlen($cookie['path'])] = $cookie;
\r
289 foreach ($matched as $cookies) {
\r
291 $ret = array_merge($ret, $cookies);
\r
297 foreach ($ret as $c) {
\r
298 $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
\r
305 * Returns all cookies stored in a jar
\r
309 public function getAll()
\r
311 $cookies = array();
\r
312 foreach (array_keys($this->cookies) as $domain) {
\r
313 foreach (array_keys($this->cookies[$domain]) as $path) {
\r
314 foreach ($this->cookies[$domain][$path] as $name => $cookie) {
\r
315 $cookies[] = $cookie;
\r
323 * Sets whether session cookies should be serialized when serializing the jar
\r
325 * @param boolean $serialize serialize?
\r
327 public function serializeSessionCookies($serialize)
\r
329 $this->serializeSession = (bool)$serialize;
\r
333 * Sets whether Public Suffix List should be used for restricting cookie-setting
\r
335 * Without PSL {@link domainMatch()} will only prevent setting cookies for
\r
336 * top-level domains like '.com' or '.org'. However, it will not prevent
\r
337 * setting a cookie for '.co.uk' even though only third-level registrations
\r
338 * are possible in .uk domain.
\r
340 * With the List it is possible to find the highest level at which a domain
\r
341 * may be registered for a particular top-level domain and consequently
\r
342 * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
\r
343 * Firefox, Chrome and Opera browsers to restrict cookie setting.
\r
345 * Note that PSL is licensed differently to HTTP_Request2 package (refer to
\r
346 * the license information in public-suffix-list.php), so you can disable
\r
347 * its use if this is an issue for you.
\r
349 * @param boolean $useList use the list?
\r
351 * @link http://publicsuffix.org/learn/
\r
353 public function usePublicSuffixList($useList)
\r
355 $this->useList = (bool)$useList;
\r
359 * Returns string representation of object
\r
363 * @see Serializable::serialize()
\r
365 public function serialize()
\r
367 $cookies = $this->getAll();
\r
368 if (!$this->serializeSession) {
\r
369 for ($i = count($cookies) - 1; $i >= 0; $i--) {
\r
370 if (empty($cookies[$i]['expires'])) {
\r
371 unset($cookies[$i]);
\r
375 return serialize(array(
\r
376 'cookies' => $cookies,
\r
377 'serializeSession' => $this->serializeSession,
\r
378 'useList' => $this->useList
\r
383 * Constructs the object from serialized string
\r
385 * @param string $serialized string representation
\r
387 * @see Serializable::unserialize()
\r
389 public function unserialize($serialized)
\r
391 $data = unserialize($serialized);
\r
392 $now = $this->now();
\r
393 $this->serializeSessionCookies($data['serializeSession']);
\r
394 $this->usePublicSuffixList($data['useList']);
\r
395 foreach ($data['cookies'] as $cookie) {
\r
396 if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
\r
399 if (!isset($this->cookies[$cookie['domain']])) {
\r
400 $this->cookies[$cookie['domain']] = array();
\r
402 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
\r
403 $this->cookies[$cookie['domain']][$cookie['path']] = array();
\r
405 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
\r
410 * Checks whether a cookie domain matches a request host.
\r
412 * The method is used by {@link store()} to check for whether a document
\r
413 * at given URL can set a cookie with a given domain attribute and by
\r
414 * {@link getMatching()} to find cookies matching the request URL.
\r
416 * @param string $requestHost request host
\r
417 * @param string $cookieDomain cookie domain
\r
419 * @return bool match success
\r
421 public function domainMatch($requestHost, $cookieDomain)
\r
423 if ($requestHost == $cookieDomain) {
\r
426 // IP address, we require exact match
\r
427 if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
\r
430 if ('.' != $cookieDomain[0]) {
\r
431 $cookieDomain = '.' . $cookieDomain;
\r
433 // prevents setting cookies for '.com' and similar domains
\r
434 if (!$this->useList && substr_count($cookieDomain, '.') < 2
\r
435 || $this->useList && !self::getRegisteredDomain($cookieDomain)
\r
439 return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
\r
443 * Removes subdomains to get the registered domain (the first after top-level)
\r
445 * The method will check Public Suffix List to find out where top-level
\r
446 * domain ends and registered domain starts. It will remove domain parts
\r
447 * to the left of registered one.
\r
449 * @param string $domain domain name
\r
451 * @return string|bool registered domain, will return false if $domain is
\r
452 * either invalid or a TLD itself
\r
454 public static function getRegisteredDomain($domain)
\r
456 $domainParts = explode('.', ltrim($domain, '.'));
\r
458 // load the list if needed
\r
459 if (empty(self::$psl)) {
\r
460 $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
\r
461 if (0 === strpos($path, '@' . 'data_dir@')) {
\r
463 dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
\r
464 . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data'
\r
467 self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
\r
470 if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
\r
471 // known TLD, invalid domain name
\r
476 if (!strpos($result, '.')) {
\r
477 // fallback to checking that domain "has at least two dots"
\r
478 if (2 > ($count = count($domainParts))) {
\r
481 return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
\r
487 * Recursive helper method for {@link getRegisteredDomain()}
\r
489 * @param array $domainParts remaining domain parts
\r
490 * @param mixed $listNode node in {@link HTTP_Request2_CookieJar::$psl} to check
\r
492 * @return string|null concatenated domain parts, null in case of error
\r
494 protected static function checkDomainsList(array $domainParts, $listNode)
\r
496 $sub = array_pop($domainParts);
\r
499 if (!is_array($listNode) || is_null($sub)
\r
500 || array_key_exists('!' . $sub, $listNode)
\r
504 } elseif (array_key_exists($sub, $listNode)) {
\r
505 $result = self::checkDomainsList($domainParts, $listNode[$sub]);
\r
507 } elseif (array_key_exists('*', $listNode)) {
\r
508 $result = self::checkDomainsList($domainParts, $listNode['*']);
\r
514 return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
\r