Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * Class representing a HTTP response
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: Response.php 309921 2011-04-03 16:43:02Z avb $
41
 * @link       http://pear.php.net/package/HTTP_Request2
42
 */
43
 
44
/**
45
 * Exception class for HTTP_Request2 package
46
 */
47
require_once 'HTTP/Request2/Exception.php';
48
 
49
/**
50
 * Class representing a HTTP response
51
 *
52
 * The class is designed to be used in "streaming" scenario, building the
53
 * response as it is being received:
54
 * <code>
55
 * $statusLine = read_status_line();
56
 * $response = new HTTP_Request2_Response($statusLine);
57
 * do {
58
 *     $headerLine = read_header_line();
59
 *     $response->parseHeaderLine($headerLine);
60
 * } while ($headerLine != '');
61
 *
62
 * while ($chunk = read_body()) {
63
 *     $response->appendBody($chunk);
64
 * }
65
 *
66
 * var_dump($response->getHeader(), $response->getCookies(), $response->getBody());
67
 * </code>
68
 *
69
 *
70
 * @category   HTTP
71
 * @package    HTTP_Request2
72
 * @author     Alexey Borzov <avb@php.net>
73
 * @version    Release: 2.0.0RC1
74
 * @link       http://tools.ietf.org/html/rfc2616#section-6
75
 */
76
class HTTP_Request2_Response
77
{
78
   /**
79
    * HTTP protocol version (e.g. 1.0, 1.1)
80
    * @var  string
81
    */
82
    protected $version;
83
 
84
   /**
85
    * Status code
86
    * @var  integer
87
    * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
88
    */
89
    protected $code;
90
 
91
   /**
92
    * Reason phrase
93
    * @var  string
94
    * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
95
    */
96
    protected $reasonPhrase;
97
 
98
   /**
99
    * Effective URL (may be different from original request URL in case of redirects)
100
    * @var  string
101
    */
102
    protected $effectiveUrl;
103
 
104
   /**
105
    * Associative array of response headers
106
    * @var  array
107
    */
108
    protected $headers = array();
109
 
110
   /**
111
    * Cookies set in the response
112
    * @var  array
113
    */
114
    protected $cookies = array();
115
 
116
   /**
117
    * Name of last header processed by parseHederLine()
118
    *
119
    * Used to handle the headers that span multiple lines
120
    *
121
    * @var  string
122
    */
123
    protected $lastHeader = null;
124
 
125
   /**
126
    * Response body
127
    * @var  string
128
    */
129
    protected $body = '';
130
 
131
   /**
132
    * Whether the body is still encoded by Content-Encoding
133
    *
134
    * cURL provides the decoded body to the callback; if we are reading from
135
    * socket the body is still gzipped / deflated
136
    *
137
    * @var  bool
138
    */
139
    protected $bodyEncoded;
140
 
141
   /**
142
    * Associative array of HTTP status code / reason phrase.
143
    *
144
    * @var  array
145
    * @link http://tools.ietf.org/html/rfc2616#section-10
146
    */
147
    protected static $phrases = array(
148
 
149
        // 1xx: Informational - Request received, continuing process
150
        100 => 'Continue',
151
        101 => 'Switching Protocols',
152
 
153
        // 2xx: Success - The action was successfully received, understood and
154
        // accepted
155
        200 => 'OK',
156
        201 => 'Created',
157
        202 => 'Accepted',
158
        203 => 'Non-Authoritative Information',
159
        204 => 'No Content',
160
        205 => 'Reset Content',
161
        206 => 'Partial Content',
162
 
163
        // 3xx: Redirection - Further action must be taken in order to complete
164
        // the request
165
        300 => 'Multiple Choices',
166
        301 => 'Moved Permanently',
167
        302 => 'Found',  // 1.1
168
        303 => 'See Other',
169
        304 => 'Not Modified',
170
        305 => 'Use Proxy',
171
        307 => 'Temporary Redirect',
172
 
173
        // 4xx: Client Error - The request contains bad syntax or cannot be
174
        // fulfilled
175
        400 => 'Bad Request',
176
        401 => 'Unauthorized',
177
        402 => 'Payment Required',
178
        403 => 'Forbidden',
179
        404 => 'Not Found',
180
        405 => 'Method Not Allowed',
181
        406 => 'Not Acceptable',
182
        407 => 'Proxy Authentication Required',
183
        408 => 'Request Timeout',
184
        409 => 'Conflict',
185
        410 => 'Gone',
186
        411 => 'Length Required',
187
        412 => 'Precondition Failed',
188
        413 => 'Request Entity Too Large',
189
        414 => 'Request-URI Too Long',
190
        415 => 'Unsupported Media Type',
191
        416 => 'Requested Range Not Satisfiable',
192
        417 => 'Expectation Failed',
193
 
194
        // 5xx: Server Error - The server failed to fulfill an apparently
195
        // valid request
196
        500 => 'Internal Server Error',
197
        501 => 'Not Implemented',
198
        502 => 'Bad Gateway',
199
        503 => 'Service Unavailable',
200
        504 => 'Gateway Timeout',
201
        505 => 'HTTP Version Not Supported',
202
        509 => 'Bandwidth Limit Exceeded',
203
 
204
    );
205
 
206
   /**
207
    * Constructor, parses the response status line
208
    *
209
    * @param    string Response status line (e.g. "HTTP/1.1 200 OK")
210
    * @param    bool   Whether body is still encoded by Content-Encoding
211
    * @param    string Effective URL of the response
212
    * @throws   HTTP_Request2_MessageException if status line is invalid according to spec
213
    */
214
    public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)
215
    {
216
        if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {
217
            throw new HTTP_Request2_MessageException(
218
                "Malformed response: {$statusLine}",
219
                HTTP_Request2_Exception::MALFORMED_RESPONSE
220
            );
221
        }
222
        $this->version = $m[1];
223
        $this->code    = intval($m[2]);
224
        if (!empty($m[3])) {
225
            $this->reasonPhrase = trim($m[3]);
226
        } elseif (!empty(self::$phrases[$this->code])) {
227
            $this->reasonPhrase = self::$phrases[$this->code];
228
        }
229
        $this->bodyEncoded  = (bool)$bodyEncoded;
230
        $this->effectiveUrl = (string)$effectiveUrl;
231
    }
232
 
233
   /**
234
    * Parses the line from HTTP response filling $headers array
235
    *
236
    * The method should be called after reading the line from socket or receiving
237
    * it into cURL callback. Passing an empty string here indicates the end of
238
    * response headers and triggers additional processing, so be sure to pass an
239
    * empty string in the end.
240
    *
241
    * @param    string  Line from HTTP response
242
    */
243
    public function parseHeaderLine($headerLine)
244
    {
245
        $headerLine = trim($headerLine, "\r\n");
246
 
247
        // empty string signals the end of headers, process the received ones
248
        if ('' == $headerLine) {
249
            if (!empty($this->headers['set-cookie'])) {
250
                $cookies = is_array($this->headers['set-cookie'])?
251
                           $this->headers['set-cookie']:
252
                           array($this->headers['set-cookie']);
253
                foreach ($cookies as $cookieString) {
254
                    $this->parseCookie($cookieString);
255
                }
256
                unset($this->headers['set-cookie']);
257
            }
258
            foreach (array_keys($this->headers) as $k) {
259
                if (is_array($this->headers[$k])) {
260
                    $this->headers[$k] = implode(', ', $this->headers[$k]);
261
                }
262
            }
263
 
264
        // string of the form header-name: header value
265
        } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {
266
            $name  = strtolower($m[1]);
267
            $value = trim($m[2]);
268
            if (empty($this->headers[$name])) {
269
                $this->headers[$name] = $value;
270
            } else {
271
                if (!is_array($this->headers[$name])) {
272
                    $this->headers[$name] = array($this->headers[$name]);
273
                }
274
                $this->headers[$name][] = $value;
275
            }
276
            $this->lastHeader = $name;
277
 
278
        // continuation of a previous header
279
        } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {
280
            if (!is_array($this->headers[$this->lastHeader])) {
281
                $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);
282
            } else {
283
                $key = count($this->headers[$this->lastHeader]) - 1;
284
                $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);
285
            }
286
        }
287
    }
288
 
289
   /**
290
    * Parses a Set-Cookie header to fill $cookies array
291
    *
292
    * @param    string    value of Set-Cookie header
293
    * @link     http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
294
    */
295
    protected function parseCookie($cookieString)
296
    {
297
        $cookie = array(
298
            'expires' => null,
299
            'domain'  => null,
300
            'path'    => null,
301
            'secure'  => false
302
        );
303
 
304
        // Only a name=value pair
305
        if (!strpos($cookieString, ';')) {
306
            $pos = strpos($cookieString, '=');
307
            $cookie['name']  = trim(substr($cookieString, 0, $pos));
308
            $cookie['value'] = trim(substr($cookieString, $pos + 1));
309
 
310
        // Some optional parameters are supplied
311
        } else {
312
            $elements = explode(';', $cookieString);
313
            $pos = strpos($elements[0], '=');
314
            $cookie['name']  = trim(substr($elements[0], 0, $pos));
315
            $cookie['value'] = trim(substr($elements[0], $pos + 1));
316
 
317
            for ($i = 1; $i < count($elements); $i++) {
318
                if (false === strpos($elements[$i], '=')) {
319
                    $elName  = trim($elements[$i]);
320
                    $elValue = null;
321
                } else {
322
                    list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
323
                }
324
                $elName = strtolower($elName);
325
                if ('secure' == $elName) {
326
                    $cookie['secure'] = true;
327
                } elseif ('expires' == $elName) {
328
                    $cookie['expires'] = str_replace('"', '', $elValue);
329
                } elseif ('path' == $elName || 'domain' == $elName) {
330
                    $cookie[$elName] = urldecode($elValue);
331
                } else {
332
                    $cookie[$elName] = $elValue;
333
                }
334
            }
335
        }
336
        $this->cookies[] = $cookie;
337
    }
338
 
339
   /**
340
    * Appends a string to the response body
341
    * @param    string
342
    */
343
    public function appendBody($bodyChunk)
344
    {
345
        $this->body .= $bodyChunk;
346
    }
347
 
348
   /**
349
    * Returns the effective URL of the response
350
    *
351
    * This may be different from the request URL if redirects were followed.
352
    *
353
    * @return string
354
    * @link   http://pear.php.net/bugs/bug.php?id=18412
355
    */
356
    public function getEffectiveUrl()
357
    {
358
        return $this->effectiveUrl;
359
    }
360
 
361
   /**
362
    * Returns the status code
363
    * @return   integer
364
    */
365
    public function getStatus()
366
    {
367
        return $this->code;
368
    }
369
 
370
   /**
371
    * Returns the reason phrase
372
    * @return   string
373
    */
374
    public function getReasonPhrase()
375
    {
376
        return $this->reasonPhrase;
377
    }
378
 
379
   /**
380
    * Whether response is a redirect that can be automatically handled by HTTP_Request2
381
    * @return   bool
382
    */
383
    public function isRedirect()
384
    {
385
        return in_array($this->code, array(300, 301, 302, 303, 307))
386
               && isset($this->headers['location']);
387
    }
388
 
389
   /**
390
    * Returns either the named header or all response headers
391
    *
392
    * @param    string          Name of header to return
393
    * @return   string|array    Value of $headerName header (null if header is
394
    *                           not present), array of all response headers if
395
    *                           $headerName is null
396
    */
397
    public function getHeader($headerName = null)
398
    {
399
        if (null === $headerName) {
400
            return $this->headers;
401
        } else {
402
            $headerName = strtolower($headerName);
403
            return isset($this->headers[$headerName])? $this->headers[$headerName]: null;
404
        }
405
    }
406
 
407
   /**
408
    * Returns cookies set in response
409
    *
410
    * @return   array
411
    */
412
    public function getCookies()
413
    {
414
        return $this->cookies;
415
    }
416
 
417
   /**
418
    * Returns the body of the response
419
    *
420
    * @return   string
421
    * @throws   HTTP_Request2_Exception if body cannot be decoded
422
    */
423
    public function getBody()
424
    {
425
        if (0 == strlen($this->body) || !$this->bodyEncoded ||
426
            !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))
427
        ) {
428
            return $this->body;
429
 
430
        } else {
431
            if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
432
                $oldEncoding = mb_internal_encoding();
433
                mb_internal_encoding('iso-8859-1');
434
            }
435
 
436
            try {
437
                switch (strtolower($this->getHeader('content-encoding'))) {
438
                    case 'gzip':
439
                        $decoded = self::decodeGzip($this->body);
440
                        break;
441
                    case 'deflate':
442
                        $decoded = self::decodeDeflate($this->body);
443
                }
444
            } catch (Exception $e) {
445
            }
446
 
447
            if (!empty($oldEncoding)) {
448
                mb_internal_encoding($oldEncoding);
449
            }
450
            if (!empty($e)) {
451
                throw $e;
452
            }
453
            return $decoded;
454
        }
455
    }
456
 
457
   /**
458
    * Get the HTTP version of the response
459
    *
460
    * @return   string
461
    */
462
    public function getVersion()
463
    {
464
        return $this->version;
465
    }
466
 
467
   /**
468
    * Decodes the message-body encoded by gzip
469
    *
470
    * The real decoding work is done by gzinflate() built-in function, this
471
    * method only parses the header and checks data for compliance with
472
    * RFC 1952
473
    *
474
    * @param    string  gzip-encoded data
475
    * @return   string  decoded data
476
    * @throws   HTTP_Request2_LogicException
477
    * @throws   HTTP_Request2_MessageException
478
    * @link     http://tools.ietf.org/html/rfc1952
479
    */
480
    public static function decodeGzip($data)
481
    {
482
        $length = strlen($data);
483
        // If it doesn't look like gzip-encoded data, don't bother
484
        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
485
            return $data;
486
        }
487
        if (!function_exists('gzinflate')) {
488
            throw new HTTP_Request2_LogicException(
489
                'Unable to decode body: gzip extension not available',
490
                HTTP_Request2_Exception::MISCONFIGURATION
491
            );
492
        }
493
        $method = ord(substr($data, 2, 1));
494
        if (8 != $method) {
495
            throw new HTTP_Request2_MessageException(
496
                'Error parsing gzip header: unknown compression method',
497
                HTTP_Request2_Exception::DECODE_ERROR
498
            );
499
        }
500
        $flags = ord(substr($data, 3, 1));
501
        if ($flags & 224) {
502
            throw new HTTP_Request2_MessageException(
503
                'Error parsing gzip header: reserved bits are set',
504
                HTTP_Request2_Exception::DECODE_ERROR
505
            );
506
        }
507
 
508
        // header is 10 bytes minimum. may be longer, though.
509
        $headerLength = 10;
510
        // extra fields, need to skip 'em
511
        if ($flags & 4) {
512
            if ($length - $headerLength - 2 < 8) {
513
                throw new HTTP_Request2_MessageException(
514
                    'Error parsing gzip header: data too short',
515
                    HTTP_Request2_Exception::DECODE_ERROR
516
                );
517
            }
518
            $extraLength = unpack('v', substr($data, 10, 2));
519
            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
520
                throw new HTTP_Request2_MessageException(
521
                    'Error parsing gzip header: data too short',
522
                    HTTP_Request2_Exception::DECODE_ERROR
523
                );
524
            }
525
            $headerLength += $extraLength[1] + 2;
526
        }
527
        // file name, need to skip that
528
        if ($flags & 8) {
529
            if ($length - $headerLength - 1 < 8) {
530
                throw new HTTP_Request2_MessageException(
531
                    'Error parsing gzip header: data too short',
532
                    HTTP_Request2_Exception::DECODE_ERROR
533
                );
534
            }
535
            $filenameLength = strpos(substr($data, $headerLength), chr(0));
536
            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
537
                throw new HTTP_Request2_MessageException(
538
                    'Error parsing gzip header: data too short',
539
                    HTTP_Request2_Exception::DECODE_ERROR
540
                );
541
            }
542
            $headerLength += $filenameLength + 1;
543
        }
544
        // comment, need to skip that also
545
        if ($flags & 16) {
546
            if ($length - $headerLength - 1 < 8) {
547
                throw new HTTP_Request2_MessageException(
548
                    'Error parsing gzip header: data too short',
549
                    HTTP_Request2_Exception::DECODE_ERROR
550
                );
551
            }
552
            $commentLength = strpos(substr($data, $headerLength), chr(0));
553
            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
554
                throw new HTTP_Request2_MessageException(
555
                    'Error parsing gzip header: data too short',
556
                    HTTP_Request2_Exception::DECODE_ERROR
557
                );
558
            }
559
            $headerLength += $commentLength + 1;
560
        }
561
        // have a CRC for header. let's check
562
        if ($flags & 2) {
563
            if ($length - $headerLength - 2 < 8) {
564
                throw new HTTP_Request2_MessageException(
565
                    'Error parsing gzip header: data too short',
566
                    HTTP_Request2_Exception::DECODE_ERROR
567
                );
568
            }
569
            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
570
            $crcStored = unpack('v', substr($data, $headerLength, 2));
571
            if ($crcReal != $crcStored[1]) {
572
                throw new HTTP_Request2_MessageException(
573
                    'Header CRC check failed',
574
                    HTTP_Request2_Exception::DECODE_ERROR
575
                );
576
            }
577
            $headerLength += 2;
578
        }
579
        // unpacked data CRC and size at the end of encoded data
580
        $tmp = unpack('V2', substr($data, -8));
581
        $dataCrc  = $tmp[1];
582
        $dataSize = $tmp[2];
583
 
584
        // finally, call the gzinflate() function
585
        // don't pass $dataSize to gzinflate, see bugs #13135, #14370
586
        $unpacked = gzinflate(substr($data, $headerLength, -8));
587
        if (false === $unpacked) {
588
            throw new HTTP_Request2_MessageException(
589
                'gzinflate() call failed',
590
                HTTP_Request2_Exception::DECODE_ERROR
591
            );
592
        } elseif ($dataSize != strlen($unpacked)) {
593
            throw new HTTP_Request2_MessageException(
594
                'Data size check failed',
595
                HTTP_Request2_Exception::DECODE_ERROR
596
            );
597
        } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
598
            throw new HTTP_Request2_Exception(
599
                'Data CRC check failed',
600
                HTTP_Request2_Exception::DECODE_ERROR
601
            );
602
        }
603
        return $unpacked;
604
    }
605
 
606
   /**
607
    * Decodes the message-body encoded by deflate
608
    *
609
    * @param    string  deflate-encoded data
610
    * @return   string  decoded data
611
    * @throws   HTTP_Request2_LogicException
612
    */
613
    public static function decodeDeflate($data)
614
    {
615
        if (!function_exists('gzuncompress')) {
616
            throw new HTTP_Request2_LogicException(
617
                'Unable to decode body: gzip extension not available',
618
                HTTP_Request2_Exception::MISCONFIGURATION
619
            );
620
        }
621
        // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,
622
        // while many applications send raw deflate stream from RFC 1951.
623
        // We should check for presence of zlib header and use gzuncompress() or
624
        // gzinflate() as needed. See bug #15305
625
        $header = unpack('n', substr($data, 0, 2));
626
        return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);
627
    }
628
}
629
?>