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 }