Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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 = '/var/www/public_html/' . 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
?>