974aff2809138957b9a7af3859c3434dc7eb74be
[Packages/TYPO3.CMS.git] / typo3 / contrib / pear / HTTP / Request2 / Response.php
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 ?>