You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1074 lines
25 KiB
1074 lines
25 KiB
<?php
|
|
|
|
|
|
/**
|
|
* CodeIgniter
|
|
*
|
|
* An open source application development framework for PHP
|
|
*
|
|
* This content is released under the MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2014-2019 British Columbia Institute of Technology
|
|
* Copyright (c) 2019 CodeIgniter Foundation
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* @package CodeIgniter
|
|
* @author CodeIgniter Dev Team
|
|
* @copyright 2019 CodeIgniter Foundation
|
|
* @license https://opensource.org/licenses/MIT MIT License
|
|
* @link https://codeigniter.com
|
|
* @since Version 4.0.0
|
|
* @filesource
|
|
*/
|
|
|
|
namespace CodeIgniter\HTTP;
|
|
|
|
use Config\App;
|
|
use Config\Format;
|
|
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
|
use CodeIgniter\Pager\PagerInterface;
|
|
|
|
/**
|
|
* Representation of an outgoing, getServer-side response.
|
|
*
|
|
* Per the HTTP specification, this interface includes properties for
|
|
* each of the following:
|
|
*
|
|
* - Protocol version
|
|
* - Status code and reason phrase
|
|
* - Headers
|
|
* - Message body
|
|
*
|
|
* @package CodeIgniter\HTTP
|
|
*/
|
|
class Response extends Message implements ResponseInterface
|
|
{
|
|
|
|
/**
|
|
* HTTP status codes
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $statusCodes = [
|
|
// 1xx: Informational
|
|
100 => 'Continue',
|
|
101 => 'Switching Protocols',
|
|
102 => 'Processing', // http://www.iana.org/go/rfc2518
|
|
103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt
|
|
// 2xx: Success
|
|
200 => 'OK',
|
|
201 => 'Created',
|
|
202 => 'Accepted',
|
|
203 => 'Non-Authoritative Information', // 1.1
|
|
204 => 'No Content',
|
|
205 => 'Reset Content',
|
|
206 => 'Partial Content',
|
|
207 => 'Multi-Status', // http://www.iana.org/go/rfc4918
|
|
208 => 'Already Reported', // http://www.iana.org/go/rfc5842
|
|
226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt
|
|
// 3xx: Redirection
|
|
300 => 'Multiple Choices',
|
|
301 => 'Moved Permanently',
|
|
302 => 'Found', // Formerly 'Moved Temporarily'
|
|
303 => 'See Other', // 1.1
|
|
304 => 'Not Modified',
|
|
305 => 'Use Proxy', // 1.1
|
|
306 => 'Switch Proxy', // No longer used
|
|
307 => 'Temporary Redirect', // 1.1
|
|
308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt
|
|
// 4xx: 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 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',
|
|
418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt
|
|
// 419 (Authentication Timeout) is a non-standard status code with unknown origin
|
|
421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2
|
|
422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918
|
|
423 => 'Locked', // http://www.iana.org/go/rfc4918
|
|
424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918
|
|
425 => 'Too Early', // https://datatracker.ietf.org/doc/draft-ietf-httpbis-replay/
|
|
426 => 'Upgrade Required',
|
|
428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
|
|
429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
|
|
431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
|
|
451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725
|
|
499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133
|
|
// 5xx: Server error
|
|
500 => 'Internal Server Error',
|
|
501 => 'Not Implemented',
|
|
502 => 'Bad Gateway',
|
|
503 => 'Service Unavailable',
|
|
504 => 'Gateway Timeout',
|
|
505 => 'HTTP Version Not Supported',
|
|
506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt
|
|
507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918
|
|
508 => 'Loop Detected', // http://www.iana.org/go/rfc5842
|
|
510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt
|
|
511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt
|
|
599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599
|
|
];
|
|
|
|
/**
|
|
* The current reason phrase for this response.
|
|
* If null, will use the default provided for the status code.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $reason;
|
|
|
|
/**
|
|
* The current status code for this response.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $statusCode = 200;
|
|
|
|
/**
|
|
* Whether Content Security Policy is being enforced.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $CSPEnabled = false;
|
|
|
|
/**
|
|
* Content security policy handler
|
|
*
|
|
* @var \CodeIgniter\HTTP\ContentSecurityPolicy
|
|
*/
|
|
public $CSP;
|
|
|
|
/**
|
|
* Set a cookie name prefix if you need to avoid collisions
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookiePrefix = '';
|
|
|
|
/**
|
|
* Set to .your-domain.com for site-wide cookies
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookieDomain = '';
|
|
|
|
/**
|
|
* Typically will be a forward slash
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookiePath = '/';
|
|
|
|
/**
|
|
* Cookie will only be set if a secure HTTPS connection exists.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $cookieSecure = false;
|
|
|
|
/**
|
|
* Cookie will only be accessible via HTTP(S) (no javascript)
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $cookieHTTPOnly = false;
|
|
|
|
/**
|
|
* Stores all cookies that were set in the response.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $cookies = [];
|
|
|
|
/**
|
|
* If true, will not write output. Useful during testing.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $pretend = false;
|
|
|
|
/**
|
|
* Type of format the body is in.
|
|
* Valid: html, json, xml
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $bodyFormat = 'html';
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param App $config
|
|
*/
|
|
public function __construct($config)
|
|
{
|
|
// Default to a non-caching page.
|
|
// Also ensures that a Cache-control header exists.
|
|
$this->noCache();
|
|
|
|
// Are we enforcing a Content Security Policy?
|
|
if ($config->CSPEnabled === true)
|
|
{
|
|
$this->CSP = new ContentSecurityPolicy(new \Config\ContentSecurityPolicy());
|
|
$this->CSPEnabled = true;
|
|
}
|
|
|
|
$this->cookiePrefix = $config->cookiePrefix;
|
|
$this->cookieDomain = $config->cookieDomain;
|
|
$this->cookiePath = $config->cookiePath;
|
|
$this->cookieSecure = $config->cookieSecure;
|
|
$this->cookieHTTPOnly = $config->cookieHTTPOnly;
|
|
|
|
// Default to an HTML Content-Type. Devs can override if needed.
|
|
$this->setContentType('text/html');
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Turns "pretend" mode on or off to aid in testing.
|
|
*
|
|
* @param boolean $pretend
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function pretend(bool $pretend = true)
|
|
{
|
|
$this->pretend = $pretend;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Gets the response status code.
|
|
*
|
|
* The status code is a 3-digit integer result code of the getServer's attempt
|
|
* to understand and satisfy the request.
|
|
*
|
|
* @return integer Status code.
|
|
*/
|
|
public function getStatusCode(): int
|
|
{
|
|
if (empty($this->statusCode))
|
|
{
|
|
throw HTTPException::forMissingResponseStatus();
|
|
}
|
|
|
|
return $this->statusCode;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Return an instance with the specified status code and, optionally, reason phrase.
|
|
*
|
|
* If no reason phrase is specified, will default recommended reason phrase for
|
|
* the response's status code.
|
|
*
|
|
* @see http://tools.ietf.org/html/rfc7231#section-6
|
|
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
|
*
|
|
* @param integer $code The 3-digit integer result code to set.
|
|
* @param string $reason The reason phrase to use with the
|
|
* provided status code; if none is provided, will
|
|
* default to the IANA name.
|
|
*
|
|
* @return $this
|
|
* @throws \CodeIgniter\HTTP\Exceptions\HTTPException For invalid status code arguments.
|
|
*/
|
|
public function setStatusCode(int $code, string $reason = '')
|
|
{
|
|
// Valid range?
|
|
if ($code < 100 || $code > 599)
|
|
{
|
|
throw HTTPException::forInvalidStatusCode($code);
|
|
}
|
|
|
|
// Unknown and no message?
|
|
if (! array_key_exists($code, static::$statusCodes) && empty($reason))
|
|
{
|
|
throw HTTPException::forUnkownStatusCode($code);
|
|
}
|
|
|
|
$this->statusCode = $code;
|
|
|
|
if (! empty($reason))
|
|
{
|
|
$this->reason = $reason;
|
|
}
|
|
else
|
|
{
|
|
$this->reason = static::$statusCodes[$code];
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Gets the response response phrase associated with the status code.
|
|
*
|
|
* @see http://tools.ietf.org/html/rfc7231#section-6
|
|
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getReason(): string
|
|
{
|
|
if (empty($this->reason))
|
|
{
|
|
return ! empty($this->statusCode) ? static::$statusCodes[$this->statusCode] : '';
|
|
}
|
|
|
|
return $this->reason;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
// Convenience Methods
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sets the date header
|
|
*
|
|
* @param \DateTime $date
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function setDate(\DateTime $date)
|
|
{
|
|
$date->setTimezone(new \DateTimeZone('UTC'));
|
|
|
|
$this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT');
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Set the Link Header
|
|
*
|
|
* @param \CodeIgniter\Pager\PagerInterface $pager
|
|
*
|
|
* @see http://tools.ietf.org/html/rfc5988
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function setLink(PagerInterface $pager)
|
|
{
|
|
$links = '';
|
|
|
|
if ($previous = $pager->getPreviousPageURI())
|
|
{
|
|
$links .= '<' . $pager->getPageURI($pager->getFirstPage()) . '>; rel="first",';
|
|
$links .= '<' . $previous . '>; rel="prev"';
|
|
}
|
|
|
|
if (($next = $pager->getNextPageURI()) && $previous)
|
|
{
|
|
$links .= ',';
|
|
}
|
|
|
|
if ($next)
|
|
{
|
|
$links .= '<' . $next . '>; rel="next",';
|
|
$links .= '<' . $pager->getPageURI($pager->getLastPage()) . '>; rel="last"';
|
|
}
|
|
|
|
$this->setHeader('Link', $links);
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sets the Content Type header for this response with the mime type
|
|
* and, optionally, the charset.
|
|
*
|
|
* @param string $mime
|
|
* @param string $charset
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function setContentType(string $mime, string $charset = 'UTF-8')
|
|
{
|
|
// add charset attribute if not already there and provided as parm
|
|
if ((strpos($mime, 'charset=') < 1) && ! empty($charset))
|
|
{
|
|
$mime .= '; charset=' . $charset;
|
|
}
|
|
|
|
$this->removeHeader('Content-Type'); // replace existing content type
|
|
$this->setHeader('Content-Type', $mime);
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Converts the $body into JSON and sets the Content Type header.
|
|
*
|
|
* @param array|string $body
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setJSON($body)
|
|
{
|
|
$this->body = $this->formatBody($body, 'json');
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns the current body, converted to JSON is it isn't already.
|
|
*
|
|
* @return mixed|string
|
|
*
|
|
* @throws \InvalidArgumentException If the body property is not array.
|
|
*/
|
|
public function getJSON()
|
|
{
|
|
$body = $this->body;
|
|
|
|
if ($this->bodyFormat !== 'json')
|
|
{
|
|
/**
|
|
* @var Format $config
|
|
*/
|
|
$config = config(Format::class);
|
|
$formatter = $config->getFormatter('application/json');
|
|
|
|
$body = $formatter->format($body);
|
|
}
|
|
|
|
return $body ?: null;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Converts $body into XML, and sets the correct Content-Type.
|
|
*
|
|
* @param array|string $body
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setXML($body)
|
|
{
|
|
$this->body = $this->formatBody($body, 'xml');
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Retrieves the current body into XML and returns it.
|
|
*
|
|
* @return mixed|string
|
|
* @throws \InvalidArgumentException If the body property is not array.
|
|
*/
|
|
public function getXML()
|
|
{
|
|
$body = $this->body;
|
|
|
|
if ($this->bodyFormat !== 'xml')
|
|
{
|
|
/**
|
|
* @var Format $config
|
|
*/
|
|
$config = config(Format::class);
|
|
$formatter = $config->getFormatter('application/xml');
|
|
|
|
$body = $formatter->format($body);
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Handles conversion of the of the data into the appropriate format,
|
|
* and sets the correct Content-Type header for our response.
|
|
*
|
|
* @param string|array $body
|
|
* @param string $format Valid: json, xml
|
|
*
|
|
* @return mixed
|
|
* @throws \InvalidArgumentException If the body property is not string or array.
|
|
*/
|
|
protected function formatBody($body, string $format)
|
|
{
|
|
$mime = "application/{$format}";
|
|
$this->setContentType($mime);
|
|
$this->bodyFormat = $format;
|
|
|
|
// Nothing much to do for a string...
|
|
if (! is_string($body))
|
|
{
|
|
/**
|
|
* @var Format $config
|
|
*/
|
|
$config = config(Format::class);
|
|
$formatter = $config->getFormatter($mime);
|
|
|
|
$body = $formatter->format($body);
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
//--------------------------------------------------------------------
|
|
// Cache Control Methods
|
|
//
|
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sets the appropriate headers to ensure this response
|
|
* is not cached by the browsers.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function noCache(): self
|
|
{
|
|
$this->removeHeader('Cache-control');
|
|
|
|
$this->setHeader('Cache-control', ['no-store', 'max-age=0', 'no-cache']);
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* A shortcut method that allows the developer to set all of the
|
|
* cache-control headers in one method call.
|
|
*
|
|
* The options array is used to provide the cache-control directives
|
|
* for the header. It might look something like:
|
|
*
|
|
* $options = [
|
|
* 'max-age' => 300,
|
|
* 's-maxage' => 900
|
|
* 'etag' => 'abcde',
|
|
* ];
|
|
*
|
|
* Typical options are:
|
|
* - etag
|
|
* - last-modified
|
|
* - max-age
|
|
* - s-maxage
|
|
* - private
|
|
* - public
|
|
* - must-revalidate
|
|
* - proxy-revalidate
|
|
* - no-transform
|
|
*
|
|
* @param array $options
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function setCache(array $options = [])
|
|
{
|
|
if (empty($options))
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
$this->removeHeader('Cache-Control');
|
|
$this->removeHeader('ETag');
|
|
|
|
// ETag
|
|
if (isset($options['etag']))
|
|
{
|
|
$this->setHeader('ETag', $options['etag']);
|
|
unset($options['etag']);
|
|
}
|
|
|
|
// Last Modified
|
|
if (isset($options['last-modified']))
|
|
{
|
|
$this->setLastModified($options['last-modified']);
|
|
|
|
unset($options['last-modified']);
|
|
}
|
|
|
|
$this->setHeader('Cache-control', $options);
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sets the Last-Modified date header.
|
|
*
|
|
* $date can be either a string representation of the date or,
|
|
* preferably, an instance of DateTime.
|
|
*
|
|
* @param \DateTime|string $date
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function setLastModified($date)
|
|
{
|
|
if ($date instanceof \DateTime)
|
|
{
|
|
$date->setTimezone(new \DateTimeZone('UTC'));
|
|
$this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
|
|
}
|
|
elseif (is_string($date))
|
|
{
|
|
$this->setHeader('Last-Modified', $date);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
//--------------------------------------------------------------------
|
|
// Output Methods
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sends the output to the browser.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function send()
|
|
{
|
|
// If we're enforcing a Content Security Policy,
|
|
// we need to give it a chance to build out it's headers.
|
|
if ($this->CSPEnabled === true)
|
|
{
|
|
$this->CSP->finalize($this);
|
|
}
|
|
else
|
|
{
|
|
$this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body);
|
|
}
|
|
|
|
$this->sendHeaders();
|
|
$this->sendCookies();
|
|
$this->sendBody();
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sends the headers of this HTTP request to the browser.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function sendHeaders()
|
|
{
|
|
// Have the headers already been sent?
|
|
if ($this->pretend || headers_sent())
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
// Per spec, MUST be sent with each request, if possible.
|
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
|
if (! isset($this->headers['Date']))
|
|
{
|
|
$this->setDate(\DateTime::createFromFormat('U', (string) time()));
|
|
}
|
|
|
|
// HTTP Status
|
|
header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->statusCode, $this->reason), true, $this->statusCode);
|
|
|
|
// Send all of our headers
|
|
foreach ($this->getHeaders() as $name => $values)
|
|
{
|
|
header($name . ': ' . $this->getHeaderLine($name), false, $this->statusCode);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sends the Body of the message to the browser.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function sendBody()
|
|
{
|
|
echo $this->body;
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Grabs the current body.
|
|
*
|
|
* @return mixed|string
|
|
*/
|
|
public function getBody()
|
|
{
|
|
return $this->body;
|
|
}
|
|
|
|
/**
|
|
* Perform a redirect to a new URL, in two flavors: header or location.
|
|
*
|
|
* @param string $uri The URI to redirect to
|
|
* @param string $method
|
|
* @param integer $code The type of redirection, defaults to 302
|
|
*
|
|
* @return $this
|
|
* @throws \CodeIgniter\HTTP\Exceptions\HTTPException For invalid status code.
|
|
*/
|
|
public function redirect(string $uri, string $method = 'auto', int $code = null)
|
|
{
|
|
// Assume 302 status code response; override if needed
|
|
if (empty($code))
|
|
{
|
|
$code = 302;
|
|
}
|
|
|
|
// IIS environment likely? Use 'refresh' for better compatibility
|
|
if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false)
|
|
{
|
|
$method = 'refresh';
|
|
}
|
|
|
|
// override status code for HTTP/1.1 & higher
|
|
// reference: http://en.wikipedia.org/wiki/Post/Redirect/Get
|
|
if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1)
|
|
{
|
|
if ($method !== 'refresh')
|
|
{
|
|
$code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 : 307;
|
|
}
|
|
}
|
|
|
|
switch ($method)
|
|
{
|
|
case 'refresh':
|
|
$this->setHeader('Refresh', '0;url=' . $uri);
|
|
break;
|
|
default:
|
|
$this->setHeader('Location', $uri);
|
|
break;
|
|
}
|
|
|
|
$this->setStatusCode($code);
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Set a cookie
|
|
*
|
|
* Accepts an arbitrary number of binds (up to 7) or an associative
|
|
* array in the first parameter containing all the values.
|
|
*
|
|
* @param string|array $name Cookie name or array containing binds
|
|
* @param string $value Cookie value
|
|
* @param string $expire Cookie expiration time in seconds
|
|
* @param string $domain Cookie domain (e.g.: '.yourdomain.com')
|
|
* @param string $path Cookie path (default: '/')
|
|
* @param string $prefix Cookie name prefix
|
|
* @param boolean|false $secure Whether to only transfer cookies via SSL
|
|
* @param boolean|false $httponly Whether only make the cookie accessible via HTTP (no javascript)
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setCookie(
|
|
$name,
|
|
$value = '',
|
|
$expire = '',
|
|
$domain = '',
|
|
$path = '/',
|
|
$prefix = '',
|
|
$secure = false,
|
|
$httponly = false
|
|
)
|
|
{
|
|
if (is_array($name))
|
|
{
|
|
// always leave 'name' in last place, as the loop will break otherwise, due to $$item
|
|
foreach (['value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item)
|
|
{
|
|
if (isset($name[$item]))
|
|
{
|
|
$$item = $name[$item];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($prefix === '' && $this->cookiePrefix !== '')
|
|
{
|
|
$prefix = $this->cookiePrefix;
|
|
}
|
|
|
|
if ($domain === '' && $this->cookieDomain !== '')
|
|
{
|
|
$domain = $this->cookieDomain;
|
|
}
|
|
|
|
if ($path === '/' && $this->cookiePath !== '/')
|
|
{
|
|
$path = $this->cookiePath;
|
|
}
|
|
|
|
if ($secure === false && $this->cookieSecure === true)
|
|
{
|
|
$secure = $this->cookieSecure;
|
|
}
|
|
|
|
if ($httponly === false && $this->cookieHTTPOnly !== false)
|
|
{
|
|
$httponly = $this->cookieHTTPOnly;
|
|
}
|
|
|
|
if (! is_numeric($expire))
|
|
{
|
|
$expire = time() - 86500;
|
|
}
|
|
else
|
|
{
|
|
$expire = ($expire > 0) ? time() + $expire : 0;
|
|
}
|
|
|
|
$this->cookies[] = [
|
|
'name' => $prefix . $name,
|
|
'value' => $value,
|
|
'expires' => $expire,
|
|
'path' => $path,
|
|
'domain' => $domain,
|
|
'secure' => $secure,
|
|
'httponly' => $httponly,
|
|
];
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Checks to see if the Response has a specified cookie or not.
|
|
*
|
|
* @param string $name
|
|
* @param string|null $value
|
|
* @param string $prefix
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function hasCookie(string $name, string $value = null, string $prefix = ''): bool
|
|
{
|
|
if ($prefix === '' && $this->cookiePrefix !== '')
|
|
{
|
|
$prefix = $this->cookiePrefix;
|
|
}
|
|
|
|
$name = $prefix . $name;
|
|
|
|
foreach ($this->cookies as $cookie)
|
|
{
|
|
if ($cookie['name'] !== $name)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($value === null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return $cookie['value'] === $value;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the cookie
|
|
*
|
|
* @param string|null $name
|
|
* @param string $prefix
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getCookie(string $name = null, string $prefix = '')
|
|
{
|
|
// if no name given, return them all
|
|
if (empty($name))
|
|
{
|
|
return $this->cookies;
|
|
}
|
|
|
|
if ($prefix === '' && $this->cookiePrefix !== '')
|
|
{
|
|
$prefix = $this->cookiePrefix;
|
|
}
|
|
|
|
$name = $prefix . $name;
|
|
|
|
foreach ($this->cookies as $cookie)
|
|
{
|
|
if ($cookie['name'] === $name)
|
|
{
|
|
return $cookie;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets a cookie to be deleted when the response is sent.
|
|
*
|
|
* @param string $name
|
|
* @param string $domain
|
|
* @param string $path
|
|
* @param string $prefix
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '')
|
|
{
|
|
if (empty($name))
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
if ($prefix === '' && $this->cookiePrefix !== '')
|
|
{
|
|
$prefix = $this->cookiePrefix;
|
|
}
|
|
|
|
$name = $prefix . $name;
|
|
|
|
foreach ($this->cookies as &$cookie)
|
|
{
|
|
if ($cookie['name'] === $name)
|
|
{
|
|
if (! empty($domain) && $cookie['domain'] !== $domain)
|
|
{
|
|
continue;
|
|
}
|
|
if (! empty($path) && $cookie['path'] !== $path)
|
|
{
|
|
continue;
|
|
}
|
|
$cookie['value'] = '';
|
|
$cookie['expires'] = '';
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Actually sets the cookies.
|
|
*/
|
|
protected function sendCookies()
|
|
{
|
|
if ($this->pretend)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach ($this->cookies as $params)
|
|
{
|
|
// PHP cannot unpack array with string keys
|
|
$params = array_values($params);
|
|
|
|
setcookie(...$params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force a download.
|
|
*
|
|
* Generates the headers that force a download to happen. And
|
|
* sends the file to the browser.
|
|
*
|
|
* @param string $filename The path to the file to send
|
|
* @param string|null $data The data to be downloaded
|
|
* @param boolean $setMime Whether to try and send the actual MIME type
|
|
*
|
|
* @return \CodeIgniter\HTTP\DownloadResponse|null
|
|
*/
|
|
public function download(string $filename = '', $data = '', bool $setMime = false)
|
|
{
|
|
if ($filename === '' || $data === '')
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$filepath = '';
|
|
if ($data === null)
|
|
{
|
|
$filepath = $filename;
|
|
$filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename));
|
|
$filename = end($filename);
|
|
}
|
|
|
|
$response = new DownloadResponse($filename, $setMime);
|
|
|
|
if ($filepath !== '')
|
|
{
|
|
$response->setFilePath($filepath);
|
|
}
|
|
elseif ($data !== null)
|
|
{
|
|
$response->setBinary($data);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
}
|
|
|