[BUGFIX] Hardcoded variable in CookieJar.php
[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-2011, 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 308629 2011-02-24 17:34:24Z 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 * @version Release: 2.0.0RC1
54 */
55 class HTTP_Request2_CookieJar implements Serializable
56 {
57 /**
58 * Array of stored cookies
59 *
60 * The array is indexed by domain, path and cookie name
61 * .example.com
62 * /
63 * some_cookie => cookie data
64 * /subdir
65 * other_cookie => cookie data
66 * .example.org
67 * ...
68 *
69 * @var array
70 */
71 protected $cookies = array();
72
73 /**
74 * Whether session cookies should be serialized when serializing the jar
75 * @var bool
76 */
77 protected $serializeSession = false;
78
79 /**
80 * Whether Public Suffix List should be used for domain matching
81 * @var bool
82 */
83 protected $useList = true;
84
85 /**
86 * Array with Public Suffix List data
87 * @var array
88 * @link http://publicsuffix.org/
89 */
90 protected static $psl = array();
91
92 /**
93 * Class constructor, sets various options
94 *
95 * @param bool Controls serializing session cookies, see {@link serializeSessionCookies()}
96 * @param bool Controls using Public Suffix List, see {@link usePublicSuffixList()}
97 */
98 public function __construct($serializeSessionCookies = false, $usePublicSuffixList = true)
99 {
100 $this->serializeSessionCookies($serializeSessionCookies);
101 $this->usePublicSuffixList($usePublicSuffixList);
102 }
103
104 /**
105 * Returns current time formatted in ISO-8601 at UTC timezone
106 *
107 * @return string
108 */
109 protected function now()
110 {
111 $dt = new DateTime();
112 $dt->setTimezone(new DateTimeZone('UTC'));
113 return $dt->format(DateTime::ISO8601);
114 }
115
116 /**
117 * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
118 *
119 * The checks are as follows:
120 * - cookie array should contain 'name' and 'value' fields;
121 * - name and value should not contain disallowed symbols;
122 * - 'expires' should be either empty parseable by DateTime;
123 * - 'domain' and 'path' should be either not empty or an URL where
124 * cookie was set should be provided.
125 * - if $setter is provided, then document at that URL should be allowed
126 * to set a cookie for that 'domain'. If $setter is not provided,
127 * then no domain checks will be made.
128 *
129 * 'expires' field will be converted to ISO8601 format from COOKIE format,
130 * 'domain' and 'path' will be set from setter URL if empty.
131 *
132 * @param array cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
133 * @param Net_URL2 URL of the document that sent Set-Cookie header
134 * @return array Updated cookie array
135 * @throws HTTP_Request2_LogicException
136 * @throws HTTP_Request2_MessageException
137 */
138 protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
139 {
140 if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
141 throw new HTTP_Request2_LogicException(
142 "Cookie array should contain 'name' and 'value' fields",
143 HTTP_Request2_Exception::MISSING_VALUE
144 );
145 }
146 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
147 throw new HTTP_Request2_LogicException(
148 "Invalid cookie name: '{$cookie['name']}'",
149 HTTP_Request2_Exception::INVALID_ARGUMENT
150 );
151 }
152 if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
153 throw new HTTP_Request2_LogicException(
154 "Invalid cookie value: '{$cookie['value']}'",
155 HTTP_Request2_Exception::INVALID_ARGUMENT
156 );
157 }
158 $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
159
160 // Need ISO-8601 date @ UTC timezone
161 if (!empty($cookie['expires'])
162 && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
163 ) {
164 try {
165 $dt = new DateTime($cookie['expires']);
166 $dt->setTimezone(new DateTimeZone('UTC'));
167 $cookie['expires'] = $dt->format(DateTime::ISO8601);
168 } catch (Exception $e) {
169 throw new HTTP_Request2_LogicException($e->getMessage());
170 }
171 }
172
173 if (empty($cookie['domain']) || empty($cookie['path'])) {
174 if (!$setter) {
175 throw new HTTP_Request2_LogicException(
176 'Cookie misses domain and/or path component, cookie setter URL needed',
177 HTTP_Request2_Exception::MISSING_VALUE
178 );
179 }
180 if (empty($cookie['domain'])) {
181 if ($host = $setter->getHost()) {
182 $cookie['domain'] = $host;
183 } else {
184 throw new HTTP_Request2_LogicException(
185 'Setter URL does not contain host part, can\'t set cookie domain',
186 HTTP_Request2_Exception::MISSING_VALUE
187 );
188 }
189 }
190 if (empty($cookie['path'])) {
191 $path = $setter->getPath();
192 $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
193 }
194 }
195
196 if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
197 throw new HTTP_Request2_MessageException(
198 "Domain " . $setter->getHost() . " cannot set cookies for "
199 . $cookie['domain']
200 );
201 }
202
203 return $cookie;
204 }
205
206 /**
207 * Stores a cookie in the jar
208 *
209 * @param array cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
210 * @param Net_URL2 URL of the document that sent Set-Cookie header
211 * @throws HTTP_Request2_Exception
212 */
213 public function store(array $cookie, Net_URL2 $setter = null)
214 {
215 $cookie = $this->checkAndUpdateFields($cookie, $setter);
216
217 if (strlen($cookie['value'])
218 && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
219 ) {
220 if (!isset($this->cookies[$cookie['domain']])) {
221 $this->cookies[$cookie['domain']] = array();
222 }
223 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
224 $this->cookies[$cookie['domain']][$cookie['path']] = array();
225 }
226 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
227
228 } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
229 unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
230 }
231 }
232
233 /**
234 * Adds cookies set in HTTP response to the jar
235 *
236 * @param HTTP_Request2_Response response
237 * @param Net_URL2 original request URL, needed for setting
238 * default domain/path
239 */
240 public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
241 {
242 foreach ($response->getCookies() as $cookie) {
243 $this->store($cookie, $setter);
244 }
245 }
246
247 /**
248 * Returns all cookies matching a given request URL
249 *
250 * The following checks are made:
251 * - cookie domain should match request host
252 * - cookie path should be a prefix for request path
253 * - 'secure' cookies will only be sent for HTTPS requests
254 *
255 * @param Net_URL2
256 * @param bool Whether to return cookies as string for "Cookie: " header
257 * @return array
258 */
259 public function getMatching(Net_URL2 $url, $asString = false)
260 {
261 $host = $url->getHost();
262 $path = $url->getPath();
263 $secure = 0 == strcasecmp($url->getScheme(), 'https');
264
265 $matched = $ret = array();
266 foreach (array_keys($this->cookies) as $domain) {
267 if ($this->domainMatch($host, $domain)) {
268 foreach (array_keys($this->cookies[$domain]) as $cPath) {
269 if (0 === strpos($path, $cPath)) {
270 foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
271 if (!$cookie['secure'] || $secure) {
272 $matched[$name][strlen($cookie['path'])] = $cookie;
273 }
274 }
275 }
276 }
277 }
278 }
279 foreach ($matched as $cookies) {
280 krsort($cookies);
281 $ret = array_merge($ret, $cookies);
282 }
283 if (!$asString) {
284 return $ret;
285 } else {
286 $str = '';
287 foreach ($ret as $c) {
288 $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
289 }
290 return $str;
291 }
292 }
293
294 /**
295 * Returns all cookies stored in a jar
296 *
297 * @return array
298 */
299 public function getAll()
300 {
301 $cookies = array();
302 foreach (array_keys($this->cookies) as $domain) {
303 foreach (array_keys($this->cookies[$domain]) as $path) {
304 foreach ($this->cookies[$domain][$path] as $name => $cookie) {
305 $cookies[] = $cookie;
306 }
307 }
308 }
309 return $cookies;
310 }
311
312 /**
313 * Sets whether session cookies should be serialized when serializing the jar
314 *
315 * @param boolean
316 */
317 public function serializeSessionCookies($serialize)
318 {
319 $this->serializeSession = (bool)$serialize;
320 }
321
322 /**
323 * Sets whether Public Suffix List should be used for restricting cookie-setting
324 *
325 * Without PSL {@link domainMatch()} will only prevent setting cookies for
326 * top-level domains like '.com' or '.org'. However, it will not prevent
327 * setting a cookie for '.co.uk' even though only third-level registrations
328 * are possible in .uk domain.
329 *
330 * With the List it is possible to find the highest level at which a domain
331 * may be registered for a particular top-level domain and consequently
332 * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
333 * Firefox, Chrome and Opera browsers to restrict cookie setting.
334 *
335 * Note that PSL is licensed differently to HTTP_Request2 package (refer to
336 * the license information in public-suffix-list.php), so you can disable
337 * its use if this is an issue for you.
338 *
339 * @param boolean
340 * @link http://publicsuffix.org/learn/
341 */
342 public function usePublicSuffixList($useList)
343 {
344 $this->useList = (bool)$useList;
345 }
346
347 /**
348 * Returns string representation of object
349 *
350 * @return string
351 * @see Serializable::serialize()
352 */
353 public function serialize()
354 {
355 $cookies = $this->getAll();
356 if (!$this->serializeSession) {
357 for ($i = count($cookies) - 1; $i >= 0; $i--) {
358 if (empty($cookies[$i]['expires'])) {
359 unset($cookies[$i]);
360 }
361 }
362 }
363 return serialize(array(
364 'cookies' => $cookies,
365 'serializeSession' => $this->serializeSession,
366 'useList' => $this->useList
367 ));
368 }
369
370 /**
371 * Constructs the object from serialized string
372 *
373 * @param string string representation
374 * @see Serializable::unserialize()
375 */
376 public function unserialize($serialized)
377 {
378 $data = unserialize($serialized);
379 $now = $this->now();
380 $this->serializeSessionCookies($data['serializeSession']);
381 $this->usePublicSuffixList($data['useList']);
382 foreach ($data['cookies'] as $cookie) {
383 if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
384 continue;
385 }
386 if (!isset($this->cookies[$cookie['domain']])) {
387 $this->cookies[$cookie['domain']] = array();
388 }
389 if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
390 $this->cookies[$cookie['domain']][$cookie['path']] = array();
391 }
392 $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
393 }
394 }
395
396 /**
397 * Checks whether a cookie domain matches a request host.
398 *
399 * The method is used by {@link store()} to check for whether a document
400 * at given URL can set a cookie with a given domain attribute and by
401 * {@link getMatching()} to find cookies matching the request URL.
402 *
403 * @param string request host
404 * @param string cookie domain
405 * @return bool match success
406 */
407 public function domainMatch($requestHost, $cookieDomain)
408 {
409 if ($requestHost == $cookieDomain) {
410 return true;
411 }
412 // IP address, we require exact match
413 if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
414 return false;
415 }
416 if ('.' != $cookieDomain[0]) {
417 $cookieDomain = '.' . $cookieDomain;
418 }
419 // prevents setting cookies for '.com' and similar domains
420 if (!$this->useList && substr_count($cookieDomain, '.') < 2
421 || $this->useList && !self::getRegisteredDomain($cookieDomain)
422 ) {
423 return false;
424 }
425 return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
426 }
427
428 /**
429 * Removes subdomains to get the registered domain (the first after top-level)
430 *
431 * The method will check Public Suffix List to find out where top-level
432 * domain ends and registered domain starts. It will remove domain parts
433 * to the left of registered one.
434 *
435 * @param string domain name
436 * @return string|bool registered domain, will return false if $domain is
437 * either invalid or a TLD itself
438 */
439 public static function getRegisteredDomain($domain)
440 {
441 $domainParts = explode('.', ltrim($domain, '.'));
442
443 // load the list if needed
444 if (empty(self::$psl)) {
445 $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
446 if (0 === strpos($path, '@' . 'data_dir@')) {
447 $path = realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
448 . DIRECTORY_SEPARATOR . 'data');
449 }
450 self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
451 }
452
453 if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
454 // known TLD, invalid domain name
455 return false;
456 }
457
458 // unknown TLD
459 if (!strpos($result, '.')) {
460 // fallback to checking that domain "has at least two dots"
461 if (2 > ($count = count($domainParts))) {
462 return false;
463 }
464 return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
465 }
466 return $result;
467 }
468
469 /**
470 * Recursive helper method for {@link getRegisteredDomain()}
471 *
472 * @param array remaining domain parts
473 * @param mixed node in {@link HTTP_Request2_CookieJar::$psl} to check
474 * @return string|null concatenated domain parts, null in case of error
475 */
476 protected static function checkDomainsList(array $domainParts, $listNode)
477 {
478 $sub = array_pop($domainParts);
479 $result = null;
480
481 if (!is_array($listNode) || is_null($sub)
482 || array_key_exists('!' . $sub, $listNode)
483 ) {
484 return $sub;
485
486 } elseif (array_key_exists($sub, $listNode)) {
487 $result = self::checkDomainsList($domainParts, $listNode[$sub]);
488
489 } elseif (array_key_exists('*', $listNode)) {
490 $result = self::checkDomainsList($domainParts, $listNode['*']);
491
492 } else {
493 return $sub;
494 }
495
496 return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
497 }
498 }
499 ?>