|
1 <?php |
|
2 |
|
3 /** |
|
4 * Zend Framework |
|
5 * |
|
6 * LICENSE |
|
7 * |
|
8 * This source file is subject to the new BSD license that is bundled |
|
9 * with this package in the file LICENSE.txt. |
|
10 * It is also available through the world-wide-web at this URL: |
|
11 * http://framework.zend.com/license/new-bsd |
|
12 * If you did not receive a copy of the license and are unable to |
|
13 * obtain it through the world-wide-web, please send an email |
|
14 * to license@zend.com so we can send you a copy immediately. |
|
15 * |
|
16 * @category Zend |
|
17 * @package Zend_Http |
|
18 * @subpackage Response |
|
19 * @version $Id: Response.php 22810 2010-08-08 10:29:09Z shahar $ |
|
20 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
21 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
22 */ |
|
23 |
|
24 /** |
|
25 * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It |
|
26 * includes easy access to all the response's different elemts, as well as some |
|
27 * convenience methods for parsing and validating HTTP responses. |
|
28 * |
|
29 * @package Zend_Http |
|
30 * @subpackage Response |
|
31 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
32 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
33 */ |
|
34 class Zend_Http_Response |
|
35 { |
|
36 /** |
|
37 * List of all known HTTP response codes - used by responseCodeAsText() to |
|
38 * translate numeric codes to messages. |
|
39 * |
|
40 * @var array |
|
41 */ |
|
42 protected static $messages = array( |
|
43 // Informational 1xx |
|
44 100 => 'Continue', |
|
45 101 => 'Switching Protocols', |
|
46 |
|
47 // Success 2xx |
|
48 200 => 'OK', |
|
49 201 => 'Created', |
|
50 202 => 'Accepted', |
|
51 203 => 'Non-Authoritative Information', |
|
52 204 => 'No Content', |
|
53 205 => 'Reset Content', |
|
54 206 => 'Partial Content', |
|
55 |
|
56 // Redirection 3xx |
|
57 300 => 'Multiple Choices', |
|
58 301 => 'Moved Permanently', |
|
59 302 => 'Found', // 1.1 |
|
60 303 => 'See Other', |
|
61 304 => 'Not Modified', |
|
62 305 => 'Use Proxy', |
|
63 // 306 is deprecated but reserved |
|
64 307 => 'Temporary Redirect', |
|
65 |
|
66 // Client Error 4xx |
|
67 400 => 'Bad Request', |
|
68 401 => 'Unauthorized', |
|
69 402 => 'Payment Required', |
|
70 403 => 'Forbidden', |
|
71 404 => 'Not Found', |
|
72 405 => 'Method Not Allowed', |
|
73 406 => 'Not Acceptable', |
|
74 407 => 'Proxy Authentication Required', |
|
75 408 => 'Request Timeout', |
|
76 409 => 'Conflict', |
|
77 410 => 'Gone', |
|
78 411 => 'Length Required', |
|
79 412 => 'Precondition Failed', |
|
80 413 => 'Request Entity Too Large', |
|
81 414 => 'Request-URI Too Long', |
|
82 415 => 'Unsupported Media Type', |
|
83 416 => 'Requested Range Not Satisfiable', |
|
84 417 => 'Expectation Failed', |
|
85 |
|
86 // Server Error 5xx |
|
87 500 => 'Internal Server Error', |
|
88 501 => 'Not Implemented', |
|
89 502 => 'Bad Gateway', |
|
90 503 => 'Service Unavailable', |
|
91 504 => 'Gateway Timeout', |
|
92 505 => 'HTTP Version Not Supported', |
|
93 509 => 'Bandwidth Limit Exceeded' |
|
94 ); |
|
95 |
|
96 /** |
|
97 * The HTTP version (1.0, 1.1) |
|
98 * |
|
99 * @var string |
|
100 */ |
|
101 protected $version; |
|
102 |
|
103 /** |
|
104 * The HTTP response code |
|
105 * |
|
106 * @var int |
|
107 */ |
|
108 protected $code; |
|
109 |
|
110 /** |
|
111 * The HTTP response code as string |
|
112 * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500) |
|
113 * |
|
114 * @var string |
|
115 */ |
|
116 protected $message; |
|
117 |
|
118 /** |
|
119 * The HTTP response headers array |
|
120 * |
|
121 * @var array |
|
122 */ |
|
123 protected $headers = array(); |
|
124 |
|
125 /** |
|
126 * The HTTP response body |
|
127 * |
|
128 * @var string |
|
129 */ |
|
130 protected $body; |
|
131 |
|
132 /** |
|
133 * HTTP response constructor |
|
134 * |
|
135 * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP |
|
136 * response string and create a new Zend_Http_Response object. |
|
137 * |
|
138 * NOTE: The constructor no longer accepts nulls or empty values for the code and |
|
139 * headers and will throw an exception if the passed values do not form a valid HTTP |
|
140 * responses. |
|
141 * |
|
142 * If no message is passed, the message will be guessed according to the response code. |
|
143 * |
|
144 * @param int $code Response code (200, 404, ...) |
|
145 * @param array $headers Headers array |
|
146 * @param string $body Response body |
|
147 * @param string $version HTTP version |
|
148 * @param string $message Response code as text |
|
149 * @throws Zend_Http_Exception |
|
150 */ |
|
151 public function __construct($code, array $headers, $body = null, $version = '1.1', $message = null) |
|
152 { |
|
153 // Make sure the response code is valid and set it |
|
154 if (self::responseCodeAsText($code) === null) { |
|
155 require_once 'Zend/Http/Exception.php'; |
|
156 throw new Zend_Http_Exception("{$code} is not a valid HTTP response code"); |
|
157 } |
|
158 |
|
159 $this->code = $code; |
|
160 |
|
161 foreach ($headers as $name => $value) { |
|
162 if (is_int($name)) { |
|
163 $header = explode(":", $value, 2); |
|
164 if (count($header) != 2) { |
|
165 require_once 'Zend/Http/Exception.php'; |
|
166 throw new Zend_Http_Exception("'{$value}' is not a valid HTTP header"); |
|
167 } |
|
168 |
|
169 $name = trim($header[0]); |
|
170 $value = trim($header[1]); |
|
171 } |
|
172 |
|
173 $this->headers[ucwords(strtolower($name))] = $value; |
|
174 } |
|
175 |
|
176 // Set the body |
|
177 $this->body = $body; |
|
178 |
|
179 // Set the HTTP version |
|
180 if (! preg_match('|^\d\.\d$|', $version)) { |
|
181 require_once 'Zend/Http/Exception.php'; |
|
182 throw new Zend_Http_Exception("Invalid HTTP response version: $version"); |
|
183 } |
|
184 |
|
185 $this->version = $version; |
|
186 |
|
187 // If we got the response message, set it. Else, set it according to |
|
188 // the response code |
|
189 if (is_string($message)) { |
|
190 $this->message = $message; |
|
191 } else { |
|
192 $this->message = self::responseCodeAsText($code); |
|
193 } |
|
194 } |
|
195 |
|
196 /** |
|
197 * Check whether the response is an error |
|
198 * |
|
199 * @return boolean |
|
200 */ |
|
201 public function isError() |
|
202 { |
|
203 $restype = floor($this->code / 100); |
|
204 if ($restype == 4 || $restype == 5) { |
|
205 return true; |
|
206 } |
|
207 |
|
208 return false; |
|
209 } |
|
210 |
|
211 /** |
|
212 * Check whether the response in successful |
|
213 * |
|
214 * @return boolean |
|
215 */ |
|
216 public function isSuccessful() |
|
217 { |
|
218 $restype = floor($this->code / 100); |
|
219 if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ??? |
|
220 return true; |
|
221 } |
|
222 |
|
223 return false; |
|
224 } |
|
225 |
|
226 /** |
|
227 * Check whether the response is a redirection |
|
228 * |
|
229 * @return boolean |
|
230 */ |
|
231 public function isRedirect() |
|
232 { |
|
233 $restype = floor($this->code / 100); |
|
234 if ($restype == 3) { |
|
235 return true; |
|
236 } |
|
237 |
|
238 return false; |
|
239 } |
|
240 |
|
241 /** |
|
242 * Get the response body as string |
|
243 * |
|
244 * This method returns the body of the HTTP response (the content), as it |
|
245 * should be in it's readable version - that is, after decoding it (if it |
|
246 * was decoded), deflating it (if it was gzip compressed), etc. |
|
247 * |
|
248 * If you want to get the raw body (as transfered on wire) use |
|
249 * $this->getRawBody() instead. |
|
250 * |
|
251 * @return string |
|
252 */ |
|
253 public function getBody() |
|
254 { |
|
255 $body = ''; |
|
256 |
|
257 // Decode the body if it was transfer-encoded |
|
258 switch (strtolower($this->getHeader('transfer-encoding'))) { |
|
259 |
|
260 // Handle chunked body |
|
261 case 'chunked': |
|
262 $body = self::decodeChunkedBody($this->body); |
|
263 break; |
|
264 |
|
265 // No transfer encoding, or unknown encoding extension: |
|
266 // return body as is |
|
267 default: |
|
268 $body = $this->body; |
|
269 break; |
|
270 } |
|
271 |
|
272 // Decode any content-encoding (gzip or deflate) if needed |
|
273 switch (strtolower($this->getHeader('content-encoding'))) { |
|
274 |
|
275 // Handle gzip encoding |
|
276 case 'gzip': |
|
277 $body = self::decodeGzip($body); |
|
278 break; |
|
279 |
|
280 // Handle deflate encoding |
|
281 case 'deflate': |
|
282 $body = self::decodeDeflate($body); |
|
283 break; |
|
284 |
|
285 default: |
|
286 break; |
|
287 } |
|
288 |
|
289 return $body; |
|
290 } |
|
291 |
|
292 /** |
|
293 * Get the raw response body (as transfered "on wire") as string |
|
294 * |
|
295 * If the body is encoded (with Transfer-Encoding, not content-encoding - |
|
296 * IE "chunked" body), gzip compressed, etc. it will not be decoded. |
|
297 * |
|
298 * @return string |
|
299 */ |
|
300 public function getRawBody() |
|
301 { |
|
302 return $this->body; |
|
303 } |
|
304 |
|
305 /** |
|
306 * Get the HTTP version of the response |
|
307 * |
|
308 * @return string |
|
309 */ |
|
310 public function getVersion() |
|
311 { |
|
312 return $this->version; |
|
313 } |
|
314 |
|
315 /** |
|
316 * Get the HTTP response status code |
|
317 * |
|
318 * @return int |
|
319 */ |
|
320 public function getStatus() |
|
321 { |
|
322 return $this->code; |
|
323 } |
|
324 |
|
325 /** |
|
326 * Return a message describing the HTTP response code |
|
327 * (Eg. "OK", "Not Found", "Moved Permanently") |
|
328 * |
|
329 * @return string |
|
330 */ |
|
331 public function getMessage() |
|
332 { |
|
333 return $this->message; |
|
334 } |
|
335 |
|
336 /** |
|
337 * Get the response headers |
|
338 * |
|
339 * @return array |
|
340 */ |
|
341 public function getHeaders() |
|
342 { |
|
343 return $this->headers; |
|
344 } |
|
345 |
|
346 /** |
|
347 * Get a specific header as string, or null if it is not set |
|
348 * |
|
349 * @param string$header |
|
350 * @return string|array|null |
|
351 */ |
|
352 public function getHeader($header) |
|
353 { |
|
354 $header = ucwords(strtolower($header)); |
|
355 if (! is_string($header) || ! isset($this->headers[$header])) return null; |
|
356 |
|
357 return $this->headers[$header]; |
|
358 } |
|
359 |
|
360 /** |
|
361 * Get all headers as string |
|
362 * |
|
363 * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK") |
|
364 * @param string $br Line breaks (eg. "\n", "\r\n", "<br />") |
|
365 * @return string |
|
366 */ |
|
367 public function getHeadersAsString($status_line = true, $br = "\n") |
|
368 { |
|
369 $str = ''; |
|
370 |
|
371 if ($status_line) { |
|
372 $str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}"; |
|
373 } |
|
374 |
|
375 // Iterate over the headers and stringify them |
|
376 foreach ($this->headers as $name => $value) |
|
377 { |
|
378 if (is_string($value)) |
|
379 $str .= "{$name}: {$value}{$br}"; |
|
380 |
|
381 elseif (is_array($value)) { |
|
382 foreach ($value as $subval) { |
|
383 $str .= "{$name}: {$subval}{$br}"; |
|
384 } |
|
385 } |
|
386 } |
|
387 |
|
388 return $str; |
|
389 } |
|
390 |
|
391 /** |
|
392 * Get the entire response as string |
|
393 * |
|
394 * @param string $br Line breaks (eg. "\n", "\r\n", "<br />") |
|
395 * @return string |
|
396 */ |
|
397 public function asString($br = "\n") |
|
398 { |
|
399 return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody(); |
|
400 } |
|
401 |
|
402 /** |
|
403 * Implements magic __toString() |
|
404 * |
|
405 * @return string |
|
406 */ |
|
407 public function __toString() |
|
408 { |
|
409 return $this->asString(); |
|
410 } |
|
411 |
|
412 /** |
|
413 * A convenience function that returns a text representation of |
|
414 * HTTP response codes. Returns 'Unknown' for unknown codes. |
|
415 * Returns array of all codes, if $code is not specified. |
|
416 * |
|
417 * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown') |
|
418 * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference |
|
419 * |
|
420 * @param int $code HTTP response code |
|
421 * @param boolean $http11 Use HTTP version 1.1 |
|
422 * @return string |
|
423 */ |
|
424 public static function responseCodeAsText($code = null, $http11 = true) |
|
425 { |
|
426 $messages = self::$messages; |
|
427 if (! $http11) $messages[302] = 'Moved Temporarily'; |
|
428 |
|
429 if ($code === null) { |
|
430 return $messages; |
|
431 } elseif (isset($messages[$code])) { |
|
432 return $messages[$code]; |
|
433 } else { |
|
434 return 'Unknown'; |
|
435 } |
|
436 } |
|
437 |
|
438 /** |
|
439 * Extract the response code from a response string |
|
440 * |
|
441 * @param string $response_str |
|
442 * @return int |
|
443 */ |
|
444 public static function extractCode($response_str) |
|
445 { |
|
446 preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m); |
|
447 |
|
448 if (isset($m[1])) { |
|
449 return (int) $m[1]; |
|
450 } else { |
|
451 return false; |
|
452 } |
|
453 } |
|
454 |
|
455 /** |
|
456 * Extract the HTTP message from a response |
|
457 * |
|
458 * @param string $response_str |
|
459 * @return string |
|
460 */ |
|
461 public static function extractMessage($response_str) |
|
462 { |
|
463 preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m); |
|
464 |
|
465 if (isset($m[1])) { |
|
466 return $m[1]; |
|
467 } else { |
|
468 return false; |
|
469 } |
|
470 } |
|
471 |
|
472 /** |
|
473 * Extract the HTTP version from a response |
|
474 * |
|
475 * @param string $response_str |
|
476 * @return string |
|
477 */ |
|
478 public static function extractVersion($response_str) |
|
479 { |
|
480 preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m); |
|
481 |
|
482 if (isset($m[1])) { |
|
483 return $m[1]; |
|
484 } else { |
|
485 return false; |
|
486 } |
|
487 } |
|
488 |
|
489 /** |
|
490 * Extract the headers from a response string |
|
491 * |
|
492 * @param string $response_str |
|
493 * @return array |
|
494 */ |
|
495 public static function extractHeaders($response_str) |
|
496 { |
|
497 $headers = array(); |
|
498 |
|
499 // First, split body and headers |
|
500 $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); |
|
501 if (! $parts[0]) return $headers; |
|
502 |
|
503 // Split headers part to lines |
|
504 $lines = explode("\n", $parts[0]); |
|
505 unset($parts); |
|
506 $last_header = null; |
|
507 |
|
508 foreach($lines as $line) { |
|
509 $line = trim($line, "\r\n"); |
|
510 if ($line == "") break; |
|
511 |
|
512 // Locate headers like 'Location: ...' and 'Location:...' (note the missing space) |
|
513 if (preg_match("|^([\w-]+):\s*(.+)|", $line, $m)) { |
|
514 unset($last_header); |
|
515 $h_name = strtolower($m[1]); |
|
516 $h_value = $m[2]; |
|
517 |
|
518 if (isset($headers[$h_name])) { |
|
519 if (! is_array($headers[$h_name])) { |
|
520 $headers[$h_name] = array($headers[$h_name]); |
|
521 } |
|
522 |
|
523 $headers[$h_name][] = $h_value; |
|
524 } else { |
|
525 $headers[$h_name] = $h_value; |
|
526 } |
|
527 $last_header = $h_name; |
|
528 } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) { |
|
529 if (is_array($headers[$last_header])) { |
|
530 end($headers[$last_header]); |
|
531 $last_header_key = key($headers[$last_header]); |
|
532 $headers[$last_header][$last_header_key] .= $m[1]; |
|
533 } else { |
|
534 $headers[$last_header] .= $m[1]; |
|
535 } |
|
536 } |
|
537 } |
|
538 |
|
539 return $headers; |
|
540 } |
|
541 |
|
542 /** |
|
543 * Extract the body from a response string |
|
544 * |
|
545 * @param string $response_str |
|
546 * @return string |
|
547 */ |
|
548 public static function extractBody($response_str) |
|
549 { |
|
550 $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); |
|
551 if (isset($parts[1])) { |
|
552 return $parts[1]; |
|
553 } |
|
554 return ''; |
|
555 } |
|
556 |
|
557 /** |
|
558 * Decode a "chunked" transfer-encoded body and return the decoded text |
|
559 * |
|
560 * @param string $body |
|
561 * @return string |
|
562 */ |
|
563 public static function decodeChunkedBody($body) |
|
564 { |
|
565 $decBody = ''; |
|
566 |
|
567 // If mbstring overloads substr and strlen functions, we have to |
|
568 // override it's internal encoding |
|
569 if (function_exists('mb_internal_encoding') && |
|
570 ((int) ini_get('mbstring.func_overload')) & 2) { |
|
571 |
|
572 $mbIntEnc = mb_internal_encoding(); |
|
573 mb_internal_encoding('ASCII'); |
|
574 } |
|
575 |
|
576 while (trim($body)) { |
|
577 if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) { |
|
578 require_once 'Zend/Http/Exception.php'; |
|
579 throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message"); |
|
580 } |
|
581 |
|
582 $length = hexdec(trim($m[1])); |
|
583 $cut = strlen($m[0]); |
|
584 $decBody .= substr($body, $cut, $length); |
|
585 $body = substr($body, $cut + $length + 2); |
|
586 } |
|
587 |
|
588 if (isset($mbIntEnc)) { |
|
589 mb_internal_encoding($mbIntEnc); |
|
590 } |
|
591 |
|
592 return $decBody; |
|
593 } |
|
594 |
|
595 /** |
|
596 * Decode a gzip encoded message (when Content-encoding = gzip) |
|
597 * |
|
598 * Currently requires PHP with zlib support |
|
599 * |
|
600 * @param string $body |
|
601 * @return string |
|
602 */ |
|
603 public static function decodeGzip($body) |
|
604 { |
|
605 if (! function_exists('gzinflate')) { |
|
606 require_once 'Zend/Http/Exception.php'; |
|
607 throw new Zend_Http_Exception( |
|
608 'zlib extension is required in order to decode "gzip" encoding' |
|
609 ); |
|
610 } |
|
611 |
|
612 return gzinflate(substr($body, 10)); |
|
613 } |
|
614 |
|
615 /** |
|
616 * Decode a zlib deflated message (when Content-encoding = deflate) |
|
617 * |
|
618 * Currently requires PHP with zlib support |
|
619 * |
|
620 * @param string $body |
|
621 * @return string |
|
622 */ |
|
623 public static function decodeDeflate($body) |
|
624 { |
|
625 if (! function_exists('gzuncompress')) { |
|
626 require_once 'Zend/Http/Exception.php'; |
|
627 throw new Zend_Http_Exception( |
|
628 'zlib extension is required in order to decode "deflate" encoding' |
|
629 ); |
|
630 } |
|
631 |
|
632 /** |
|
633 * Some servers (IIS ?) send a broken deflate response, without the |
|
634 * RFC-required zlib header. |
|
635 * |
|
636 * We try to detect the zlib header, and if it does not exsit we |
|
637 * teat the body is plain DEFLATE content. |
|
638 * |
|
639 * This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov |
|
640 * |
|
641 * @link http://framework.zend.com/issues/browse/ZF-6040 |
|
642 */ |
|
643 $zlibHeader = unpack('n', substr($body, 0, 2)); |
|
644 if ($zlibHeader[1] % 31 == 0) { |
|
645 return gzuncompress($body); |
|
646 } else { |
|
647 return gzinflate($body); |
|
648 } |
|
649 } |
|
650 |
|
651 /** |
|
652 * Create a new Zend_Http_Response object from a string |
|
653 * |
|
654 * @param string $response_str |
|
655 * @return Zend_Http_Response |
|
656 */ |
|
657 public static function fromString($response_str) |
|
658 { |
|
659 $code = self::extractCode($response_str); |
|
660 $headers = self::extractHeaders($response_str); |
|
661 $body = self::extractBody($response_str); |
|
662 $version = self::extractVersion($response_str); |
|
663 $message = self::extractMessage($response_str); |
|
664 |
|
665 return new Zend_Http_Response($code, $headers, $body, $version, $message); |
|
666 } |
|
667 } |