File indexing completed on 2024-12-22 05:36:47

0001 <?php
0002 
0003 /**
0004  * Zend Framework
0005  *
0006  * LICENSE
0007  *
0008  * This source file is subject to the new BSD license that is bundled
0009  * with this package in the file LICENSE.txt.
0010  * It is also available through the world-wide-web at this URL:
0011  * http://framework.zend.com/license/new-bsd
0012  * If you did not receive a copy of the license and are unable to
0013  * obtain it through the world-wide-web, please send an email
0014  * to license@zend.com so we can send you a copy immediately.
0015  *
0016  * @category   Zend
0017  * @package    Zend_Http
0018  * @subpackage Response
0019  * @version    $Id$
0020  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0021  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0022  */
0023 
0024 /**
0025  * @see Zend_Http_Header_HeaderValue
0026  */
0027 // require_once 'Zend/Http/Header/HeaderValue.php';
0028 
0029 /**
0030  * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
0031  * includes easy access to all the response's different elemts, as well as some
0032  * convenience methods for parsing and validating HTTP responses.
0033  *
0034  * @package    Zend_Http
0035  * @subpackage Response
0036  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0037  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0038  */
0039 class Zend_Http_Response
0040 {
0041     /**
0042      * List of all known HTTP response codes - used by responseCodeAsText() to
0043      * translate numeric codes to messages.
0044      *
0045      * @var array
0046      */
0047     protected static $messages = array(
0048         // Informational 1xx
0049         100 => 'Continue',
0050         101 => 'Switching Protocols',
0051 
0052         // Success 2xx
0053         200 => 'OK',
0054         201 => 'Created',
0055         202 => 'Accepted',
0056         203 => 'Non-Authoritative Information',
0057         204 => 'No Content',
0058         205 => 'Reset Content',
0059         206 => 'Partial Content',
0060 
0061         // Redirection 3xx
0062         300 => 'Multiple Choices',
0063         301 => 'Moved Permanently',
0064         302 => 'Found',  // 1.1
0065         303 => 'See Other',
0066         304 => 'Not Modified',
0067         305 => 'Use Proxy',
0068         // 306 is deprecated but reserved
0069         307 => 'Temporary Redirect',
0070 
0071         // Client Error 4xx
0072         400 => 'Bad Request',
0073         401 => 'Unauthorized',
0074         402 => 'Payment Required',
0075         403 => 'Forbidden',
0076         404 => 'Not Found',
0077         405 => 'Method Not Allowed',
0078         406 => 'Not Acceptable',
0079         407 => 'Proxy Authentication Required',
0080         408 => 'Request Timeout',
0081         409 => 'Conflict',
0082         410 => 'Gone',
0083         411 => 'Length Required',
0084         412 => 'Precondition Failed',
0085         413 => 'Request Entity Too Large',
0086         414 => 'Request-URI Too Long',
0087         415 => 'Unsupported Media Type',
0088         416 => 'Requested Range Not Satisfiable',
0089         417 => 'Expectation Failed',
0090 
0091         // Server Error 5xx
0092         500 => 'Internal Server Error',
0093         501 => 'Not Implemented',
0094         502 => 'Bad Gateway',
0095         503 => 'Service Unavailable',
0096         504 => 'Gateway Timeout',
0097         505 => 'HTTP Version Not Supported',
0098         509 => 'Bandwidth Limit Exceeded'
0099     );
0100 
0101     /**
0102      * The HTTP version (1.0, 1.1)
0103      *
0104      * @var string
0105      */
0106     protected $version;
0107 
0108     /**
0109      * The HTTP response code
0110      *
0111      * @var int
0112      */
0113     protected $code;
0114 
0115     /**
0116      * The HTTP response code as string
0117      * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
0118      *
0119      * @var string
0120      */
0121     protected $message;
0122 
0123     /**
0124      * The HTTP response headers array
0125      *
0126      * @var array
0127      */
0128     protected $headers = array();
0129 
0130     /**
0131      * The HTTP response body
0132      *
0133      * @var string
0134      */
0135     protected $body;
0136 
0137     /**
0138      * HTTP response constructor
0139      *
0140      * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP
0141      * response string and create a new Zend_Http_Response object.
0142      *
0143      * NOTE: The constructor no longer accepts nulls or empty values for the code and
0144      * headers and will throw an exception if the passed values do not form a valid HTTP
0145      * responses.
0146      *
0147      * If no message is passed, the message will be guessed according to the response code.
0148      *
0149      * @param int    $code Response code (200, 404, ...)
0150      * @param array  $headers Headers array
0151      * @param string $body Response body
0152      * @param string $version HTTP version
0153      * @param string $message Response code as text
0154      * @throws Zend_Http_Exception
0155      */
0156     public function __construct($code, array $headers, $body = null, $version = '1.1', $message = null)
0157     {
0158         // Make sure the response code is valid and set it
0159         if (self::responseCodeAsText($code) === null) {
0160             // require_once 'Zend/Http/Exception.php';
0161             throw new Zend_Http_Exception("{$code} is not a valid HTTP response code");
0162         }
0163 
0164         $this->code = $code;
0165 
0166         foreach ($headers as $name => $value) {
0167             if (is_int($name)) {
0168                 $header = explode(":", $value, 2);
0169                 if (count($header) != 2) {
0170                     // require_once 'Zend/Http/Exception.php';
0171                     throw new Zend_Http_Exception("'{$value}' is not a valid HTTP header");
0172                 }
0173 
0174                 $name  = trim($header[0]);
0175                 $value = trim($header[1]);
0176             }
0177 
0178             $this->headers[ucwords(strtolower($name))] = $value;
0179         }
0180 
0181         // Set the body
0182         $this->body = $body;
0183 
0184         // Set the HTTP version
0185         if (! preg_match('|^\d\.\d$|', $version)) {
0186             // require_once 'Zend/Http/Exception.php';
0187             throw new Zend_Http_Exception("Invalid HTTP response version: $version");
0188         }
0189 
0190         $this->version = $version;
0191 
0192         // If we got the response message, set it. Else, set it according to
0193         // the response code
0194         if (is_string($message)) {
0195             $this->message = $message;
0196         } else {
0197             $this->message = self::responseCodeAsText($code);
0198         }
0199     }
0200 
0201     /**
0202      * Check whether the response is an error
0203      *
0204      * @return boolean
0205      */
0206     public function isError()
0207     {
0208         $restype = floor($this->code / 100);
0209         if ($restype == 4 || $restype == 5) {
0210             return true;
0211         }
0212 
0213         return false;
0214     }
0215 
0216     /**
0217      * Check whether the response in successful
0218      *
0219      * @return boolean
0220      */
0221     public function isSuccessful()
0222     {
0223         $restype = floor($this->code / 100);
0224         if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ???
0225             return true;
0226         }
0227 
0228         return false;
0229     }
0230 
0231     /**
0232      * Check whether the response is a redirection
0233      *
0234      * @return boolean
0235      */
0236     public function isRedirect()
0237     {
0238         $restype = floor($this->code / 100);
0239         if ($restype == 3) {
0240             return true;
0241         }
0242 
0243         return false;
0244     }
0245 
0246     /**
0247      * Get the response body as string
0248      *
0249      * This method returns the body of the HTTP response (the content), as it
0250      * should be in it's readable version - that is, after decoding it (if it
0251      * was decoded), deflating it (if it was gzip compressed), etc.
0252      *
0253      * If you want to get the raw body (as transfered on wire) use
0254      * $this->getRawBody() instead.
0255      *
0256      * @return string
0257      */
0258     public function getBody()
0259     {
0260         $body = '';
0261 
0262         // Decode the body if it was transfer-encoded
0263         switch (strtolower($this->getHeader('transfer-encoding'))) {
0264 
0265             // Handle chunked body
0266             case 'chunked':
0267                 $body = self::decodeChunkedBody($this->body);
0268                 break;
0269 
0270             // No transfer encoding, or unknown encoding extension:
0271             // return body as is
0272             default:
0273                 $body = $this->body;
0274                 break;
0275         }
0276 
0277         // Decode any content-encoding (gzip or deflate) if needed
0278         switch (strtolower($this->getHeader('content-encoding'))) {
0279 
0280             // Handle gzip encoding
0281             case 'gzip':
0282                 $body = self::decodeGzip($body);
0283                 break;
0284 
0285             // Handle deflate encoding
0286             case 'deflate':
0287                 $body = self::decodeDeflate($body);
0288                 break;
0289 
0290             default:
0291                 break;
0292         }
0293 
0294         return $body;
0295     }
0296 
0297     /**
0298      * Get the raw response body (as transfered "on wire") as string
0299      *
0300      * If the body is encoded (with Transfer-Encoding, not content-encoding -
0301      * IE "chunked" body), gzip compressed, etc. it will not be decoded.
0302      *
0303      * @return string
0304      */
0305     public function getRawBody()
0306     {
0307         return $this->body;
0308     }
0309 
0310     /**
0311      * Get the HTTP version of the response
0312      *
0313      * @return string
0314      */
0315     public function getVersion()
0316     {
0317         return $this->version;
0318     }
0319 
0320     /**
0321      * Get the HTTP response status code
0322      *
0323      * @return int
0324      */
0325     public function getStatus()
0326     {
0327         return $this->code;
0328     }
0329 
0330     /**
0331      * Return a message describing the HTTP response code
0332      * (Eg. "OK", "Not Found", "Moved Permanently")
0333      *
0334      * @return string
0335      */
0336     public function getMessage()
0337     {
0338         return $this->message;
0339     }
0340 
0341     /**
0342      * Get the response headers
0343      *
0344      * @return array
0345      */
0346     public function getHeaders()
0347     {
0348         return $this->headers;
0349     }
0350 
0351     /**
0352      * Get a specific header as string, or null if it is not set
0353      *
0354      * @param string$header
0355      * @return string|array|null
0356      */
0357     public function getHeader($header)
0358     {
0359         $header = ucwords(strtolower($header));
0360         if (! is_string($header) || ! isset($this->headers[$header])) return null;
0361 
0362         return $this->headers[$header];
0363     }
0364 
0365     /**
0366      * Get all headers as string
0367      *
0368      * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK")
0369      * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
0370      * @return string
0371      */
0372     public function getHeadersAsString($status_line = true, $br = "\n")
0373     {
0374         $str = '';
0375 
0376         if ($status_line) {
0377             $str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}";
0378         }
0379 
0380         // Iterate over the headers and stringify them
0381         foreach ($this->headers as $name => $value)
0382         {
0383             if (is_string($value))
0384                 $str .= "{$name}: {$value}{$br}";
0385 
0386             elseif (is_array($value)) {
0387                 foreach ($value as $subval) {
0388                     $str .= "{$name}: {$subval}{$br}";
0389                 }
0390             }
0391         }
0392 
0393         return $str;
0394     }
0395 
0396     /**
0397      * Get the entire response as string
0398      *
0399      * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
0400      * @return string
0401      */
0402     public function asString($br = "\r\n")
0403     {
0404         return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
0405     }
0406 
0407     /**
0408      * Implements magic __toString()
0409      *
0410      * @return string
0411      */
0412     public function __toString()
0413     {
0414         return $this->asString();
0415     }
0416 
0417     /**
0418      * A convenience function that returns a text representation of
0419      * HTTP response codes. Returns 'Unknown' for unknown codes.
0420      * Returns array of all codes, if $code is not specified.
0421      *
0422      * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown')
0423      * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference
0424      *
0425      * @param int $code HTTP response code
0426      * @param boolean $http11 Use HTTP version 1.1
0427      * @return string
0428      */
0429     public static function responseCodeAsText($code = null, $http11 = true)
0430     {
0431         $messages = self::$messages;
0432         if (! $http11) $messages[302] = 'Moved Temporarily';
0433 
0434         if ($code === null) {
0435             return $messages;
0436         } elseif (isset($messages[$code])) {
0437             return $messages[$code];
0438         } else {
0439             return 'Unknown';
0440         }
0441     }
0442 
0443     /**
0444      * Extract the response code from a response string
0445      *
0446      * @param string $response_str
0447      * @return int
0448      */
0449     public static function extractCode($response_str)
0450     {
0451         preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m);
0452 
0453         if (isset($m[1])) {
0454             return (int) $m[1];
0455         } else {
0456             return false;
0457         }
0458     }
0459 
0460     /**
0461      * Extract the HTTP message from a response
0462      *
0463      * @param string $response_str
0464      * @return string
0465      */
0466     public static function extractMessage($response_str)
0467     {
0468         preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m);
0469 
0470         if (isset($m[1])) {
0471             return $m[1];
0472         } else {
0473             return false;
0474         }
0475     }
0476 
0477     /**
0478      * Extract the HTTP version from a response
0479      *
0480      * @param string $response_str
0481      * @return string
0482      */
0483     public static function extractVersion($response_str)
0484     {
0485         preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m);
0486 
0487         if (isset($m[1])) {
0488             return $m[1];
0489         } else {
0490             return false;
0491         }
0492     }
0493 
0494     /**
0495      * Extract the headers from a response string
0496      *
0497      * @param   string $response_str
0498      * @return  array
0499      */
0500     public static function extractHeaders($response_str)
0501     {
0502         $headers = array();
0503 
0504         // First, split body and headers. Headers are separated from the
0505         // message at exactly the sequence "\r\n\r\n"
0506         $parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
0507         if (! $parts[0]) {
0508             return $headers;
0509         }
0510 
0511         // Split headers part to lines; "\r\n" is the only valid line separator.
0512         $lines = explode("\r\n", $parts[0]);
0513         unset($parts);
0514         $last_header = null;
0515 
0516         foreach($lines as $index => $line) {
0517             if ($index === 0 && preg_match('#^HTTP/\d+(?:\.\d+) [1-5]\d+#', $line)) {
0518                 // Status line; ignore
0519                 continue;
0520             }
0521 
0522             if ($line == "") {
0523                 // Done processing headers
0524                 break;
0525             }
0526 
0527             // Locate headers like 'Location: ...' and 'Location:...' (note the missing space)
0528             if (preg_match("|^([a-zA-Z0-9\'`#$%&*+.^_\|\~!-]+):\s*(.*)|s", $line, $m)) {
0529                 unset($last_header);
0530                 $h_name  = strtolower($m[1]);
0531                 $h_value = $m[2];
0532                 Zend_Http_Header_HeaderValue::assertValid($h_value);
0533 
0534                 if (isset($headers[$h_name])) {
0535                     if (! is_array($headers[$h_name])) {
0536                         $headers[$h_name] = array($headers[$h_name]);
0537                     }
0538 
0539                     $headers[$h_name][] = ltrim($h_value);
0540                     $last_header = $h_name;
0541                     continue;
0542                 }
0543 
0544                 $headers[$h_name] = ltrim($h_value);
0545                 $last_header = $h_name;
0546                 continue;
0547             }
0548 
0549             // Identify header continuations
0550             if (preg_match("|^[ \t](.+)$|s", $line, $m) && $last_header !== null) {
0551                 $h_value = trim($m[1]);
0552                 if (is_array($headers[$last_header])) {
0553                     end($headers[$last_header]);
0554                     $last_header_key = key($headers[$last_header]);
0555 
0556                     $h_value = $headers[$last_header][$last_header_key] . $h_value;
0557                     Zend_Http_Header_HeaderValue::assertValid($h_value);
0558 
0559                     $headers[$last_header][$last_header_key] = $h_value;
0560                     continue;
0561                 }
0562 
0563                 $h_value = $headers[$last_header] . $h_value;
0564                 Zend_Http_Header_HeaderValue::assertValid($h_value);
0565 
0566                 $headers[$last_header] = $h_value;
0567                 continue;
0568             }
0569 
0570             // Anything else is an error condition
0571             // require_once 'Zend/Http/Exception.php';
0572             throw new Zend_Http_Exception('Invalid header line detected');
0573         }
0574 
0575         return $headers;
0576     }
0577 
0578     /**
0579      * Extract the body from a response string
0580      *
0581      * @param string $response_str
0582      * @return string
0583      */
0584     public static function extractBody($response_str)
0585     {
0586         $parts = preg_split('|(?:\r\n){2}|m', $response_str, 2);
0587         if (isset($parts[1])) {
0588             return $parts[1];
0589         }
0590         return '';
0591     }
0592 
0593     /**
0594      * Decode a "chunked" transfer-encoded body and return the decoded text
0595      *
0596      * @param string $body
0597      * @return string
0598      */
0599     public static function decodeChunkedBody($body)
0600     {
0601         $decBody = '';
0602 
0603         // If mbstring overloads substr and strlen functions, we have to
0604         // override it's internal encoding
0605         if (function_exists('mb_internal_encoding') &&
0606            ((int) ini_get('mbstring.func_overload')) & 2) {
0607 
0608             $mbIntEnc = mb_internal_encoding();
0609             mb_internal_encoding('ASCII');
0610         }
0611 
0612         while (trim($body)) {
0613             if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
0614                 // require_once 'Zend/Http/Exception.php';
0615                 throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message");
0616             }
0617 
0618             $length = hexdec(trim($m[1]));
0619             $cut = strlen($m[0]);
0620             $decBody .= substr($body, $cut, $length);
0621             $body = substr($body, $cut + $length + 2);
0622         }
0623 
0624         if (isset($mbIntEnc)) {
0625             mb_internal_encoding($mbIntEnc);
0626         }
0627 
0628         return $decBody;
0629     }
0630 
0631     /**
0632      * Decode a gzip encoded message (when Content-encoding = gzip)
0633      *
0634      * Currently requires PHP with zlib support
0635      *
0636      * @param string $body
0637      * @return string
0638      */
0639     public static function decodeGzip($body)
0640     {
0641         if (! function_exists('gzinflate')) {
0642             // require_once 'Zend/Http/Exception.php';
0643             throw new Zend_Http_Exception(
0644                 'zlib extension is required in order to decode "gzip" encoding'
0645             );
0646         }
0647 
0648         return gzinflate(substr($body, 10));
0649     }
0650 
0651     /**
0652      * Decode a zlib deflated message (when Content-encoding = deflate)
0653      *
0654      * Currently requires PHP with zlib support
0655      *
0656      * @param string $body
0657      * @return string
0658      */
0659     public static function decodeDeflate($body)
0660     {
0661         if (! function_exists('gzuncompress')) {
0662             // require_once 'Zend/Http/Exception.php';
0663             throw new Zend_Http_Exception(
0664                 'zlib extension is required in order to decode "deflate" encoding'
0665             );
0666         }
0667 
0668         /**
0669          * Some servers (IIS ?) send a broken deflate response, without the
0670          * RFC-required zlib header.
0671          *
0672          * We try to detect the zlib header, and if it does not exsit we
0673          * teat the body is plain DEFLATE content.
0674          *
0675          * This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov
0676          *
0677          * @link http://framework.zend.com/issues/browse/ZF-6040
0678          */
0679         $zlibHeader = unpack('n', substr($body, 0, 2));
0680         if ($zlibHeader[1] % 31 == 0 && ord($body[0]) == 0x78 && in_array(ord($body[1]), array(0x01, 0x5e, 0x9c, 0xda))) {
0681             return gzuncompress($body);
0682         } else {
0683             return gzinflate($body);
0684         }
0685     }
0686 
0687     /**
0688      * Create a new Zend_Http_Response object from a string
0689      *
0690      * @param string $response_str
0691      * @return Zend_Http_Response
0692      */
0693     public static function fromString($response_str)
0694     {
0695         $code    = self::extractCode($response_str);
0696         $headers = self::extractHeaders($response_str);
0697         $body    = self::extractBody($response_str);
0698         $version = self::extractVersion($response_str);
0699         $message = self::extractMessage($response_str);
0700 
0701         return new Zend_Http_Response($code, $headers, $body, $version, $message);
0702     }
0703 }