]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - Zend/Http/Response.php
Release 6.5.0
[Github/sugarcrm.git] / Zend / Http / Response.php
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
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 }