diff --git a/src/Network/Response.php b/src/Network/Response.php index 034a15be276..52fbe4dae2b 100644 --- a/src/Network/Response.php +++ b/src/Network/Response.php @@ -21,6 +21,7 @@ use DateTime; use DateTimeZone; use InvalidArgumentException; +use Zend\Diactoros\MessageTrait; /** * Cake Response is responsible for managing the response text, status and headers of a HTTP response. @@ -31,6 +32,8 @@ class Response { + use MessageTrait; + /** * Holds HTTP response statuses * @@ -355,13 +358,6 @@ class Response */ protected $_contentType = 'text/html'; - /** - * Buffer list of headers - * - * @var array - */ - protected $_headers = []; - /** * Buffer string or callable for response message * @@ -405,6 +401,13 @@ class Response */ protected $_cookies = []; + /** + * Reason Phrase + * + * @var string|null + */ + protected $_reasonPhrase = null; + /** * Constructor * @@ -443,7 +446,7 @@ public function __construct(array $options = []) */ public function send() { - if (isset($this->_headers['Location']) && $this->_status === 200) { + if (isset($this->headers['Location']) && $this->_status === 200) { $this->statusCode(302); } @@ -481,7 +484,7 @@ public function sendHeaders() $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}"); $this->_setContentType(); - foreach ($this->_headers as $header => $values) { + foreach ($this->headers as $header => $values) { foreach ((array)$values as $value) { $this->_sendHeader($header, $value); } @@ -533,9 +536,9 @@ protected function _setContentType() } if ($charset) { - $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}"); + $this->_setHeader('Content-Type', "{$this->_contentType}; charset={$this->_charset}"); } else { - $this->header('Content-Type', "{$this->_contentType}"); + $this->_setHeader('Content-Type', "{$this->_contentType}"); } } @@ -630,8 +633,9 @@ protected function _sendContent($content) public function header($header = null, $value = null) { if ($header === null) { - return $this->_headers; + return $this->headers; } + $headers = is_array($header) ? $header : [$header => $value]; foreach ($headers as $header => $value) { if (is_numeric($header)) { @@ -640,10 +644,17 @@ public function header($header = null, $value = null) if ($value === null) { list($header, $value) = explode(':', $header, 2); } - $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value); + + if ($this->hasHeader($header)) { + $header = $this->headerNames[strtolower($header)]; + } else { + $this->headerNames[strtolower($header)] = $header; + } + + $this->headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value); } - return $this->_headers; + return $this->headers; } /** @@ -658,15 +669,31 @@ public function header($header = null, $value = null) public function location($url = null) { if ($url === null) { - $headers = $this->header(); + $result = $this->getHeaderLine('Location'); + if (empty($result)) { + return null; + } - return isset($headers['Location']) ? $headers['Location'] : null; + return $result; } - $this->header('Location', $url); + $this->_setHeader('Location', $url); return null; } + /** + * Sets a header. + * + * @param string $header Header key. + * @param string $value Header value. + */ + protected function _setHeader($header, $value) + { + $normalized = strtolower($header); + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + /** * Buffers the response message to be sent * if $content is null the current buffer is returned @@ -703,6 +730,68 @@ public function statusCode($code = null) return $this->_status = $code; } + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int Status code. + */ + public function getStatusCode() + { + return $this->_status; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return static + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->_status = $code; + if (empty($reasonPhrase) && isset($new->_statusCodes[$code])) { + $reasonPhrase = $new->_statusCodes[$code]; + } + $new->_reasonPhrase = $reasonPhrase; + return $new; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + return $this->_reasonPhrase; + } + /** * Queries & sets valid HTTP response codes & messages. * @@ -875,11 +964,9 @@ public function charset($charset = null) */ public function disableCache() { - $this->header([ - 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', - 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT", - 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' - ]); + $this->_setHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'); + $this->_setHeader('Last-Modified', gmdate("D, d M Y H:i:s") . " GMT"); + $this->_setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); } /** @@ -894,9 +981,9 @@ public function cache($since, $time = '+1 day') if (!is_int($time)) { $time = strtotime($time); } - $this->header([ - 'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT' - ]); + + $this->_setHeader('Date', gmdate("D, j M Y G:i:s ", time()) . 'GMT'); + $this->modified($since); $this->expires($time); $this->sharable(true); @@ -1026,7 +1113,7 @@ protected function _setCacheControl() $control .= ', '; } $control = rtrim($control, ', '); - $this->header('Cache-Control', $control); + $this->_setHeader('Cache-Control', $control); } /** @@ -1046,10 +1133,11 @@ public function expires($time = null) { if ($time !== null) { $date = $this->_getUTCDate($time); - $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT'; + $this->_setHeader('Expires', $date->format('D, j M Y H:i:s') . ' GMT'); } - if (isset($this->_headers['Expires'])) { - return $this->_headers['Expires']; + + if ($this->hasHeader('Expires')) { + return $this->getHeaderLine('Expires'); } return null; @@ -1072,10 +1160,10 @@ public function modified($time = null) { if ($time !== null) { $date = $this->_getUTCDate($time); - $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT'; + $this->headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT'; } - if (isset($this->_headers['Last-Modified'])) { - return $this->_headers['Last-Modified']; + if (isset($this->headers['Last-Modified'])) { + return $this->headers['Last-Modified']; } return null; @@ -1102,7 +1190,7 @@ public function notModified() 'Last-Modified' ]; foreach ($remove as $header) { - unset($this->_headers[$header]); + unset($this->headers[$header]); } } @@ -1120,10 +1208,11 @@ public function vary($cacheVariances = null) { if ($cacheVariances !== null) { $cacheVariances = (array)$cacheVariances; - $this->_headers['Vary'] = implode(', ', $cacheVariances); + $this->_setHeader('Vary', implode(', ', $cacheVariances)); } - if (isset($this->_headers['Vary'])) { - return explode(', ', $this->_headers['Vary']); + + if ($this->hasHeader('Vary')) { + return explode(', ', $this->getHeaderLine('Vary')); } return null; @@ -1153,10 +1242,11 @@ public function vary($cacheVariances = null) public function etag($hash = null, $weak = false) { if ($hash !== null) { - $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $hash); + $this->_setHeader('Etag', sprintf('%s"%s"', ($weak) ? 'W/' : null, $hash)); } - if (isset($this->_headers['Etag'])) { - return $this->_headers['Etag']; + + if ($this->hasHeader('Etag')) { + return $this->getHeaderLine('Etag'); } return null; @@ -1246,10 +1336,11 @@ public function protocol($protocol = null) public function length($bytes = null) { if ($bytes !== null) { - $this->_headers['Content-Length'] = $bytes; + $this->_setHeader('Content-Length', $bytes); } - if (isset($this->_headers['Content-Length'])) { - return $this->_headers['Content-Length']; + + if ($this->hasHeader('Content-Length')) { + return $this->getHeaderLine('Content-Length'); } return null; diff --git a/tests/TestCase/Network/ResponseTest.php b/tests/TestCase/Network/ResponseTest.php index 393efd8c010..39d93b439b7 100644 --- a/tests/TestCase/Network/ResponseTest.php +++ b/tests/TestCase/Network/ResponseTest.php @@ -2096,4 +2096,65 @@ public function testLocation() $this->assertNull($response->location('http://example.org'), 'Setting a location should return null'); $this->assertEquals('http://example.org', $response->location(), 'Reading a location should return the value.'); } + + /** + * Test get protocol version. + * + * @return void + */ + public function getProtocolVersion() + { + $response = new Response(); + $version = $response->getProtocolVersion(); + $this->assertEquals('1.1', $version); + } + /** + * Test with protocol. + * + * @return void + */ + public function testWithProtocol() + { + $response = new Response(); + $version = $response->getProtocolVersion(); + $this->assertEquals('1.1', $version); + $response2 = $response->withProtocolVersion('1.0'); + $version = $response2->getProtocolVersion(); + $this->assertEquals('1.0', $version); + $version = $response->getProtocolVersion(); + $this->assertEquals('1.1', $version); + $this->assertNotEquals($response, $response2); + } + /** + * Test with protocol. + * + * @return void + */ + public function testWithStatusCode() + { + $response = new Response(); + $statusCode = $response->getStatusCode(); + $this->assertEquals(200, $statusCode); + $response2 = $response->withStatus(404); + $statusCode = $response2->getStatusCode(); + $this->assertEquals(404, $statusCode); + $statusCode = $response->getStatusCode(); + $this->assertEquals(200, $statusCode); + $this->assertNotEquals($response, $response2); + } + + /** + * Test get reason phrase. + * + * @return void + */ + public function testGetReasonPhrase() + { + $response = new Response(); + $reasonPhrase = $response->getReasonPhrase(); + $this->assertNull($reasonPhrase); + $response = $response->withStatus(404); + $reasonPhrase = $response->getReasonPhrase(); + $this->assertEquals('Not Found', $reasonPhrase); + } }