[TASK] Update PEAR packages
[Packages/TYPO3.CMS.git] / typo3 / contrib / pear / HTTP / Request2 / CookieJar.php
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 */
45 require_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 */
57 class 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_dir@' . 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 ?>