<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Http; use Zend\Stdlib\ErrorHandler; use Zend\Stdlib\ResponseInterface; /** * HTTP Response * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6 */ class Response extends AbstractMessage implements ResponseInterface { /**#@+ * @const int Status codes */ const STATUS_CODE_CUSTOM = 0; const STATUS_CODE_100 = 100; const STATUS_CODE_101 = 101; const STATUS_CODE_102 = 102; const STATUS_CODE_200 = 200; const STATUS_CODE_201 = 201; const STATUS_CODE_202 = 202; const STATUS_CODE_203 = 203; const STATUS_CODE_204 = 204; const STATUS_CODE_205 = 205; const STATUS_CODE_206 = 206; const STATUS_CODE_207 = 207; const STATUS_CODE_208 = 208; const STATUS_CODE_300 = 300; const STATUS_CODE_301 = 301; const STATUS_CODE_302 = 302; const STATUS_CODE_303 = 303; const STATUS_CODE_304 = 304; const STATUS_CODE_305 = 305; const STATUS_CODE_306 = 306; const STATUS_CODE_307 = 307; const STATUS_CODE_400 = 400; const STATUS_CODE_401 = 401; const STATUS_CODE_402 = 402; const STATUS_CODE_403 = 403; const STATUS_CODE_404 = 404; const STATUS_CODE_405 = 405; const STATUS_CODE_406 = 406; const STATUS_CODE_407 = 407; const STATUS_CODE_408 = 408; const STATUS_CODE_409 = 409; const STATUS_CODE_410 = 410; const STATUS_CODE_411 = 411; const STATUS_CODE_412 = 412; const STATUS_CODE_413 = 413; const STATUS_CODE_414 = 414; const STATUS_CODE_415 = 415; const STATUS_CODE_416 = 416; const STATUS_CODE_417 = 417; const STATUS_CODE_418 = 418; const STATUS_CODE_422 = 422; const STATUS_CODE_423 = 423; const STATUS_CODE_424 = 424; const STATUS_CODE_425 = 425; const STATUS_CODE_426 = 426; const STATUS_CODE_428 = 428; const STATUS_CODE_429 = 429; const STATUS_CODE_431 = 431; const STATUS_CODE_500 = 500; const STATUS_CODE_501 = 501; const STATUS_CODE_502 = 502; const STATUS_CODE_503 = 503; const STATUS_CODE_504 = 504; const STATUS_CODE_505 = 505; const STATUS_CODE_506 = 506; const STATUS_CODE_507 = 507; const STATUS_CODE_508 = 508; const STATUS_CODE_511 = 511; /**#@-*/ /** * @var array Recommended Reason Phrases */ protected $recommendedReasonPhrases = array( // INFORMATIONAL CODES 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', // SUCCESS CODES 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', // REDIRECTION CODES 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', // Deprecated 307 => 'Temporary Redirect', // CLIENT ERROR 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 Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', // SERVER ERROR 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', ); /** * @var int Status code */ protected $statusCode = 200; /** * @var string|null Null means it will be looked up from the $reasonPhrase list above */ protected $reasonPhrase = null; /** * Populate object from string * * @param string $string * @return Response * @throws Exception\InvalidArgumentException */ public static function fromString($string) { $lines = explode("\r\n", $string); if (!is_array($lines) || count($lines) == 1) { $lines = explode("\n", $string); } $firstLine = array_shift($lines); $response = new static(); $regex = '/^HTTP\/(?P<version>1\.[01]) (?P<status>\d{3})(?:[ ]+(?P<reason>.*))?$/'; $matches = array(); if (!preg_match($regex, $firstLine, $matches)) { throw new Exception\InvalidArgumentException( 'A valid response status line was not found in the provided string' ); } $response->version = $matches['version']; $response->setStatusCode($matches['status']); $response->setReasonPhrase((isset($matches['reason']) ? $matches['reason'] : '')); if (count($lines) == 0) { return $response; } $isHeader = true; $headers = $content = array(); while ($lines) { $nextLine = array_shift($lines); if ($isHeader && $nextLine == '') { $isHeader = false; continue; } if ($isHeader) { $headers[] = $nextLine; } else { $content[] = $nextLine; } } if ($headers) { $response->headers = implode("\r\n", $headers); } if ($content) { $response->setContent(implode("\r\n", $content)); } return $response; } /** * @return Header\SetCookie[] */ public function getCookie() { return $this->getHeaders()->get('Set-Cookie'); } /** * Set HTTP status code and (optionally) message * * @param int $code * @throws Exception\InvalidArgumentException * @return Response */ public function setStatusCode($code) { if (!is_numeric($code)) { $code = is_scalar($code) ? $code : gettype($code); throw new Exception\InvalidArgumentException(sprintf( 'Invalid status code provided: "%s"', $code )); } $this->statusCode = (int) $code; return $this; } /** * Retrieve HTTP status code * * @return int */ public function getStatusCode() { return $this->statusCode; } /** * @param string $reasonPhrase * @return Response */ public function setReasonPhrase($reasonPhrase) { $this->reasonPhrase = trim($reasonPhrase); return $this; } /** * Get HTTP status message * * @return string */ public function getReasonPhrase() { if ($this->reasonPhrase == null) { return $this->recommendedReasonPhrases[$this->statusCode]; } return $this->reasonPhrase; } /** * Get the body of the response * * @return string */ public function getBody() { $body = (string) $this->getContent(); $transferEncoding = $this->getHeaders()->get('Transfer-Encoding'); if (!empty($transferEncoding)) { if (strtolower($transferEncoding->getFieldValue()) == 'chunked') { $body = $this->decodeChunkedBody($body); } } $contentEncoding = $this->getHeaders()->get('Content-Encoding'); if (!empty($contentEncoding)) { $contentEncoding = $contentEncoding->getFieldValue(); if ($contentEncoding =='gzip') { $body = $this->decodeGzip($body); } elseif ($contentEncoding == 'deflate') { $body = $this->decodeDeflate($body); } } return $body; } /** * Does the status code indicate a client error? * * @return bool */ public function isClientError() { $code = $this->getStatusCode(); return ($code < 500 && $code >= 400); } /** * Is the request forbidden due to ACLs? * * @return bool */ public function isForbidden() { return (403 == $this->getStatusCode()); } /** * Is the current status "informational"? * * @return bool */ public function isInformational() { $code = $this->getStatusCode(); return ($code >= 100 && $code < 200); } /** * Does the status code indicate the resource is not found? * * @return bool */ public function isNotFound() { return (404 === $this->getStatusCode()); } /** * Do we have a normal, OK response? * * @return bool */ public function isOk() { return (200 === $this->getStatusCode()); } /** * Does the status code reflect a server error? * * @return bool */ public function isServerError() { $code = $this->getStatusCode(); return (500 <= $code && 600 > $code); } /** * Do we have a redirect? * * @return bool */ public function isRedirect() { $code = $this->getStatusCode(); return (300 <= $code && 400 > $code); } /** * Was the response successful? * * @return bool */ public function isSuccess() { $code = $this->getStatusCode(); return (200 <= $code && 300 > $code); } /** * Render the status line header * * @return string */ public function renderStatusLine() { $status = sprintf( 'HTTP/%s %d %s', $this->getVersion(), $this->getStatusCode(), $this->getReasonPhrase() ); return trim($status); } /** * Render entire response as HTTP response string * * @return string */ public function toString() { $str = $this->renderStatusLine() . "\r\n"; $str .= $this->getHeaders()->toString(); $str .= "\r\n"; $str .= $this->getContent(); return $str; } /** * Decode a "chunked" transfer-encoded body and return the decoded text * * @param string $body * @return string * @throws Exception\RuntimeException */ protected function decodeChunkedBody($body) { $decBody = ''; while (trim($body)) { if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) { throw new Exception\RuntimeException( "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); } return $decBody; } /** * Decode a gzip encoded message (when Content-encoding = gzip) * * Currently requires PHP with zlib support * * @param string $body * @return string * @throws Exception\RuntimeException */ protected function decodeGzip($body) { if (!function_exists('gzinflate')) { throw new Exception\RuntimeException( 'zlib extension is required in order to decode "gzip" encoding' ); } ErrorHandler::start(); $return = gzinflate(substr($body, 10)); $test = ErrorHandler::stop(); if ($test) { throw new Exception\RuntimeException( 'Error occurred during gzip inflation', 0, $test ); } return $return; } /** * Decode a zlib deflated message (when Content-encoding = deflate) * * Currently requires PHP with zlib support * * @param string $body * @return string * @throws Exception\RuntimeException */ protected function decodeDeflate($body) { if (!function_exists('gzuncompress')) { throw new Exception\RuntimeException( '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 exist 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) { return gzuncompress($body); } return gzinflate($body); } }