668 lines
18 KiB
PHP
668 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Zend Framework
|
|
*
|
|
* LICENSE
|
|
*
|
|
* This source file is subject to the new BSD license that is bundled
|
|
* with this package in the file LICENSE.txt.
|
|
* It is also available through the world-wide-web at this URL:
|
|
* http://framework.zend.com/license/new-bsd
|
|
* If you did not receive a copy of the license and are unable to
|
|
* obtain it through the world-wide-web, please send an email
|
|
* to license@zend.com so we can send you a copy immediately.
|
|
*
|
|
* @category Zend
|
|
* @package Zend_Http
|
|
* @subpackage Response
|
|
* @version $Id: Response.php 25081 2012-11-06 20:59:47Z rob $
|
|
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
|
|
* @license http://framework.zend.com/license/new-bsd New BSD License
|
|
*/
|
|
|
|
/**
|
|
* Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
|
|
* includes easy access to all the response's different elemts, as well as some
|
|
* convenience methods for parsing and validating HTTP responses.
|
|
*
|
|
* @package Zend_Http
|
|
* @subpackage Response
|
|
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
|
|
* @license http://framework.zend.com/license/new-bsd New BSD License
|
|
*/
|
|
class Zend_Http_Response
|
|
{
|
|
/**
|
|
* List of all known HTTP response codes - used by responseCodeAsText() to
|
|
* translate numeric codes to messages.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $messages = array(
|
|
// Informational 1xx
|
|
100 => 'Continue',
|
|
101 => 'Switching Protocols',
|
|
|
|
// Success 2xx
|
|
200 => 'OK',
|
|
201 => 'Created',
|
|
202 => 'Accepted',
|
|
203 => 'Non-Authoritative Information',
|
|
204 => 'No Content',
|
|
205 => 'Reset Content',
|
|
206 => 'Partial Content',
|
|
|
|
// Redirection 3xx
|
|
300 => 'Multiple Choices',
|
|
301 => 'Moved Permanently',
|
|
302 => 'Found', // 1.1
|
|
303 => 'See Other',
|
|
304 => 'Not Modified',
|
|
305 => 'Use Proxy',
|
|
// 306 is deprecated but reserved
|
|
307 => 'Temporary Redirect',
|
|
|
|
// Client Error 4xx
|
|
400 => 'Bad Request',
|
|
401 => 'Unauthorized',
|
|
402 => 'Payment Required',
|
|
403 => 'Forbidden',
|
|
404 => 'Not Found',
|
|
405 => 'Method Not Allowed',
|
|
406 => 'Not Acceptable',
|
|
407 => 'Proxy Authentication Required',
|
|
408 => 'Request Timeout',
|
|
409 => 'Conflict',
|
|
410 => 'Gone',
|
|
411 => 'Length Required',
|
|
412 => 'Precondition Failed',
|
|
413 => 'Request Entity Too Large',
|
|
414 => 'Request-URI Too Long',
|
|
415 => 'Unsupported Media Type',
|
|
416 => 'Requested Range Not Satisfiable',
|
|
417 => 'Expectation Failed',
|
|
|
|
// Server Error 5xx
|
|
500 => 'Internal Server Error',
|
|
501 => 'Not Implemented',
|
|
502 => 'Bad Gateway',
|
|
503 => 'Service Unavailable',
|
|
504 => 'Gateway Timeout',
|
|
505 => 'HTTP Version Not Supported',
|
|
509 => 'Bandwidth Limit Exceeded'
|
|
);
|
|
|
|
/**
|
|
* The HTTP version (1.0, 1.1)
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $version;
|
|
|
|
/**
|
|
* The HTTP response code
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $code;
|
|
|
|
/**
|
|
* The HTTP response code as string
|
|
* (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $message;
|
|
|
|
/**
|
|
* The HTTP response headers array
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $headers = array();
|
|
|
|
/**
|
|
* The HTTP response body
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $body;
|
|
|
|
/**
|
|
* HTTP response constructor
|
|
*
|
|
* In most cases, you would use Zend_Http_Response::fromString to parse an HTTP
|
|
* response string and create a new Zend_Http_Response object.
|
|
*
|
|
* NOTE: The constructor no longer accepts nulls or empty values for the code and
|
|
* headers and will throw an exception if the passed values do not form a valid HTTP
|
|
* responses.
|
|
*
|
|
* If no message is passed, the message will be guessed according to the response code.
|
|
*
|
|
* @param int $code Response code (200, 404, ...)
|
|
* @param array $headers Headers array
|
|
* @param string $body Response body
|
|
* @param string $version HTTP version
|
|
* @param string $message Response code as text
|
|
* @throws Zend_Http_Exception
|
|
*/
|
|
public function __construct($code, array $headers, $body = null, $version = '1.1', $message = null)
|
|
{
|
|
// Make sure the response code is valid and set it
|
|
if (self::responseCodeAsText($code) === null) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception("{$code} is not a valid HTTP response code");
|
|
}
|
|
|
|
$this->code = $code;
|
|
|
|
foreach ($headers as $name => $value) {
|
|
if (is_int($name)) {
|
|
$header = explode(":", $value, 2);
|
|
if (count($header) != 2) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception("'{$value}' is not a valid HTTP header");
|
|
}
|
|
|
|
$name = trim($header[0]);
|
|
$value = trim($header[1]);
|
|
}
|
|
|
|
$this->headers[ucwords(strtolower($name))] = $value;
|
|
}
|
|
|
|
// Set the body
|
|
$this->body = $body;
|
|
|
|
// Set the HTTP version
|
|
if (! preg_match('|^\d\.\d$|', $version)) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception("Invalid HTTP response version: $version");
|
|
}
|
|
|
|
$this->version = $version;
|
|
|
|
// If we got the response message, set it. Else, set it according to
|
|
// the response code
|
|
if (is_string($message)) {
|
|
$this->message = $message;
|
|
} else {
|
|
$this->message = self::responseCodeAsText($code);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the response is an error
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isError()
|
|
{
|
|
$restype = floor($this->code / 100);
|
|
if ($restype == 4 || $restype == 5) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check whether the response in successful
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isSuccessful()
|
|
{
|
|
$restype = floor($this->code / 100);
|
|
if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ???
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check whether the response is a redirection
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isRedirect()
|
|
{
|
|
$restype = floor($this->code / 100);
|
|
if ($restype == 3) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the response body as string
|
|
*
|
|
* This method returns the body of the HTTP response (the content), as it
|
|
* should be in it's readable version - that is, after decoding it (if it
|
|
* was decoded), deflating it (if it was gzip compressed), etc.
|
|
*
|
|
* If you want to get the raw body (as transfered on wire) use
|
|
* $this->getRawBody() instead.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBody()
|
|
{
|
|
$body = '';
|
|
|
|
// Decode the body if it was transfer-encoded
|
|
switch (strtolower($this->getHeader('transfer-encoding'))) {
|
|
|
|
// Handle chunked body
|
|
case 'chunked':
|
|
$body = self::decodeChunkedBody($this->body);
|
|
break;
|
|
|
|
// No transfer encoding, or unknown encoding extension:
|
|
// return body as is
|
|
default:
|
|
$body = $this->body;
|
|
break;
|
|
}
|
|
|
|
// Decode any content-encoding (gzip or deflate) if needed
|
|
switch (strtolower($this->getHeader('content-encoding'))) {
|
|
|
|
// Handle gzip encoding
|
|
case 'gzip':
|
|
$body = self::decodeGzip($body);
|
|
break;
|
|
|
|
// Handle deflate encoding
|
|
case 'deflate':
|
|
$body = self::decodeDeflate($body);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Get the raw response body (as transfered "on wire") as string
|
|
*
|
|
* If the body is encoded (with Transfer-Encoding, not content-encoding -
|
|
* IE "chunked" body), gzip compressed, etc. it will not be decoded.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRawBody()
|
|
{
|
|
return $this->body;
|
|
}
|
|
|
|
/**
|
|
* Get the HTTP version of the response
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getVersion()
|
|
{
|
|
return $this->version;
|
|
}
|
|
|
|
/**
|
|
* Get the HTTP response status code
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getStatus()
|
|
{
|
|
return $this->code;
|
|
}
|
|
|
|
/**
|
|
* Return a message describing the HTTP response code
|
|
* (Eg. "OK", "Not Found", "Moved Permanently")
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getMessage()
|
|
{
|
|
return $this->message;
|
|
}
|
|
|
|
/**
|
|
* Get the response headers
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getHeaders()
|
|
{
|
|
return $this->headers;
|
|
}
|
|
|
|
/**
|
|
* Get a specific header as string, or null if it is not set
|
|
*
|
|
* @param string$header
|
|
* @return string|array|null
|
|
*/
|
|
public function getHeader($header)
|
|
{
|
|
$header = ucwords(strtolower($header));
|
|
if (! is_string($header) || ! isset($this->headers[$header])) return null;
|
|
|
|
return $this->headers[$header];
|
|
}
|
|
|
|
/**
|
|
* Get all headers as string
|
|
*
|
|
* @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK")
|
|
* @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
|
|
* @return string
|
|
*/
|
|
public function getHeadersAsString($status_line = true, $br = "\n")
|
|
{
|
|
$str = '';
|
|
|
|
if ($status_line) {
|
|
$str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}";
|
|
}
|
|
|
|
// Iterate over the headers and stringify them
|
|
foreach ($this->headers as $name => $value)
|
|
{
|
|
if (is_string($value))
|
|
$str .= "{$name}: {$value}{$br}";
|
|
|
|
elseif (is_array($value)) {
|
|
foreach ($value as $subval) {
|
|
$str .= "{$name}: {$subval}{$br}";
|
|
}
|
|
}
|
|
}
|
|
|
|
return $str;
|
|
}
|
|
|
|
/**
|
|
* Get the entire response as string
|
|
*
|
|
* @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
|
|
* @return string
|
|
*/
|
|
public function asString($br = "\n")
|
|
{
|
|
return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody();
|
|
}
|
|
|
|
/**
|
|
* Implements magic __toString()
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString()
|
|
{
|
|
return $this->asString();
|
|
}
|
|
|
|
/**
|
|
* A convenience function that returns a text representation of
|
|
* HTTP response codes. Returns 'Unknown' for unknown codes.
|
|
* Returns array of all codes, if $code is not specified.
|
|
*
|
|
* Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown')
|
|
* See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference
|
|
*
|
|
* @param int $code HTTP response code
|
|
* @param boolean $http11 Use HTTP version 1.1
|
|
* @return string
|
|
*/
|
|
public static function responseCodeAsText($code = null, $http11 = true)
|
|
{
|
|
$messages = self::$messages;
|
|
if (! $http11) $messages[302] = 'Moved Temporarily';
|
|
|
|
if ($code === null) {
|
|
return $messages;
|
|
} elseif (isset($messages[$code])) {
|
|
return $messages[$code];
|
|
} else {
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the response code from a response string
|
|
*
|
|
* @param string $response_str
|
|
* @return int
|
|
*/
|
|
public static function extractCode($response_str)
|
|
{
|
|
preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m);
|
|
|
|
if (isset($m[1])) {
|
|
return (int) $m[1];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the HTTP message from a response
|
|
*
|
|
* @param string $response_str
|
|
* @return string
|
|
*/
|
|
public static function extractMessage($response_str)
|
|
{
|
|
preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m);
|
|
|
|
if (isset($m[1])) {
|
|
return $m[1];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the HTTP version from a response
|
|
*
|
|
* @param string $response_str
|
|
* @return string
|
|
*/
|
|
public static function extractVersion($response_str)
|
|
{
|
|
preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m);
|
|
|
|
if (isset($m[1])) {
|
|
return $m[1];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the headers from a response string
|
|
*
|
|
* @param string $response_str
|
|
* @return array
|
|
*/
|
|
public static function extractHeaders($response_str)
|
|
{
|
|
$headers = array();
|
|
|
|
// First, split body and headers
|
|
$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
|
|
if (! $parts[0]) return $headers;
|
|
|
|
// Split headers part to lines
|
|
$lines = explode("\n", $parts[0]);
|
|
unset($parts);
|
|
$last_header = null;
|
|
|
|
foreach($lines as $line) {
|
|
$line = trim($line, "\r\n");
|
|
if ($line == "") break;
|
|
|
|
// Locate headers like 'Location: ...' and 'Location:...' (note the missing space)
|
|
if (preg_match("|^([\w-]+):\s*(.+)|", $line, $m)) {
|
|
unset($last_header);
|
|
$h_name = strtolower($m[1]);
|
|
$h_value = $m[2];
|
|
|
|
if (isset($headers[$h_name])) {
|
|
if (! is_array($headers[$h_name])) {
|
|
$headers[$h_name] = array($headers[$h_name]);
|
|
}
|
|
|
|
$headers[$h_name][] = $h_value;
|
|
} else {
|
|
$headers[$h_name] = $h_value;
|
|
}
|
|
$last_header = $h_name;
|
|
} elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
|
|
if (is_array($headers[$last_header])) {
|
|
end($headers[$last_header]);
|
|
$last_header_key = key($headers[$last_header]);
|
|
$headers[$last_header][$last_header_key] .= $m[1];
|
|
} else {
|
|
$headers[$last_header] .= $m[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $headers;
|
|
}
|
|
|
|
/**
|
|
* Extract the body from a response string
|
|
*
|
|
* @param string $response_str
|
|
* @return string
|
|
*/
|
|
public static function extractBody($response_str)
|
|
{
|
|
$parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
|
|
if (isset($parts[1])) {
|
|
return $parts[1];
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Decode a "chunked" transfer-encoded body and return the decoded text
|
|
*
|
|
* @param string $body
|
|
* @return string
|
|
*/
|
|
public static function decodeChunkedBody($body)
|
|
{
|
|
$decBody = '';
|
|
|
|
// If mbstring overloads substr and strlen functions, we have to
|
|
// override it's internal encoding
|
|
if (function_exists('mb_internal_encoding') &&
|
|
((int) ini_get('mbstring.func_overload')) & 2) {
|
|
|
|
$mbIntEnc = mb_internal_encoding();
|
|
mb_internal_encoding('ASCII');
|
|
}
|
|
|
|
while (trim($body)) {
|
|
if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message");
|
|
}
|
|
|
|
$length = hexdec(trim($m[1]));
|
|
$cut = strlen($m[0]);
|
|
$decBody .= substr($body, $cut, $length);
|
|
$body = substr($body, $cut + $length + 2);
|
|
}
|
|
|
|
if (isset($mbIntEnc)) {
|
|
mb_internal_encoding($mbIntEnc);
|
|
}
|
|
|
|
return $decBody;
|
|
}
|
|
|
|
/**
|
|
* Decode a gzip encoded message (when Content-encoding = gzip)
|
|
*
|
|
* Currently requires PHP with zlib support
|
|
*
|
|
* @param string $body
|
|
* @return string
|
|
*/
|
|
public static function decodeGzip($body)
|
|
{
|
|
if (! function_exists('gzinflate')) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception(
|
|
'zlib extension is required in order to decode "gzip" encoding'
|
|
);
|
|
}
|
|
|
|
return gzinflate(substr($body, 10));
|
|
}
|
|
|
|
/**
|
|
* Decode a zlib deflated message (when Content-encoding = deflate)
|
|
*
|
|
* Currently requires PHP with zlib support
|
|
*
|
|
* @param string $body
|
|
* @return string
|
|
*/
|
|
public static function decodeDeflate($body)
|
|
{
|
|
if (! function_exists('gzuncompress')) {
|
|
require_once 'Zend/Http/Exception.php';
|
|
throw new Zend_Http_Exception(
|
|
'zlib extension is required in order to decode "deflate" encoding'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Some servers (IIS ?) send a broken deflate response, without the
|
|
* RFC-required zlib header.
|
|
*
|
|
* We try to detect the zlib header, and if it does not exsit we
|
|
* teat the body is plain DEFLATE content.
|
|
*
|
|
* This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov
|
|
*
|
|
* @link http://framework.zend.com/issues/browse/ZF-6040
|
|
*/
|
|
$zlibHeader = unpack('n', substr($body, 0, 2));
|
|
if ($zlibHeader[1] % 31 == 0 && ord($body[0]) == 0x78 && in_array(ord($body[1]), array(0x01, 0x5e, 0x9c, 0xda))) {
|
|
return gzuncompress($body);
|
|
} else {
|
|
return gzinflate($body);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new Zend_Http_Response object from a string
|
|
*
|
|
* @param string $response_str
|
|
* @return Zend_Http_Response
|
|
*/
|
|
public static function fromString($response_str)
|
|
{
|
|
$code = self::extractCode($response_str);
|
|
$headers = self::extractHeaders($response_str);
|
|
$body = self::extractBody($response_str);
|
|
$version = self::extractVersion($response_str);
|
|
$message = self::extractMessage($response_str);
|
|
|
|
return new Zend_Http_Response($code, $headers, $body, $version, $message);
|
|
}
|
|
}
|