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 317591 2011-10-01 08:37:49Z 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.0
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 : * Returns the default reason phrase for the given code or all reason phrases
208 : *
209 : * @param int $code Response code
210 : * @return string|array|null Default reason phrase for $code if $code is given
211 : * (null if no phrase is available), array of all
212 : * reason phrases if $code is null
213 : * @link http://pear.php.net/bugs/18716
214 : */
215 : public static function getDefaultReasonPhrase($code = null)
216 : {
217 0 : if (null === $code) {
218 0 : return self::$phrases;
219 : } else {
220 0 : return isset(self::$phrases[$code]) ? self::$phrases[$code] : null;
221 : }
222 : }
223 :
224 : /**
225 : * Constructor, parses the response status line
226 : *
227 : * @param string Response status line (e.g. "HTTP/1.1 200 OK")
228 : * @param bool Whether body is still encoded by Content-Encoding
229 : * @param string Effective URL of the response
230 : * @throws HTTP_Request2_MessageException if status line is invalid according to spec
231 : */
232 : public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)
233 : {
234 10 : if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {
235 0 : throw new HTTP_Request2_MessageException(
236 0 : "Malformed response: {$statusLine}",
237 : HTTP_Request2_Exception::MALFORMED_RESPONSE
238 0 : );
239 : }
240 10 : $this->version = $m[1];
241 10 : $this->code = intval($m[2]);
242 10 : $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code);
243 10 : $this->bodyEncoded = (bool)$bodyEncoded;
244 10 : $this->effectiveUrl = (string)$effectiveUrl;
245 10 : }
246 :
247 : /**
248 : * Parses the line from HTTP response filling $headers array
249 : *
250 : * The method should be called after reading the line from socket or receiving
251 : * it into cURL callback. Passing an empty string here indicates the end of
252 : * response headers and triggers additional processing, so be sure to pass an
253 : * empty string in the end.
254 : *
255 : * @param string Line from HTTP response
256 : */
257 : public function parseHeaderLine($headerLine)
258 : {
259 10 : $headerLine = trim($headerLine, "\r\n");
260 :
261 : // empty string signals the end of headers, process the received ones
262 10 : if ('' == $headerLine) {
263 10 : if (!empty($this->headers['set-cookie'])) {
264 10 : $cookies = is_array($this->headers['set-cookie'])?
265 10 : $this->headers['set-cookie']:
266 10 : array($this->headers['set-cookie']);
267 10 : foreach ($cookies as $cookieString) {
268 10 : $this->parseCookie($cookieString);
269 10 : }
270 10 : unset($this->headers['set-cookie']);
271 10 : }
272 10 : foreach (array_keys($this->headers) as $k) {
273 10 : if (is_array($this->headers[$k])) {
274 0 : $this->headers[$k] = implode(', ', $this->headers[$k]);
275 0 : }
276 10 : }
277 :
278 : // string of the form header-name: header value
279 10 : } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {
280 10 : $name = strtolower($m[1]);
281 10 : $value = trim($m[2]);
282 10 : if (empty($this->headers[$name])) {
283 10 : $this->headers[$name] = $value;
284 10 : } else {
285 0 : if (!is_array($this->headers[$name])) {
286 0 : $this->headers[$name] = array($this->headers[$name]);
287 0 : }
288 0 : $this->headers[$name][] = $value;
289 : }
290 10 : $this->lastHeader = $name;
291 :
292 : // continuation of a previous header
293 10 : } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {
294 0 : if (!is_array($this->headers[$this->lastHeader])) {
295 0 : $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);
296 0 : } else {
297 0 : $key = count($this->headers[$this->lastHeader]) - 1;
298 0 : $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);
299 : }
300 0 : }
301 10 : }
302 :
303 : /**
304 : * Parses a Set-Cookie header to fill $cookies array
305 : *
306 : * @param string value of Set-Cookie header
307 : * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
308 : */
309 : protected function parseCookie($cookieString)
310 : {
311 : $cookie = array(
312 10 : 'expires' => null,
313 10 : 'domain' => null,
314 10 : 'path' => null,
315 : 'secure' => false
316 10 : );
317 :
318 : // Only a name=value pair
319 10 : if (!strpos($cookieString, ';')) {
320 0 : $pos = strpos($cookieString, '=');
321 0 : $cookie['name'] = trim(substr($cookieString, 0, $pos));
322 0 : $cookie['value'] = trim(substr($cookieString, $pos + 1));
323 :
324 : // Some optional parameters are supplied
325 0 : } else {
326 10 : $elements = explode(';', $cookieString);
327 10 : $pos = strpos($elements[0], '=');
328 10 : $cookie['name'] = trim(substr($elements[0], 0, $pos));
329 10 : $cookie['value'] = trim(substr($elements[0], $pos + 1));
330 :
331 10 : for ($i = 1; $i < count($elements); $i++) {
332 10 : if (false === strpos($elements[$i], '=')) {
333 10 : $elName = trim($elements[$i]);
334 10 : $elValue = null;
335 10 : } else {
336 10 : list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
337 : }
338 10 : $elName = strtolower($elName);
339 10 : if ('secure' == $elName) {
340 0 : $cookie['secure'] = true;
341 10 : } elseif ('expires' == $elName) {
342 0 : $cookie['expires'] = str_replace('"', '', $elValue);
343 10 : } elseif ('path' == $elName || 'domain' == $elName) {
344 10 : $cookie[$elName] = urldecode($elValue);
345 10 : } else {
346 10 : $cookie[$elName] = $elValue;
347 : }
348 10 : }
349 : }
350 10 : $this->cookies[] = $cookie;
351 10 : }
352 :
353 : /**
354 : * Appends a string to the response body
355 : * @param string
356 : */
357 : public function appendBody($bodyChunk)
358 : {
359 10 : $this->body .= $bodyChunk;
360 10 : }
361 :
362 : /**
363 : * Returns the effective URL of the response
364 : *
365 : * This may be different from the request URL if redirects were followed.
366 : *
367 : * @return string
368 : * @link http://pear.php.net/bugs/bug.php?id=18412
369 : */
370 : public function getEffectiveUrl()
371 : {
372 0 : return $this->effectiveUrl;
373 : }
374 :
375 : /**
376 : * Returns the status code
377 : * @return integer
378 : */
379 : public function getStatus()
380 : {
381 10 : return $this->code;
382 : }
383 :
384 : /**
385 : * Returns the reason phrase
386 : * @return string
387 : */
388 : public function getReasonPhrase()
389 : {
390 0 : return $this->reasonPhrase;
391 : }
392 :
393 : /**
394 : * Whether response is a redirect that can be automatically handled by HTTP_Request2
395 : * @return bool
396 : */
397 : public function isRedirect()
398 : {
399 0 : return in_array($this->code, array(300, 301, 302, 303, 307))
400 0 : && isset($this->headers['location']);
401 : }
402 :
403 : /**
404 : * Returns either the named header or all response headers
405 : *
406 : * @param string Name of header to return
407 : * @return string|array Value of $headerName header (null if header is
408 : * not present), array of all response headers if
409 : * $headerName is null
410 : */
411 : public function getHeader($headerName = null)
412 : {
413 10 : if (null === $headerName) {
414 0 : return $this->headers;
415 : } else {
416 10 : $headerName = strtolower($headerName);
417 10 : return isset($this->headers[$headerName])? $this->headers[$headerName]: null;
418 : }
419 : }
420 :
421 : /**
422 : * Returns cookies set in response
423 : *
424 : * @return array
425 : */
426 : public function getCookies()
427 : {
428 0 : return $this->cookies;
429 : }
430 :
431 : /**
432 : * Returns the body of the response
433 : *
434 : * @return string
435 : * @throws HTTP_Request2_Exception if body cannot be decoded
436 : */
437 : public function getBody()
438 : {
439 10 : if (0 == strlen($this->body) || !$this->bodyEncoded ||
440 10 : !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))
441 10 : ) {
442 10 : return $this->body;
443 :
444 : } else {
445 0 : if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
446 0 : $oldEncoding = mb_internal_encoding();
447 0 : mb_internal_encoding('iso-8859-1');
448 0 : }
449 :
450 : try {
451 0 : switch (strtolower($this->getHeader('content-encoding'))) {
452 0 : case 'gzip':
453 0 : $decoded = self::decodeGzip($this->body);
454 0 : break;
455 0 : case 'deflate':
456 0 : $decoded = self::decodeDeflate($this->body);
457 0 : }
458 0 : } catch (Exception $e) {
459 : }
460 :
461 0 : if (!empty($oldEncoding)) {
462 0 : mb_internal_encoding($oldEncoding);
463 0 : }
464 0 : if (!empty($e)) {
465 0 : throw $e;
466 : }
467 0 : return $decoded;
468 : }
469 : }
470 :
471 : /**
472 : * Get the HTTP version of the response
473 : *
474 : * @return string
475 : */
476 : public function getVersion()
477 : {
478 0 : return $this->version;
479 : }
480 :
481 : /**
482 : * Decodes the message-body encoded by gzip
483 : *
484 : * The real decoding work is done by gzinflate() built-in function, this
485 : * method only parses the header and checks data for compliance with
486 : * RFC 1952
487 : *
488 : * @param string gzip-encoded data
489 : * @return string decoded data
490 : * @throws HTTP_Request2_LogicException
491 : * @throws HTTP_Request2_MessageException
492 : * @link http://tools.ietf.org/html/rfc1952
493 : */
494 : public static function decodeGzip($data)
495 : {
496 0 : $length = strlen($data);
497 : // If it doesn't look like gzip-encoded data, don't bother
498 0 : if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
499 0 : return $data;
500 : }
501 0 : if (!function_exists('gzinflate')) {
502 0 : throw new HTTP_Request2_LogicException(
503 0 : 'Unable to decode body: gzip extension not available',
504 : HTTP_Request2_Exception::MISCONFIGURATION
505 0 : );
506 : }
507 0 : $method = ord(substr($data, 2, 1));
508 0 : if (8 != $method) {
509 0 : throw new HTTP_Request2_MessageException(
510 0 : 'Error parsing gzip header: unknown compression method',
511 : HTTP_Request2_Exception::DECODE_ERROR
512 0 : );
513 : }
514 0 : $flags = ord(substr($data, 3, 1));
515 0 : if ($flags & 224) {
516 0 : throw new HTTP_Request2_MessageException(
517 0 : 'Error parsing gzip header: reserved bits are set',
518 : HTTP_Request2_Exception::DECODE_ERROR
519 0 : );
520 : }
521 :
522 : // header is 10 bytes minimum. may be longer, though.
523 0 : $headerLength = 10;
524 : // extra fields, need to skip 'em
525 0 : if ($flags & 4) {
526 0 : if ($length - $headerLength - 2 < 8) {
527 0 : throw new HTTP_Request2_MessageException(
528 0 : 'Error parsing gzip header: data too short',
529 : HTTP_Request2_Exception::DECODE_ERROR
530 0 : );
531 : }
532 0 : $extraLength = unpack('v', substr($data, 10, 2));
533 0 : if ($length - $headerLength - 2 - $extraLength[1] < 8) {
534 0 : throw new HTTP_Request2_MessageException(
535 0 : 'Error parsing gzip header: data too short',
536 : HTTP_Request2_Exception::DECODE_ERROR
537 0 : );
538 : }
539 0 : $headerLength += $extraLength[1] + 2;
540 0 : }
541 : // file name, need to skip that
542 0 : if ($flags & 8) {
543 0 : if ($length - $headerLength - 1 < 8) {
544 0 : throw new HTTP_Request2_MessageException(
545 0 : 'Error parsing gzip header: data too short',
546 : HTTP_Request2_Exception::DECODE_ERROR
547 0 : );
548 : }
549 0 : $filenameLength = strpos(substr($data, $headerLength), chr(0));
550 0 : if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
551 0 : throw new HTTP_Request2_MessageException(
552 0 : 'Error parsing gzip header: data too short',
553 : HTTP_Request2_Exception::DECODE_ERROR
554 0 : );
555 : }
556 0 : $headerLength += $filenameLength + 1;
557 0 : }
558 : // comment, need to skip that also
559 0 : if ($flags & 16) {
560 0 : if ($length - $headerLength - 1 < 8) {
561 0 : throw new HTTP_Request2_MessageException(
562 0 : 'Error parsing gzip header: data too short',
563 : HTTP_Request2_Exception::DECODE_ERROR
564 0 : );
565 : }
566 0 : $commentLength = strpos(substr($data, $headerLength), chr(0));
567 0 : if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
568 0 : throw new HTTP_Request2_MessageException(
569 0 : 'Error parsing gzip header: data too short',
570 : HTTP_Request2_Exception::DECODE_ERROR
571 0 : );
572 : }
573 0 : $headerLength += $commentLength + 1;
574 0 : }
575 : // have a CRC for header. let's check
576 0 : if ($flags & 2) {
577 0 : if ($length - $headerLength - 2 < 8) {
578 0 : throw new HTTP_Request2_MessageException(
579 0 : 'Error parsing gzip header: data too short',
580 : HTTP_Request2_Exception::DECODE_ERROR
581 0 : );
582 : }
583 0 : $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));
584 0 : $crcStored = unpack('v', substr($data, $headerLength, 2));
585 0 : if ($crcReal != $crcStored[1]) {
586 0 : throw new HTTP_Request2_MessageException(
587 0 : 'Header CRC check failed',
588 : HTTP_Request2_Exception::DECODE_ERROR
589 0 : );
590 : }
591 0 : $headerLength += 2;
592 0 : }
593 : // unpacked data CRC and size at the end of encoded data
594 0 : $tmp = unpack('V2', substr($data, -8));
595 0 : $dataCrc = $tmp[1];
596 0 : $dataSize = $tmp[2];
597 :
598 : // finally, call the gzinflate() function
599 : // don't pass $dataSize to gzinflate, see bugs #13135, #14370
600 0 : $unpacked = gzinflate(substr($data, $headerLength, -8));
601 0 : if (false === $unpacked) {
602 0 : throw new HTTP_Request2_MessageException(
603 0 : 'gzinflate() call failed',
604 : HTTP_Request2_Exception::DECODE_ERROR
605 0 : );
606 0 : } elseif ($dataSize != strlen($unpacked)) {
607 0 : throw new HTTP_Request2_MessageException(
608 0 : 'Data size check failed',
609 : HTTP_Request2_Exception::DECODE_ERROR
610 0 : );
611 0 : } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
612 0 : throw new HTTP_Request2_Exception(
613 0 : 'Data CRC check failed',
614 : HTTP_Request2_Exception::DECODE_ERROR
615 0 : );
616 : }
617 0 : return $unpacked;
618 : }
619 :
620 : /**
621 : * Decodes the message-body encoded by deflate
622 : *
623 : * @param string deflate-encoded data
624 : * @return string decoded data
625 : * @throws HTTP_Request2_LogicException
626 : */
627 : public static function decodeDeflate($data)
628 : {
629 0 : if (!function_exists('gzuncompress')) {
630 0 : throw new HTTP_Request2_LogicException(
631 0 : 'Unable to decode body: gzip extension not available',
632 : HTTP_Request2_Exception::MISCONFIGURATION
633 0 : );
634 : }
635 : // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,
636 : // while many applications send raw deflate stream from RFC 1951.
637 : // We should check for presence of zlib header and use gzuncompress() or
638 : // gzinflate() as needed. See bug #15305
639 0 : $header = unpack('n', substr($data, 0, 2));
640 0 : return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);
641 : }
642 : }
|