first commit
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use Config\App;
|
||||
use Locale;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a request from the command-line. Provides additional
|
||||
* tools to interact with that request since CLI requests are not
|
||||
* static like HTTP requests might be.
|
||||
*
|
||||
* Portions of this code were initially from the FuelPHP Framework,
|
||||
* version 1.7.x, and used here under the MIT license they were
|
||||
* originally made available under.
|
||||
*
|
||||
* http://fuelphp.com
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\CLIRequestTest
|
||||
*/
|
||||
class CLIRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Stores the segments of our cli "URI" command.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $segments = [];
|
||||
|
||||
/**
|
||||
* Command line options and their values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* Command line arguments (segments and options).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $args = [];
|
||||
|
||||
/**
|
||||
* Set the expected HTTP verb
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $method = 'CLI';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(App $config)
|
||||
{
|
||||
if (! is_cli()) {
|
||||
throw new RuntimeException(static::class . ' needs to run from the command line.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
parent::__construct($config);
|
||||
|
||||
// Don't terminate the script when the cli's tty goes away
|
||||
ignore_user_abort(true);
|
||||
|
||||
$this->parseCommand();
|
||||
|
||||
// Set SiteURI for this request
|
||||
$this->uri = new SiteURI($config, $this->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "path" of the request script so that it can be used
|
||||
* in routing to the appropriate controller/method.
|
||||
*
|
||||
* The path is determined by treating the command line arguments
|
||||
* as if it were a URL - up until we hit our first option.
|
||||
*
|
||||
* Example:
|
||||
* php index.php users 21 profile -foo bar
|
||||
*
|
||||
* // Routes to /users/21/profile (index is removed for routing sake)
|
||||
* // with the option foo = bar.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
$path = implode('/', $this->segments);
|
||||
|
||||
return ($path === '') ? '' : $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array of all CLI options found, with
|
||||
* their values.
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all CLI arguments (segments and options).
|
||||
*/
|
||||
public function getArgs(): array
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path segments.
|
||||
*/
|
||||
public function getSegments(): array
|
||||
{
|
||||
return $this->segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for a single CLI option that was passed in.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOption(string $key)
|
||||
{
|
||||
return $this->options[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options as a string, suitable for passing along on
|
||||
* the CLI to other commands.
|
||||
*
|
||||
* Example:
|
||||
* $options = [
|
||||
* 'foo' => 'bar',
|
||||
* 'baz' => 'queue some stuff'
|
||||
* ];
|
||||
*
|
||||
* getOptionString() = '-foo bar -baz "queue some stuff"'
|
||||
*/
|
||||
public function getOptionString(bool $useLongOpts = false): string
|
||||
{
|
||||
if ($this->options === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = '';
|
||||
|
||||
foreach ($this->options as $name => $value) {
|
||||
if ($useLongOpts && mb_strlen($name) > 1) {
|
||||
$out .= "--{$name} ";
|
||||
} else {
|
||||
$out .= "-{$name} ";
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mb_strpos($value, ' ') !== false) {
|
||||
$out .= '"' . $value . '" ';
|
||||
} else {
|
||||
$out .= "{$value} ";
|
||||
}
|
||||
}
|
||||
|
||||
return trim($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the command line it was called from and collects all options
|
||||
* and valid segments.
|
||||
*
|
||||
* NOTE: I tried to use getopt but had it fail occasionally to find
|
||||
* any options, where argv has always had our back.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseCommand()
|
||||
{
|
||||
$args = $this->getServer('argv');
|
||||
array_shift($args); // Scrap index.php
|
||||
|
||||
$optionValue = false;
|
||||
|
||||
foreach ($args as $i => $arg) {
|
||||
if (mb_strpos($arg, '-') !== 0) {
|
||||
if ($optionValue) {
|
||||
$optionValue = false;
|
||||
} else {
|
||||
$this->segments[] = $arg;
|
||||
$this->args[] = $arg;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$arg = ltrim($arg, '-');
|
||||
$value = null;
|
||||
|
||||
if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
|
||||
$value = $args[$i + 1];
|
||||
$optionValue = true;
|
||||
}
|
||||
|
||||
$this->options[$arg] = $value;
|
||||
$this->args[$arg] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this request was made from the command line (CLI).
|
||||
*/
|
||||
public function isCLI(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from GET data.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_GET.
|
||||
* @param int|null $filter A filter name to apply.
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getGet($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->returnNullOrEmptyArray($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from POST.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_POST.
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getPost($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->returnNullOrEmptyArray($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from POST data with fallback to GET.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_POST or $_GET
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getPostGet($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->returnNullOrEmptyArray($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from GET data with fallback to POST.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_GET or $_POST
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getGetPost($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->returnNullOrEmptyArray($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a place holder for calls from cookie_helper get_cookie().
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_COOKIE
|
||||
* @param int|null $filter A filter name to be applied
|
||||
* @param mixed $flags
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCookie($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->returnNullOrEmptyArray($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string|null $index
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function returnNullOrEmptyArray($index)
|
||||
{
|
||||
return ($index === null || is_array($index)) ? [] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current locale, with a fallback to the default
|
||||
* locale if none is set.
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
return Locale::getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks this request type.
|
||||
*
|
||||
* @param string $type HTTP verb or 'json' or 'ajax'
|
||||
* @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type
|
||||
*/
|
||||
public function is(string $type): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use Config\App;
|
||||
use Config\CURLRequest as ConfigCURLRequest;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A lightweight HTTP client for sending synchronous HTTP requests via cURL.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\CURLRequestTest
|
||||
*/
|
||||
class CURLRequest extends OutgoingRequest
|
||||
{
|
||||
/**
|
||||
* The response object associated with this request
|
||||
*
|
||||
* @var ResponseInterface|null
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* The original response object associated with this request
|
||||
*
|
||||
* @var ResponseInterface|null
|
||||
*/
|
||||
protected $responseOrig;
|
||||
|
||||
/**
|
||||
* The URI associated with this request
|
||||
*
|
||||
* @var URI
|
||||
*/
|
||||
protected $baseURI;
|
||||
|
||||
/**
|
||||
* The setting values
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The default setting values
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultConfig = [
|
||||
'timeout' => 0.0,
|
||||
'connect_timeout' => 150,
|
||||
'debug' => false,
|
||||
'verify' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Default values for when 'allow_redirects'
|
||||
* option is true.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $redirectDefaults = [
|
||||
'max' => 5,
|
||||
'strict' => true,
|
||||
'protocols' => [
|
||||
'http',
|
||||
'https',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The number of milliseconds to delay before
|
||||
* sending the request.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $delay = 0.0;
|
||||
|
||||
/**
|
||||
* The default options from the constructor. Applied to all requests.
|
||||
*/
|
||||
private readonly array $defaultOptions;
|
||||
|
||||
/**
|
||||
* Whether share options between requests or not.
|
||||
*
|
||||
* If true, all the options won't be reset between requests.
|
||||
* It may cause an error request with unnecessary headers.
|
||||
*/
|
||||
private readonly bool $shareOptions;
|
||||
|
||||
/**
|
||||
* Takes an array of options to set the following possible class properties:
|
||||
*
|
||||
* - baseURI
|
||||
* - timeout
|
||||
* - any other request options to use as defaults.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
|
||||
{
|
||||
if (! function_exists('curl_version')) {
|
||||
throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
parent::__construct(Method::GET, $uri);
|
||||
|
||||
$this->responseOrig = $response ?? new Response($config);
|
||||
// Remove the default Content-Type header.
|
||||
$this->responseOrig->removeHeader('Content-Type');
|
||||
|
||||
$this->baseURI = $uri->useRawQueryString();
|
||||
$this->defaultOptions = $options;
|
||||
|
||||
/** @var ConfigCURLRequest|null $configCURLRequest */
|
||||
$configCURLRequest = config(ConfigCURLRequest::class);
|
||||
$this->shareOptions = $configCURLRequest->shareOptions ?? true;
|
||||
|
||||
$this->config = $this->defaultConfig;
|
||||
$this->parseOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request to the specified $url. If this is a relative
|
||||
* URL, it will be merged with $this->baseURI to form a complete URL.
|
||||
*
|
||||
* @param string $method HTTP method
|
||||
*/
|
||||
public function request($method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$this->response = clone $this->responseOrig;
|
||||
|
||||
$this->parseOptions($options);
|
||||
|
||||
$url = $this->prepareURL($url);
|
||||
|
||||
$method = esc(strip_tags($method));
|
||||
|
||||
$this->send($method, $url);
|
||||
|
||||
if ($this->shareOptions === false) {
|
||||
$this->resetOptions();
|
||||
}
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all options to default.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function resetOptions()
|
||||
{
|
||||
// Reset headers
|
||||
$this->headers = [];
|
||||
$this->headerMap = [];
|
||||
|
||||
// Reset body
|
||||
$this->body = null;
|
||||
|
||||
// Reset configs
|
||||
$this->config = $this->defaultConfig;
|
||||
|
||||
// Set the default options for next request
|
||||
$this->parseOptions($this->defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a GET request.
|
||||
*/
|
||||
public function get(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request(Method::GET, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a DELETE request.
|
||||
*/
|
||||
public function delete(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('DELETE', $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a HEAD request.
|
||||
*/
|
||||
public function head(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('HEAD', $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending an OPTIONS request.
|
||||
*/
|
||||
public function options(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('OPTIONS', $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a PATCH request.
|
||||
*/
|
||||
public function patch(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('PATCH', $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a POST request.
|
||||
*/
|
||||
public function post(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request(Method::POST, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for sending a PUT request.
|
||||
*/
|
||||
public function put(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request(Method::PUT, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP Authentication.
|
||||
*
|
||||
* @param string $type basic or digest
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuth(string $username, string $password, string $type = 'basic')
|
||||
{
|
||||
$this->config['auth'] = [
|
||||
$username,
|
||||
$password,
|
||||
$type,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set form data to be sent.
|
||||
*
|
||||
* @param bool $multipart Set TRUE if you are sending CURLFiles
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setForm(array $params, bool $multipart = false)
|
||||
{
|
||||
if ($multipart) {
|
||||
$this->config['multipart'] = $params;
|
||||
} else {
|
||||
$this->config['form_params'] = $params;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set JSON data to be sent.
|
||||
*
|
||||
* @param array|bool|float|int|object|string|null $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setJSON($data)
|
||||
{
|
||||
$this->config['json'] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the correct settings based on the options array
|
||||
* passed in.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parseOptions(array $options)
|
||||
{
|
||||
if (array_key_exists('baseURI', $options)) {
|
||||
$this->baseURI = $this->baseURI->setURI($options['baseURI']);
|
||||
unset($options['baseURI']);
|
||||
}
|
||||
|
||||
if (array_key_exists('headers', $options) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $name => $value) {
|
||||
$this->setHeader($name, $value);
|
||||
}
|
||||
|
||||
unset($options['headers']);
|
||||
}
|
||||
|
||||
if (array_key_exists('delay', $options)) {
|
||||
// Convert from the milliseconds passed in
|
||||
// to the seconds that sleep requires.
|
||||
$this->delay = (float) $options['delay'] / 1000;
|
||||
unset($options['delay']);
|
||||
}
|
||||
|
||||
if (array_key_exists('body', $options)) {
|
||||
$this->setBody($options['body']);
|
||||
unset($options['body']);
|
||||
}
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
$this->config[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the $url is a relative URL, will attempt to create
|
||||
* a full URL by prepending $this->baseURI to it.
|
||||
*/
|
||||
protected function prepareURL(string $url): string
|
||||
{
|
||||
// If it's a full URI, then we have nothing to do here...
|
||||
if (str_contains($url, '://')) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$uri = $this->baseURI->resolveRelativeURI($url);
|
||||
|
||||
// Create the string instead of casting to prevent baseURL muddling
|
||||
return URI::createURIString(
|
||||
$uri->getScheme(),
|
||||
$uri->getAuthority(),
|
||||
$uri->getPath(),
|
||||
$uri->getQuery(),
|
||||
$uri->getFragment()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the actual cURL request.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function send(string $method, string $url)
|
||||
{
|
||||
// Reset our curl options so we're on a fresh slate.
|
||||
$curlOptions = [];
|
||||
|
||||
if (! empty($this->config['query']) && is_array($this->config['query'])) {
|
||||
// This is likely too naive a solution.
|
||||
// Should look into handling when $url already
|
||||
// has query vars on it.
|
||||
$url .= '?' . http_build_query($this->config['query']);
|
||||
unset($this->config['query']);
|
||||
}
|
||||
|
||||
$curlOptions[CURLOPT_URL] = $url;
|
||||
$curlOptions[CURLOPT_RETURNTRANSFER] = true;
|
||||
$curlOptions[CURLOPT_HEADER] = true;
|
||||
$curlOptions[CURLOPT_FRESH_CONNECT] = true;
|
||||
// Disable @file uploads in post data.
|
||||
$curlOptions[CURLOPT_SAFE_UPLOAD] = true;
|
||||
|
||||
$curlOptions = $this->setCURLOptions($curlOptions, $this->config);
|
||||
$curlOptions = $this->applyMethod($method, $curlOptions);
|
||||
$curlOptions = $this->applyRequestHeaders($curlOptions);
|
||||
|
||||
// Do we need to delay this request?
|
||||
if ($this->delay > 0) {
|
||||
usleep((int) $this->delay * 1_000_000);
|
||||
}
|
||||
|
||||
$output = $this->sendRequest($curlOptions);
|
||||
|
||||
// Set the string we want to break our response from
|
||||
$breakString = "\r\n\r\n";
|
||||
|
||||
while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
|
||||
$output = substr($output, strpos($output, $breakString) + 4);
|
||||
}
|
||||
|
||||
if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) {
|
||||
$output = substr($output, strpos($output, $breakString) + 4);
|
||||
}
|
||||
|
||||
// If request and response have Digest
|
||||
if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
|
||||
$output = substr($output, strpos($output, $breakString) + 4);
|
||||
}
|
||||
|
||||
// Split out our headers and body
|
||||
$break = strpos($output, $breakString);
|
||||
|
||||
if ($break !== false) {
|
||||
// Our headers
|
||||
$headers = explode("\n", substr($output, 0, $break));
|
||||
|
||||
$this->setResponseHeaders($headers);
|
||||
|
||||
// Our body
|
||||
$body = substr($output, $break + 4);
|
||||
$this->response->setBody($body);
|
||||
} else {
|
||||
$this->response->setBody($output);
|
||||
}
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds $this->headers to the cURL request.
|
||||
*/
|
||||
protected function applyRequestHeaders(array $curlOptions = []): array
|
||||
{
|
||||
if (empty($this->headers)) {
|
||||
return $curlOptions;
|
||||
}
|
||||
|
||||
$set = [];
|
||||
|
||||
foreach (array_keys($this->headers) as $name) {
|
||||
$set[] = $name . ': ' . $this->getHeaderLine($name);
|
||||
}
|
||||
|
||||
$curlOptions[CURLOPT_HTTPHEADER] = $set;
|
||||
|
||||
return $curlOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply method
|
||||
*/
|
||||
protected function applyMethod(string $method, array $curlOptions): array
|
||||
{
|
||||
$this->method = $method;
|
||||
$curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
|
||||
|
||||
$size = strlen($this->body ?? '');
|
||||
|
||||
// Have content?
|
||||
if ($size > 0) {
|
||||
return $this->applyBody($curlOptions);
|
||||
}
|
||||
|
||||
if ($method === Method::PUT || $method === Method::POST) {
|
||||
// See http://tools.ietf.org/html/rfc7230#section-3.3.2
|
||||
if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
|
||||
$this->setHeader('Content-Length', '0');
|
||||
}
|
||||
} elseif ($method === 'HEAD') {
|
||||
$curlOptions[CURLOPT_NOBODY] = 1;
|
||||
}
|
||||
|
||||
return $curlOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply body
|
||||
*/
|
||||
protected function applyBody(array $curlOptions = []): array
|
||||
{
|
||||
if (! empty($this->body)) {
|
||||
$curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
|
||||
}
|
||||
|
||||
return $curlOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the header retrieved from the cURL response into
|
||||
* our Response object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setResponseHeaders(array $headers = [])
|
||||
{
|
||||
foreach ($headers as $header) {
|
||||
if (($pos = strpos($header, ':')) !== false) {
|
||||
$title = trim(substr($header, 0, $pos));
|
||||
$value = trim(substr($header, $pos + 1));
|
||||
|
||||
if ($this->response instanceof Response) {
|
||||
$this->response->addHeader($title, $value);
|
||||
} else {
|
||||
$this->response->setHeader($title, $value);
|
||||
}
|
||||
} elseif (str_starts_with($header, 'HTTP')) {
|
||||
preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
|
||||
|
||||
if (isset($matches[1])) {
|
||||
$this->response->setProtocolVersion($matches[1]);
|
||||
}
|
||||
|
||||
if (isset($matches[2])) {
|
||||
$this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CURL options
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function setCURLOptions(array $curlOptions = [], array $config = [])
|
||||
{
|
||||
// Auth Headers
|
||||
if (! empty($config['auth'])) {
|
||||
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
|
||||
|
||||
if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
|
||||
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
|
||||
} else {
|
||||
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
|
||||
}
|
||||
}
|
||||
|
||||
// Certificate
|
||||
if (! empty($config['cert'])) {
|
||||
$cert = $config['cert'];
|
||||
|
||||
if (is_array($cert)) {
|
||||
$curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
|
||||
$cert = $cert[0];
|
||||
}
|
||||
|
||||
if (! is_file($cert)) {
|
||||
throw HTTPException::forSSLCertNotFound($cert);
|
||||
}
|
||||
|
||||
$curlOptions[CURLOPT_SSLCERT] = $cert;
|
||||
}
|
||||
|
||||
// SSL Verification
|
||||
if (isset($config['verify'])) {
|
||||
if (is_string($config['verify'])) {
|
||||
$file = realpath($config['verify']) ?: $config['verify'];
|
||||
|
||||
if (! is_file($file)) {
|
||||
throw HTTPException::forInvalidSSLKey($config['verify']);
|
||||
}
|
||||
|
||||
$curlOptions[CURLOPT_CAINFO] = $file;
|
||||
$curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
|
||||
} elseif (is_bool($config['verify'])) {
|
||||
$curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
|
||||
$curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy
|
||||
if (isset($config['proxy'])) {
|
||||
$curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
|
||||
$curlOptions[CURLOPT_PROXY] = $config['proxy'];
|
||||
}
|
||||
|
||||
// Debug
|
||||
if ($config['debug']) {
|
||||
$curlOptions[CURLOPT_VERBOSE] = 1;
|
||||
$curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
|
||||
}
|
||||
|
||||
// Decode Content
|
||||
if (! empty($config['decode_content'])) {
|
||||
$accept = $this->getHeaderLine('Accept-Encoding');
|
||||
|
||||
if ($accept !== '') {
|
||||
$curlOptions[CURLOPT_ENCODING] = $accept;
|
||||
} else {
|
||||
$curlOptions[CURLOPT_ENCODING] = '';
|
||||
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Redirects
|
||||
if (array_key_exists('allow_redirects', $config)) {
|
||||
$settings = $this->redirectDefaults;
|
||||
|
||||
if (is_array($config['allow_redirects'])) {
|
||||
$settings = array_merge($settings, $config['allow_redirects']);
|
||||
}
|
||||
|
||||
if ($config['allow_redirects'] === false) {
|
||||
$curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
|
||||
} else {
|
||||
$curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
|
||||
$curlOptions[CURLOPT_MAXREDIRS] = $settings['max'];
|
||||
|
||||
if ($settings['strict'] === true) {
|
||||
$curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
|
||||
}
|
||||
|
||||
$protocols = 0;
|
||||
|
||||
foreach ($settings['protocols'] as $proto) {
|
||||
$protocols += constant('CURLPROTO_' . strtoupper($proto));
|
||||
}
|
||||
|
||||
$curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout
|
||||
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
|
||||
|
||||
// Connection Timeout
|
||||
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
|
||||
|
||||
// Post Data - application/x-www-form-urlencoded
|
||||
if (! empty($config['form_params']) && is_array($config['form_params'])) {
|
||||
$postFields = http_build_query($config['form_params']);
|
||||
$curlOptions[CURLOPT_POSTFIELDS] = $postFields;
|
||||
|
||||
// Ensure content-length is set, since CURL doesn't seem to
|
||||
// calculate it when HTTPHEADER is set.
|
||||
$this->setHeader('Content-Length', (string) strlen($postFields));
|
||||
$this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
|
||||
// Post Data - multipart/form-data
|
||||
if (! empty($config['multipart']) && is_array($config['multipart'])) {
|
||||
// setting the POSTFIELDS option automatically sets multipart
|
||||
$curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
|
||||
}
|
||||
|
||||
// HTTP Errors
|
||||
$curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
|
||||
|
||||
// JSON
|
||||
if (isset($config['json'])) {
|
||||
// Will be set as the body in `applyBody()`
|
||||
$json = json_encode($config['json']);
|
||||
$this->setBody($json);
|
||||
$this->setHeader('Content-Type', 'application/json');
|
||||
$this->setHeader('Content-Length', (string) strlen($json));
|
||||
}
|
||||
|
||||
// version
|
||||
if (! empty($config['version'])) {
|
||||
$version = sprintf('%.1F', $config['version']);
|
||||
if ($version === '1.0') {
|
||||
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
|
||||
} elseif ($version === '1.1') {
|
||||
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
|
||||
} elseif ($version === '2.0') {
|
||||
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
|
||||
}
|
||||
}
|
||||
|
||||
// Cookie
|
||||
if (isset($config['cookie'])) {
|
||||
$curlOptions[CURLOPT_COOKIEJAR] = $config['cookie'];
|
||||
$curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
|
||||
}
|
||||
|
||||
// User Agent
|
||||
if (isset($config['user_agent'])) {
|
||||
$curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
|
||||
}
|
||||
|
||||
return $curlOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual work of initializing cURL, setting the options,
|
||||
* and grabbing the output.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function sendRequest(array $curlOptions = []): string
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array($ch, $curlOptions);
|
||||
|
||||
// Send the request and wait for a response.
|
||||
$output = curl_exec($ch);
|
||||
|
||||
if ($output === false) {
|
||||
throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,840 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use Config\App;
|
||||
use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig;
|
||||
|
||||
/**
|
||||
* Provides tools for working with the Content-Security-Policy header
|
||||
* to help defeat XSS attacks.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/
|
||||
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
* @see http://content-security-policy.com/
|
||||
* @see https://www.owasp.org/index.php/Content_Security_Policy
|
||||
* @see \CodeIgniter\HTTP\ContentSecurityPolicyTest
|
||||
*/
|
||||
class ContentSecurityPolicy
|
||||
{
|
||||
/**
|
||||
* CSP directives
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $directives = [
|
||||
'base-uri' => 'baseURI',
|
||||
'child-src' => 'childSrc',
|
||||
'connect-src' => 'connectSrc',
|
||||
'default-src' => 'defaultSrc',
|
||||
'font-src' => 'fontSrc',
|
||||
'form-action' => 'formAction',
|
||||
'frame-ancestors' => 'frameAncestors',
|
||||
'frame-src' => 'frameSrc',
|
||||
'img-src' => 'imageSrc',
|
||||
'media-src' => 'mediaSrc',
|
||||
'object-src' => 'objectSrc',
|
||||
'plugin-types' => 'pluginTypes',
|
||||
'script-src' => 'scriptSrc',
|
||||
'style-src' => 'styleSrc',
|
||||
'manifest-src' => 'manifestSrc',
|
||||
'sandbox' => 'sandbox',
|
||||
'report-uri' => 'reportURI',
|
||||
];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $baseURI = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $childSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $connectSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $defaultSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $fontSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $formAction = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $frameAncestors = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $frameSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $imageSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $mediaSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $objectSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $pluginTypes = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $scriptSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $styleSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $manifestSrc = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
protected $sandbox = [];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $reportURI;
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $upgradeInsecureRequests = false;
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $reportOnly = false;
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $validSources = [
|
||||
'self',
|
||||
'none',
|
||||
'unsafe-inline',
|
||||
'unsafe-eval',
|
||||
];
|
||||
|
||||
/**
|
||||
* Used for security enforcement
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $nonces = [];
|
||||
|
||||
/**
|
||||
* Nonce for style
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $styleNonce;
|
||||
|
||||
/**
|
||||
* Nonce for script
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $scriptNonce;
|
||||
|
||||
/**
|
||||
* Nonce tag for style
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $styleNonceTag = '{csp-style-nonce}';
|
||||
|
||||
/**
|
||||
* Nonce tag for script
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $scriptNonceTag = '{csp-script-nonce}';
|
||||
|
||||
/**
|
||||
* Replace nonce tag automatically
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $autoNonce = true;
|
||||
|
||||
/**
|
||||
* An array of header info since we have
|
||||
* to build ourself before passing to Response.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tempHeaders = [];
|
||||
|
||||
/**
|
||||
* An array of header info to build
|
||||
* that should only be reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $reportOnlyHeaders = [];
|
||||
|
||||
/**
|
||||
* Whether Content Security Policy is being enforced.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $CSPEnabled = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Stores our default values from the Config file.
|
||||
*/
|
||||
public function __construct(ContentSecurityPolicyConfig $config)
|
||||
{
|
||||
$appConfig = config(App::class);
|
||||
$this->CSPEnabled = $appConfig->CSPEnabled;
|
||||
|
||||
foreach (get_object_vars($config) as $setting => $value) {
|
||||
if (property_exists($this, $setting)) {
|
||||
$this->{$setting} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($this->styleSrc)) {
|
||||
$this->styleSrc = [$this->styleSrc];
|
||||
}
|
||||
|
||||
if (! is_array($this->scriptSrc)) {
|
||||
$this->scriptSrc = [$this->scriptSrc];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Content Security Policy is being enforced.
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return $this->CSPEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nonce for the style tag.
|
||||
*/
|
||||
public function getStyleNonce(): string
|
||||
{
|
||||
if ($this->styleNonce === null) {
|
||||
$this->styleNonce = bin2hex(random_bytes(12));
|
||||
$this->styleSrc[] = 'nonce-' . $this->styleNonce;
|
||||
}
|
||||
|
||||
return $this->styleNonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nonce for the script tag.
|
||||
*/
|
||||
public function getScriptNonce(): string
|
||||
{
|
||||
if ($this->scriptNonce === null) {
|
||||
$this->scriptNonce = bin2hex(random_bytes(12));
|
||||
$this->scriptSrc[] = 'nonce-' . $this->scriptNonce;
|
||||
}
|
||||
|
||||
return $this->scriptNonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles and sets the appropriate headers in the request.
|
||||
*
|
||||
* Should be called just prior to sending the response to the user agent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finalize(ResponseInterface $response)
|
||||
{
|
||||
if ($this->autoNonce) {
|
||||
$this->generateNonces($response);
|
||||
}
|
||||
|
||||
$this->buildHeaders($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* If TRUE, nothing will be restricted. Instead all violations will
|
||||
* be reported to the reportURI for monitoring. This is useful when
|
||||
* you are just starting to implement the policy, and will help
|
||||
* determine what errors need to be addressed before you turn on
|
||||
* all filtering.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function reportOnly(bool $value = true)
|
||||
{
|
||||
$this->reportOnly = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new base_uri value. Can be either a URI class or a simple string.
|
||||
*
|
||||
* base_uri restricts the URLs that can appear in a page's <base> element.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-base-uri
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addBaseURI($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for a form's action. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* child-src lists the URLs for workers and embedded frame contents.
|
||||
* For example: child-src https://youtube.com would enable embedding
|
||||
* videos from YouTube but not from other origins.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-child-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addChildSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for a form's action. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* connect-src limits the origins to which you can connect
|
||||
* (via XHR, WebSockets, and EventSource).
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-connect-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addConnectSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for a form's action. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* default_src is the URI that is used for many of the settings when
|
||||
* no other source has been set.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-default-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefaultSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for a form's action. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* font-src specifies the origins that can serve web fonts.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-font-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFontSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for a form's action. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-form-action
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFormAction($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new resource that should allow embedding the resource using
|
||||
* <frame>, <iframe>, <object>, <embed>, or <applet>
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-frame-ancestors
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFrameAncestor($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for valid frame sources. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-frame-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFrameSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'frameSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for valid image sources. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-img-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addImageSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for valid video and audio. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-media-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addMediaSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for manifest sources. Can be either
|
||||
* a URI class or simple string.
|
||||
*
|
||||
* @see https://www.w3.org/TR/CSP/#directive-manifest-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addManifestSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for Flash and other plugin sources. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-object-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addObjectSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the types of plugins that can be used. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-plugin-types
|
||||
*
|
||||
* @param array|string $mime One or more plugin mime types, separate by spaces
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addPluginType($mime, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a URL where a browser will send reports when a content
|
||||
* security policy is violated. Can be either a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-report-uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setReportURI(string $uri)
|
||||
{
|
||||
$this->reportURI = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* specifies an HTML sandbox policy that the user agent applies to
|
||||
* the protected resource.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-sandbox
|
||||
*
|
||||
* @param array|string $flags An array of sandbox flags that can be added to the directive.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addSandbox($flags, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for javascript file sources. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-connect-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addScriptSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid endpoint for CSS file sources. Can be either
|
||||
* a URI class or a simple string.
|
||||
*
|
||||
* @see http://www.w3.org/TR/CSP/#directive-connect-src
|
||||
*
|
||||
* @param array|string $uri
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addStyleSrc($uri, ?bool $explicitReporting = null)
|
||||
{
|
||||
$this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the user agents should rewrite URL schemes, changing
|
||||
* HTTP to HTTPS.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function upgradeInsecureRequests(bool $value = true)
|
||||
{
|
||||
$this->upgradeInsecureRequests = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* DRY method to add an string or array to a class property.
|
||||
*
|
||||
* @param list<string>|string $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addOption($options, string $target, ?bool $explicitReporting = null)
|
||||
{
|
||||
// Ensure we have an array to work with...
|
||||
if (is_string($this->{$target})) {
|
||||
$this->{$target} = [$this->{$target}];
|
||||
}
|
||||
|
||||
if (is_array($options)) {
|
||||
foreach ($options as $opt) {
|
||||
$this->{$target}[$opt] = $explicitReporting ?? $this->reportOnly;
|
||||
}
|
||||
} else {
|
||||
$this->{$target}[$options] = $explicitReporting ?? $this->reportOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the body of the request message and replaces any nonce
|
||||
* placeholders with actual nonces, that we'll then add to our
|
||||
* headers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function generateNonces(ResponseInterface $response)
|
||||
{
|
||||
$body = $response->getBody();
|
||||
|
||||
if (empty($body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace style and script placeholders with nonces
|
||||
$pattern = '/(' . preg_quote($this->styleNonceTag, '/')
|
||||
. '|' . preg_quote($this->scriptNonceTag, '/') . ')/';
|
||||
|
||||
$body = preg_replace_callback($pattern, function ($match) {
|
||||
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
|
||||
|
||||
return "nonce=\"{$nonce}\"";
|
||||
}, $body);
|
||||
|
||||
$response->setBody($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the current state of the elements, will add the appropriate
|
||||
* Content-Security-Policy and Content-Security-Policy-Report-Only headers
|
||||
* with their values to the response object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function buildHeaders(ResponseInterface $response)
|
||||
{
|
||||
// Ensure both headers are available and arrays...
|
||||
$response->setHeader('Content-Security-Policy', []);
|
||||
$response->setHeader('Content-Security-Policy-Report-Only', []);
|
||||
|
||||
// inject default base & default URIs if needed
|
||||
if (empty($this->baseURI)) {
|
||||
$this->baseURI = 'self';
|
||||
}
|
||||
|
||||
if (empty($this->defaultSrc)) {
|
||||
$this->defaultSrc = 'self';
|
||||
}
|
||||
|
||||
foreach ($this->directives as $name => $property) {
|
||||
if (! empty($this->{$property})) {
|
||||
$this->addToHeader($name, $this->{$property});
|
||||
}
|
||||
}
|
||||
|
||||
// Compile our own header strings here since if we just
|
||||
// append it to the response, it will be joined with
|
||||
// commas, not semi-colons as we need.
|
||||
if (! empty($this->tempHeaders)) {
|
||||
$header = '';
|
||||
|
||||
foreach ($this->tempHeaders as $name => $value) {
|
||||
$header .= " {$name} {$value};";
|
||||
}
|
||||
|
||||
// add token only if needed
|
||||
if ($this->upgradeInsecureRequests) {
|
||||
$header .= ' upgrade-insecure-requests;';
|
||||
}
|
||||
|
||||
$response->appendHeader('Content-Security-Policy', $header);
|
||||
}
|
||||
|
||||
if (! empty($this->reportOnlyHeaders)) {
|
||||
$header = '';
|
||||
|
||||
foreach ($this->reportOnlyHeaders as $name => $value) {
|
||||
$header .= " {$name} {$value};";
|
||||
}
|
||||
|
||||
$response->appendHeader('Content-Security-Policy-Report-Only', $header);
|
||||
}
|
||||
|
||||
$this->tempHeaders = [];
|
||||
$this->reportOnlyHeaders = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a directive and it's options to the appropriate header. The $values
|
||||
* array might have options that are geared toward either the regular or the
|
||||
* reportOnly header, since it's viable to have both simultaneously.
|
||||
*
|
||||
* @param array|string|null $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addToHeader(string $name, $values = null)
|
||||
{
|
||||
if (is_string($values)) {
|
||||
$values = [$values => $this->reportOnly];
|
||||
}
|
||||
|
||||
$sources = [];
|
||||
$reportSources = [];
|
||||
|
||||
foreach ($values as $value => $reportOnly) {
|
||||
if (is_numeric($value) && is_string($reportOnly) && ($reportOnly !== '')) {
|
||||
$value = $reportOnly;
|
||||
$reportOnly = $this->reportOnly;
|
||||
}
|
||||
|
||||
if (str_starts_with($value, 'nonce-')) {
|
||||
$value = "'{$value}'";
|
||||
}
|
||||
|
||||
if ($reportOnly === true) {
|
||||
$reportSources[] = in_array($value, $this->validSources, true) ? "'{$value}'" : $value;
|
||||
} else {
|
||||
$sources[] = in_array($value, $this->validSources, true) ? "'{$value}'" : $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sources !== []) {
|
||||
$this->tempHeaders[$name] = implode(' ', $sources);
|
||||
}
|
||||
|
||||
if ($reportSources !== []) {
|
||||
$this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the directive.
|
||||
*
|
||||
* @param string $directive CSP directive
|
||||
*/
|
||||
public function clearDirective(string $directive): void
|
||||
{
|
||||
if ($directive === 'report-uris') {
|
||||
$this->{$this->directives[$directive]} = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->{$this->directives[$directive]} = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use Config\Cors as CorsConfig;
|
||||
|
||||
/**
|
||||
* Cross-Origin Resource Sharing (CORS)
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\CorsTest
|
||||
*/
|
||||
class Cors
|
||||
{
|
||||
/**
|
||||
* @var array{
|
||||
* allowedOrigins: list<string>,
|
||||
* allowedOriginsPatterns: list<string>,
|
||||
* supportsCredentials: bool,
|
||||
* allowedHeaders: list<string>,
|
||||
* exposedHeaders: list<string>,
|
||||
* allowedMethods: list<string>,
|
||||
* maxAge: int,
|
||||
* }
|
||||
*/
|
||||
private array $config = [
|
||||
'allowedOrigins' => [],
|
||||
'allowedOriginsPatterns' => [],
|
||||
'supportsCredentials' => false,
|
||||
'allowedHeaders' => [],
|
||||
'exposedHeaders' => [],
|
||||
'allowedMethods' => [],
|
||||
'maxAge' => 7200,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* allowedOrigins?: list<string>,
|
||||
* allowedOriginsPatterns?: list<string>,
|
||||
* supportsCredentials?: bool,
|
||||
* allowedHeaders?: list<string>,
|
||||
* exposedHeaders?: list<string>,
|
||||
* allowedMethods?: list<string>,
|
||||
* maxAge?: int,
|
||||
* }|CorsConfig|null $config
|
||||
*/
|
||||
public function __construct($config = null)
|
||||
{
|
||||
$config ??= config(CorsConfig::class);
|
||||
if ($config instanceof CorsConfig) {
|
||||
$config = $config->default;
|
||||
}
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance by config name.
|
||||
*/
|
||||
public static function factory(string $configName = 'default'): self
|
||||
{
|
||||
$config = config(CorsConfig::class)->{$configName};
|
||||
|
||||
return new self($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether if the request is a preflight request.
|
||||
*/
|
||||
public function isPreflightRequest(IncomingRequest $request): bool
|
||||
{
|
||||
return $request->is('OPTIONS')
|
||||
&& $request->hasHeader('Access-Control-Request-Method');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the preflight request, and returns the response.
|
||||
*/
|
||||
public function handlePreflightRequest(RequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$response->setStatusCode(204);
|
||||
|
||||
$this->setAllowOrigin($request, $response);
|
||||
|
||||
if ($response->hasHeader('Access-Control-Allow-Origin')) {
|
||||
$this->setAllowHeaders($response);
|
||||
$this->setAllowMethods($response);
|
||||
$this->setAllowMaxAge($response);
|
||||
$this->setAllowCredentials($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function checkWildcard(string $name, int $count): void
|
||||
{
|
||||
if (in_array('*', $this->config[$name], true) && $count > 1) {
|
||||
throw new ConfigException(
|
||||
"If wildcard is specified, you must set `'{$name}' => ['*']`."
|
||||
. ' But using wildcard is not recommended.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkWildcardAndCredentials(string $name, string $header): void
|
||||
{
|
||||
if (
|
||||
$this->config[$name] === ['*']
|
||||
&& $this->config['supportsCredentials']
|
||||
) {
|
||||
throw new ConfigException(
|
||||
'When responding to a credentialed request, '
|
||||
. 'the server must not specify the "*" wildcard for the '
|
||||
. $header . ' response-header value.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function setAllowOrigin(RequestInterface $request, ResponseInterface $response): void
|
||||
{
|
||||
$originCount = count($this->config['allowedOrigins']);
|
||||
$originPatternCount = count($this->config['allowedOriginsPatterns']);
|
||||
|
||||
$this->checkWildcard('allowedOrigins', $originCount);
|
||||
$this->checkWildcardAndCredentials('allowedOrigins', 'Access-Control-Allow-Origin');
|
||||
|
||||
// Single Origin.
|
||||
if ($originCount === 1 && $originPatternCount === 0) {
|
||||
$response->setHeader('Access-Control-Allow-Origin', $this->config['allowedOrigins'][0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple Origins.
|
||||
if (! $request->hasHeader('Origin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if ($originCount > 1 && in_array($origin, $this->config['allowedOrigins'], true)) {
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->appendHeader('Vary', 'Origin');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($originPatternCount > 0) {
|
||||
foreach ($this->config['allowedOriginsPatterns'] as $pattern) {
|
||||
$regex = '#\A' . $pattern . '\z#';
|
||||
|
||||
if (preg_match($regex, $origin)) {
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->appendHeader('Vary', 'Origin');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setAllowHeaders(ResponseInterface $response): void
|
||||
{
|
||||
$this->checkWildcard('allowedHeaders', count($this->config['allowedHeaders']));
|
||||
$this->checkWildcardAndCredentials('allowedHeaders', 'Access-Control-Allow-Headers');
|
||||
|
||||
$response->setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
implode(', ', $this->config['allowedHeaders'])
|
||||
);
|
||||
}
|
||||
|
||||
private function setAllowMethods(ResponseInterface $response): void
|
||||
{
|
||||
$this->checkWildcard('allowedMethods', count($this->config['allowedMethods']));
|
||||
$this->checkWildcardAndCredentials('allowedMethods', 'Access-Control-Allow-Methods');
|
||||
|
||||
$response->setHeader(
|
||||
'Access-Control-Allow-Methods',
|
||||
implode(', ', $this->config['allowedMethods'])
|
||||
);
|
||||
}
|
||||
|
||||
private function setAllowMaxAge(ResponseInterface $response): void
|
||||
{
|
||||
$response->setHeader('Access-Control-Max-Age', (string) $this->config['maxAge']);
|
||||
}
|
||||
|
||||
private function setAllowCredentials(ResponseInterface $response): void
|
||||
{
|
||||
if ($this->config['supportsCredentials']) {
|
||||
$response->setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds CORS headers to the Response.
|
||||
*/
|
||||
public function addResponseHeaders(RequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$this->setAllowOrigin($request, $response);
|
||||
|
||||
if ($response->hasHeader('Access-Control-Allow-Origin')) {
|
||||
$this->setAllowCredentials($response);
|
||||
$this->setExposeHeaders($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function setExposeHeaders(ResponseInterface $response): void
|
||||
{
|
||||
if ($this->config['exposedHeaders'] !== []) {
|
||||
$response->setHeader(
|
||||
'Access-Control-Expose-Headers',
|
||||
implode(', ', $this->config['exposedHeaders'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Exceptions\DownloadException;
|
||||
use CodeIgniter\Files\File;
|
||||
use Config\App;
|
||||
use Config\Mimes;
|
||||
|
||||
/**
|
||||
* HTTP response when a download is requested.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\DownloadResponseTest
|
||||
*/
|
||||
class DownloadResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Download file name
|
||||
*/
|
||||
private string $filename;
|
||||
|
||||
/**
|
||||
* Download for file
|
||||
*/
|
||||
private ?File $file = null;
|
||||
|
||||
/**
|
||||
* mime set flag
|
||||
*/
|
||||
private readonly bool $setMime;
|
||||
|
||||
/**
|
||||
* Download for binary
|
||||
*/
|
||||
private ?string $binary = null;
|
||||
|
||||
/**
|
||||
* Download charset
|
||||
*/
|
||||
private string $charset = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Download reason
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $reason = 'OK';
|
||||
|
||||
/**
|
||||
* The current status code for this response.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $statusCode = 200;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(string $filename, bool $setMime)
|
||||
{
|
||||
parent::__construct(config(App::class));
|
||||
|
||||
$this->filename = $filename;
|
||||
$this->setMime = $setMime;
|
||||
|
||||
// Make sure the content type is either specified or detected
|
||||
$this->removeHeader('Content-Type');
|
||||
}
|
||||
|
||||
/**
|
||||
* set download for binary string.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBinary(string $binary)
|
||||
{
|
||||
if ($this->file !== null) {
|
||||
throw DownloadException::forCannotSetBinary();
|
||||
}
|
||||
|
||||
$this->binary = $binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* set download for file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFilePath(string $filepath)
|
||||
{
|
||||
if ($this->binary !== null) {
|
||||
throw DownloadException::forCannotSetFilePath($filepath);
|
||||
}
|
||||
|
||||
$this->file = new File($filepath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* set name for the download.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFileName(string $filename)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* get content length.
|
||||
*/
|
||||
public function getContentLength(): int
|
||||
{
|
||||
if (is_string($this->binary)) {
|
||||
return strlen($this->binary);
|
||||
}
|
||||
|
||||
if ($this->file instanceof File) {
|
||||
return $this->file->getSize();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content type by guessing mime type from file extension
|
||||
*/
|
||||
private function setContentTypeByMimeType(): void
|
||||
{
|
||||
$mime = null;
|
||||
$charset = '';
|
||||
|
||||
if ($this->setMime === true && ($lastDotPosition = strrpos($this->filename, '.')) !== false) {
|
||||
$mime = Mimes::guessTypeFromExtension(substr($this->filename, $lastDotPosition + 1));
|
||||
$charset = $this->charset;
|
||||
}
|
||||
|
||||
if (! is_string($mime)) {
|
||||
// Set the default MIME type to send
|
||||
$mime = 'application/octet-stream';
|
||||
$charset = '';
|
||||
}
|
||||
|
||||
$this->setContentType($mime, $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* get download filename.
|
||||
*/
|
||||
private function getDownloadFileName(): string
|
||||
{
|
||||
$filename = $this->filename;
|
||||
$x = explode('.', $this->filename);
|
||||
$extension = end($x);
|
||||
|
||||
/* It was reported that browsers on Android 2.1 (and possibly older as well)
|
||||
* need to have the filename extension upper-cased in order to be able to
|
||||
* download it.
|
||||
*
|
||||
* Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
|
||||
*/
|
||||
// @todo: depend super global
|
||||
if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT'])
|
||||
&& preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) {
|
||||
$x[count($x) - 1] = strtoupper($extension);
|
||||
$filename = implode('.', $x);
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* get Content-Disposition Header string.
|
||||
*/
|
||||
private function getContentDisposition(): string
|
||||
{
|
||||
$downloadFilename = $this->getDownloadFileName();
|
||||
|
||||
$utf8Filename = $downloadFilename;
|
||||
|
||||
if (strtoupper($this->charset) !== 'UTF-8') {
|
||||
$utf8Filename = mb_convert_encoding($downloadFilename, 'UTF-8', $this->charset);
|
||||
}
|
||||
|
||||
$result = sprintf('attachment; filename="%s"', $downloadFilename);
|
||||
|
||||
if ($utf8Filename !== '') {
|
||||
$result .= '; filename*=UTF-8\'\'' . rawurlencode($utf8Filename);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallows status changing.
|
||||
*
|
||||
* @throws DownloadException
|
||||
*/
|
||||
public function setStatusCode(int $code, string $reason = '')
|
||||
{
|
||||
throw DownloadException::forCannotSetStatusCode($code, $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content Type header for this response with the mime type
|
||||
* and, optionally, the charset.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function setContentType(string $mime, string $charset = 'UTF-8')
|
||||
{
|
||||
parent::setContentType($mime, $charset);
|
||||
|
||||
if ($charset !== '') {
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the appropriate headers to ensure this response
|
||||
* is not cached by the browsers.
|
||||
*/
|
||||
public function noCache(): self
|
||||
{
|
||||
$this->removeHeader('Cache-Control');
|
||||
$this->setHeader('Cache-Control', ['private', 'no-transform', 'no-store', 'must-revalidate']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables cache configuration.
|
||||
*
|
||||
* @throws DownloadException
|
||||
*/
|
||||
public function setCache(array $options = [])
|
||||
{
|
||||
throw DownloadException::forCannotSetCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @todo Do downloads need CSP or Cookies? Compare with ResponseTrait::send()
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
// Turn off output buffering completely, even if php.ini output_buffering is not off
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildHeaders();
|
||||
$this->sendHeaders();
|
||||
$this->sendBody();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set header for file download.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function buildHeaders()
|
||||
{
|
||||
if (! $this->hasHeader('Content-Type')) {
|
||||
$this->setContentTypeByMimeType();
|
||||
}
|
||||
|
||||
if (! $this->hasHeader('Content-Disposition')) {
|
||||
$this->setHeader('Content-Disposition', $this->getContentDisposition());
|
||||
}
|
||||
|
||||
$this->setHeader('Expires-Disposition', '0');
|
||||
$this->setHeader('Content-Transfer-Encoding', 'binary');
|
||||
$this->setHeader('Content-Length', (string) $this->getContentLength());
|
||||
$this->noCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* output download file text.
|
||||
*
|
||||
* @return DownloadResponse
|
||||
*
|
||||
* @throws DownloadException
|
||||
*/
|
||||
public function sendBody()
|
||||
{
|
||||
if ($this->binary !== null) {
|
||||
return $this->sendBodyByBinary();
|
||||
}
|
||||
|
||||
if ($this->file !== null) {
|
||||
return $this->sendBodyByFilePath();
|
||||
}
|
||||
|
||||
throw DownloadException::forNotFoundDownloadSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* output download text by file.
|
||||
*
|
||||
* @return DownloadResponse
|
||||
*/
|
||||
private function sendBodyByFilePath()
|
||||
{
|
||||
$splFileObject = $this->file->openFile('rb');
|
||||
|
||||
// Flush 1MB chunks of data
|
||||
while (! $splFileObject->eof() && ($data = $splFileObject->fread(1_048_576)) !== false) {
|
||||
echo $data;
|
||||
unset($data);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* output download text by binary
|
||||
*
|
||||
* @return DownloadResponse
|
||||
*/
|
||||
private function sendBodyByBinary()
|
||||
{
|
||||
echo $this->binary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response header to display the file in the browser.
|
||||
*
|
||||
* @return DownloadResponse
|
||||
*/
|
||||
public function inline()
|
||||
{
|
||||
$this->setHeader('Content-Disposition', 'inline');
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\HTTPExceptionInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 400 Bad Request
|
||||
*/
|
||||
class BadRequestException extends RuntimeException implements HTTPExceptionInterface
|
||||
{
|
||||
/**
|
||||
* HTTP status code for Bad Request
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 400; // @phpstan-ignore-line
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\FrameworkException;
|
||||
|
||||
/**
|
||||
* Things that can go wrong with HTTP
|
||||
*/
|
||||
class HTTPException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* For CurlRequest
|
||||
*
|
||||
* @return HTTPException
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function forMissingCurl()
|
||||
{
|
||||
return new static(lang('HTTP.missingCurl'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For CurlRequest
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forSSLCertNotFound(string $cert)
|
||||
{
|
||||
return new static(lang('HTTP.sslCertNotFound', [$cert]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For CurlRequest
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidSSLKey(string $key)
|
||||
{
|
||||
return new static(lang('HTTP.invalidSSLKey', [$key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For CurlRequest
|
||||
*
|
||||
* @return HTTPException
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function forCurlError(string $errorNum, string $error)
|
||||
{
|
||||
return new static(lang('HTTP.curlError', [$errorNum, $error]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For IncomingRequest
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidNegotiationType(string $type)
|
||||
{
|
||||
return new static(lang('HTTP.invalidNegotiationType', [$type]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown in IncomingRequest when the json_decode() produces
|
||||
* an error code other than JSON_ERROR_NONE.
|
||||
*
|
||||
* @param string $error The error message
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidJSON(?string $error = null)
|
||||
{
|
||||
return new static(lang('HTTP.invalidJSON', [$error]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Message
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidHTTPProtocol(string $invalidVersion)
|
||||
{
|
||||
return new static(lang('HTTP.invalidHTTPProtocol', [$invalidVersion]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Negotiate
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forEmptySupportedNegotiations()
|
||||
{
|
||||
return new static(lang('HTTP.emptySupportedNegotiations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For RedirectResponse
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidRedirectRoute(string $route)
|
||||
{
|
||||
return new static(lang('HTTP.invalidRoute', [$route]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Response
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forMissingResponseStatus()
|
||||
{
|
||||
return new static(lang('HTTP.missingResponseStatus'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Response
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidStatusCode(int $code)
|
||||
{
|
||||
return new static(lang('HTTP.invalidStatusCode', [$code]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Response
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forUnkownStatusCode(int $code)
|
||||
{
|
||||
return new static(lang('HTTP.unknownStatusCode', [$code]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For URI
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forUnableToParseURI(string $uri)
|
||||
{
|
||||
return new static(lang('HTTP.cannotParseURI', [$uri]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For URI
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forURISegmentOutOfRange(int $segment)
|
||||
{
|
||||
return new static(lang('HTTP.segmentOutOfRange', [$segment]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For URI
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidPort(int $port)
|
||||
{
|
||||
return new static(lang('HTTP.invalidPort', [$port]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For URI
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forMalformedQueryString()
|
||||
{
|
||||
return new static(lang('HTTP.malformedQueryString'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Uploaded file move
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forAlreadyMoved()
|
||||
{
|
||||
return new static(lang('HTTP.alreadyMoved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Uploaded file move
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forInvalidFile(?string $path = null)
|
||||
{
|
||||
return new static(lang('HTTP.invalidFile'));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Uploaded file move
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forMoveFailed(string $source, string $target, string $error)
|
||||
{
|
||||
return new static(lang('HTTP.moveFailed', [$source, $target, $error]));
|
||||
}
|
||||
|
||||
/**
|
||||
* For Invalid SameSite attribute setting
|
||||
*
|
||||
* @return HTTPException
|
||||
*
|
||||
* @deprecated Use `CookieException::forInvalidSameSite()` instead.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function forInvalidSameSiteSetting(string $samesite)
|
||||
{
|
||||
return new static(lang('Security.invalidSameSiteSetting', [$samesite]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the JSON format is not supported.
|
||||
* This is specifically for cases where data validation is expected to work with key-value structures.
|
||||
*
|
||||
* @return HTTPException
|
||||
*/
|
||||
public static function forUnsupportedJSONFormat()
|
||||
{
|
||||
return new static(lang('HTTP.unsupportedJSONFormat'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\HTTPExceptionInterface;
|
||||
use CodeIgniter\HTTP\ResponsableInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* RedirectException
|
||||
*/
|
||||
class RedirectException extends Exception implements ResponsableInterface, HTTPExceptionInterface
|
||||
{
|
||||
/**
|
||||
* HTTP status code for redirects
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 302;
|
||||
|
||||
protected ?ResponseInterface $response = null;
|
||||
|
||||
/**
|
||||
* @param ResponseInterface|string $message Response object or a string containing a relative URI.
|
||||
* @param int $code HTTP status code to redirect if $message is a string.
|
||||
*/
|
||||
public function __construct($message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
if (! is_string($message) && ! $message instanceof ResponseInterface) {
|
||||
throw new InvalidArgumentException(
|
||||
'RedirectException::__construct() first argument must be a string or ResponseInterface',
|
||||
0,
|
||||
$this
|
||||
);
|
||||
}
|
||||
|
||||
if ($message instanceof ResponseInterface) {
|
||||
$this->response = $message;
|
||||
$message = '';
|
||||
|
||||
if ($this->response->getHeaderLine('Location') === '' && $this->response->getHeaderLine('Refresh') === '') {
|
||||
throw new LogicException(
|
||||
'The Response object passed to RedirectException does not contain a redirect address.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->response->getStatusCode() < 301 || $this->response->getStatusCode() > 308) {
|
||||
$this->response->setStatusCode($this->code);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
if (null === $this->response) {
|
||||
$this->response = service('response')
|
||||
->redirect(base_url($this->getMessage()), 'auto', $this->getCode());
|
||||
}
|
||||
|
||||
service('logger')->info(
|
||||
'REDIRECTED ROUTE at '
|
||||
. ($this->response->getHeaderLine('Location') ?: substr($this->response->getHeaderLine('Refresh'), 6))
|
||||
);
|
||||
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Files;
|
||||
|
||||
use RecursiveArrayIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* Class FileCollection
|
||||
*
|
||||
* Provides easy access to uploaded files for a request.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\Files\FileCollectionTest
|
||||
*/
|
||||
class FileCollection
|
||||
{
|
||||
/**
|
||||
* An array of UploadedFile instances for any files
|
||||
* uploaded as part of this request.
|
||||
* Populated the first time either files(), file(), or hasFile()
|
||||
* is called.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* Returns an array of all uploaded files that were found.
|
||||
* Each element in the array will be an instance of UploadedFile.
|
||||
* The key of each element will be the client filename.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$this->populateFiles();
|
||||
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get a single file from the collection of uploaded files.
|
||||
*
|
||||
* @return UploadedFile|null
|
||||
*/
|
||||
public function getFile(string $name)
|
||||
{
|
||||
$this->populateFiles();
|
||||
|
||||
if ($this->hasFile($name)) {
|
||||
if (str_contains($name, '.')) {
|
||||
$name = explode('.', $name);
|
||||
$uploadedFile = $this->getValueDotNotationSyntax($name, $this->files);
|
||||
|
||||
return $uploadedFile instanceof UploadedFile ? $uploadedFile : null;
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $this->files)) {
|
||||
$uploadedFile = $this->files[$name];
|
||||
|
||||
return $uploadedFile instanceof UploadedFile ? $uploadedFile : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a file exist in the collection of uploaded files and is have been uploaded with multiple option.
|
||||
*
|
||||
* @return list<UploadedFile>|null
|
||||
*/
|
||||
public function getFileMultiple(string $name)
|
||||
{
|
||||
$this->populateFiles();
|
||||
|
||||
if ($this->hasFile($name)) {
|
||||
if (str_contains($name, '.')) {
|
||||
$name = explode('.', $name);
|
||||
$uploadedFile = $this->getValueDotNotationSyntax($name, $this->files);
|
||||
|
||||
return (is_array($uploadedFile) && ($uploadedFile[array_key_first($uploadedFile)] instanceof UploadedFile)) ?
|
||||
$uploadedFile : null;
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $this->files)) {
|
||||
$uploadedFile = $this->files[$name];
|
||||
|
||||
return (is_array($uploadedFile) && ($uploadedFile[array_key_first($uploadedFile)] instanceof UploadedFile)) ?
|
||||
$uploadedFile : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an uploaded file with name $fileID exists in
|
||||
* this request.
|
||||
*
|
||||
* @param string $fileID The name of the uploaded file (from the input)
|
||||
*/
|
||||
public function hasFile(string $fileID): bool
|
||||
{
|
||||
$this->populateFiles();
|
||||
|
||||
if (str_contains($fileID, '.')) {
|
||||
$segments = explode('.', $fileID);
|
||||
|
||||
$el = $this->files;
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if (! array_key_exists($segment, $el)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$el = $el[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($this->files[$fileID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Taking information from the $_FILES array, it creates an instance
|
||||
* of UploadedFile for each one, saving the results to this->files.
|
||||
*
|
||||
* Called by files(), file(), and hasFile()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function populateFiles()
|
||||
{
|
||||
if (is_array($this->files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->files = [];
|
||||
|
||||
if ($_FILES === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = $this->fixFilesArray($_FILES);
|
||||
|
||||
foreach ($files as $name => $file) {
|
||||
$this->files[$name] = $this->createFileObject($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a file array, will create UploadedFile instances. Will
|
||||
* loop over an array and create objects for each.
|
||||
*
|
||||
* @return list<UploadedFile>|UploadedFile
|
||||
*/
|
||||
protected function createFileObject(array $array)
|
||||
{
|
||||
if (! isset($array['name'])) {
|
||||
$output = [];
|
||||
|
||||
foreach ($array as $key => $values) {
|
||||
if (! is_array($values)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[$key] = $this->createFileObject($values);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
return new UploadedFile(
|
||||
$array['tmp_name'] ?? null,
|
||||
$array['name'] ?? null,
|
||||
$array['type'] ?? null,
|
||||
($array['size'] ?? null) === null ? null : (int) $array['size'],
|
||||
$array['error'] ?? null,
|
||||
$array['full_path'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reformats the odd $_FILES array into something much more like
|
||||
* we would expect, with each object having its own array.
|
||||
*
|
||||
* Thanks to Jack Sleight on the PHP Manual page for the basis
|
||||
* of this method.
|
||||
*
|
||||
* @see http://php.net/manual/en/reserved.variables.files.php#118294
|
||||
*/
|
||||
protected function fixFilesArray(array $data): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($data as $name => $array) {
|
||||
foreach ($array as $field => $value) {
|
||||
$pointer = &$output[$name];
|
||||
|
||||
if (! is_array($value)) {
|
||||
$pointer[$field] = $value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$stack = [&$pointer];
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveArrayIterator($value),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $key => $val) {
|
||||
array_splice($stack, $iterator->getDepth() + 1);
|
||||
$pointer = &$stack[count($stack) - 1];
|
||||
$pointer = &$pointer[$key];
|
||||
$stack[] = &$pointer;
|
||||
|
||||
// RecursiveIteratorIterator::hasChildren() can be used. RecursiveIteratorIterator
|
||||
// forwards all unknown method calls to the underlying RecursiveIterator internally.
|
||||
// See https://github.com/php/doc-en/issues/787#issuecomment-881446121
|
||||
if (! $iterator->hasChildren()) {
|
||||
$pointer[$field] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate through an array looking for a particular index
|
||||
*
|
||||
* @param array $index The index sequence we are navigating down
|
||||
* @param array $value The portion of the array to process
|
||||
*
|
||||
* @return list<UploadedFile>|UploadedFile|null
|
||||
*/
|
||||
protected function getValueDotNotationSyntax(array $index, array $value)
|
||||
{
|
||||
$currentIndex = array_shift($index);
|
||||
|
||||
if (isset($currentIndex) && is_array($index) && $index && is_array($value[$currentIndex]) && $value[$currentIndex]) {
|
||||
return $this->getValueDotNotationSyntax($index, $value[$currentIndex]);
|
||||
}
|
||||
|
||||
return $value[$currentIndex] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Files;
|
||||
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use Config\Mimes;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Value object representing a single file uploaded through an
|
||||
* HTTP request. Used by the IncomingRequest class to
|
||||
* provide files.
|
||||
*
|
||||
* Typically, implementors will extend the SplFileInfo class.
|
||||
*/
|
||||
class UploadedFile extends File implements UploadedFileInterface
|
||||
{
|
||||
/**
|
||||
* The path to the temporary file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The webkit relative path of the file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $clientPath;
|
||||
|
||||
/**
|
||||
* The original filename as provided by the client.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $originalName;
|
||||
|
||||
/**
|
||||
* The filename given to a file during a move.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The type of file as provided by PHP
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $originalMimeType;
|
||||
|
||||
/**
|
||||
* The error constant of the upload
|
||||
* (one of PHP's UPLOADERRXXX constants)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $error;
|
||||
|
||||
/**
|
||||
* Whether the file has been moved already or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasMoved = false;
|
||||
|
||||
/**
|
||||
* Accepts the file information as would be filled in from the $_FILES array.
|
||||
*
|
||||
* @param string $path The temporary location of the uploaded file.
|
||||
* @param string $originalName The client-provided filename.
|
||||
* @param string|null $mimeType The type of file as provided by PHP
|
||||
* @param int|null $size The size of the file, in bytes
|
||||
* @param int|null $error The error constant of the upload (one of PHP's UPLOADERRXXX constants)
|
||||
* @param string|null $clientPath The webkit relative path of the uploaded file.
|
||||
*/
|
||||
public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->name = $originalName;
|
||||
$this->originalName = $originalName;
|
||||
$this->originalMimeType = $mimeType;
|
||||
$this->size = $size;
|
||||
$this->error = $error;
|
||||
$this->clientPath = $clientPath;
|
||||
|
||||
parent::__construct($path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the uploaded file to a new location.
|
||||
*
|
||||
* $targetPath may be an absolute path, or a relative path. If it is a
|
||||
* relative path, resolution should be the same as used by PHP's rename()
|
||||
* function.
|
||||
*
|
||||
* The original file MUST be removed on completion.
|
||||
*
|
||||
* If this method is called more than once, any subsequent calls MUST raise
|
||||
* an exception.
|
||||
*
|
||||
* When used in an SAPI environment where $_FILES is populated, when writing
|
||||
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
|
||||
* used to ensure permissions and upload status are verified correctly.
|
||||
*
|
||||
* If you wish to move to a stream, use getStream(), as SAPI operations
|
||||
* cannot guarantee writing to stream destinations.
|
||||
*
|
||||
* @see http://php.net/is_uploaded_file
|
||||
* @see http://php.net/move_uploaded_file
|
||||
*
|
||||
* @param string $targetPath Path to which to move the uploaded file.
|
||||
* @param string|null $name the name to rename the file to.
|
||||
* @param bool $overwrite State for indicating whether to overwrite the previously generated file with the same
|
||||
* name or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function move(string $targetPath, ?string $name = null, bool $overwrite = false)
|
||||
{
|
||||
$targetPath = rtrim($targetPath, '/') . '/';
|
||||
$targetPath = $this->setPath($targetPath); // set the target path
|
||||
|
||||
if ($this->hasMoved) {
|
||||
throw HTTPException::forAlreadyMoved();
|
||||
}
|
||||
|
||||
if (! $this->isValid()) {
|
||||
throw HTTPException::forInvalidFile();
|
||||
}
|
||||
|
||||
$name ??= $this->getName();
|
||||
$destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name);
|
||||
|
||||
try {
|
||||
$this->hasMoved = move_uploaded_file($this->path, $destination);
|
||||
} catch (Exception) {
|
||||
$error = error_get_last();
|
||||
$message = strip_tags($error['message'] ?? '');
|
||||
|
||||
throw HTTPException::forMoveFailed(basename($this->path), $targetPath, $message);
|
||||
}
|
||||
|
||||
if ($this->hasMoved === false) {
|
||||
$message = 'move_uploaded_file() returned false';
|
||||
|
||||
throw HTTPException::forMoveFailed(basename($this->path), $targetPath, $message);
|
||||
}
|
||||
|
||||
@chmod($targetPath, 0777 & ~umask());
|
||||
|
||||
// Success, so store our new information
|
||||
$this->path = $targetPath;
|
||||
$this->name = basename($destination);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* create file target path if
|
||||
* the set path does not exist
|
||||
*
|
||||
* @return string The path set or created.
|
||||
*/
|
||||
protected function setPath(string $path): string
|
||||
{
|
||||
if (! is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
// create the index.html file
|
||||
if (! is_file($path . 'index.html')) {
|
||||
$file = fopen($path . 'index.html', 'x+b');
|
||||
fclose($file);
|
||||
}
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file has been moved or not. If it has,
|
||||
* the move() method will not work and certain properties, like
|
||||
* the tempName, will no longer be available.
|
||||
*/
|
||||
public function hasMoved(): bool
|
||||
{
|
||||
return $this->hasMoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error associated with the uploaded file.
|
||||
*
|
||||
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
|
||||
*
|
||||
* If the file was uploaded successfully, this method MUST return
|
||||
* UPLOAD_ERR_OK.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "error" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
*
|
||||
* @return int One of PHP's UPLOAD_ERR_XXX constants.
|
||||
*/
|
||||
public function getError(): int
|
||||
{
|
||||
return $this->error ?? UPLOAD_ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error string
|
||||
*/
|
||||
public function getErrorString(): string
|
||||
{
|
||||
$errors = [
|
||||
UPLOAD_ERR_OK => lang('HTTP.uploadErrOk'),
|
||||
UPLOAD_ERR_INI_SIZE => lang('HTTP.uploadErrIniSize'),
|
||||
UPLOAD_ERR_FORM_SIZE => lang('HTTP.uploadErrFormSize'),
|
||||
UPLOAD_ERR_PARTIAL => lang('HTTP.uploadErrPartial'),
|
||||
UPLOAD_ERR_NO_FILE => lang('HTTP.uploadErrNoFile'),
|
||||
UPLOAD_ERR_CANT_WRITE => lang('HTTP.uploadErrCantWrite'),
|
||||
UPLOAD_ERR_NO_TMP_DIR => lang('HTTP.uploadErrNoTmpDir'),
|
||||
UPLOAD_ERR_EXTENSION => lang('HTTP.uploadErrExtension'),
|
||||
];
|
||||
|
||||
$error = $this->error ?? UPLOAD_ERR_OK;
|
||||
|
||||
return sprintf($errors[$error] ?? lang('HTTP.uploadErrUnknown'), $this->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type as provided by the client.
|
||||
* This is NOT a trusted value.
|
||||
* For a trusted version, use getMimeType() instead.
|
||||
*
|
||||
* @return string The media type sent by the client or null if none was provided.
|
||||
*/
|
||||
public function getClientMimeType(): string
|
||||
{
|
||||
return $this->originalMimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filename. This will typically be the filename sent
|
||||
* by the client, and should not be trusted. If the file has been
|
||||
* moved, this will return the final name of the moved file.
|
||||
*
|
||||
* @return string The filename sent by the client or null if none was provided.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the file as provided by the client during upload.
|
||||
*/
|
||||
public function getClientName(): string
|
||||
{
|
||||
return $this->originalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 8.1+)
|
||||
* Returns the webkit relative path of the uploaded file on directory uploads.
|
||||
*/
|
||||
public function getClientPath(): ?string
|
||||
{
|
||||
return $this->clientPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the temporary filename where the file was uploaded to.
|
||||
*/
|
||||
public function getTempName(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides SPLFileInfo's to work with uploaded files, since
|
||||
* the temp file that's been uploaded doesn't have an extension.
|
||||
*
|
||||
* This method tries to guess the extension from the files mime
|
||||
* type but will return the clientExtension if it fails to do so.
|
||||
*
|
||||
* This method will always return a more or less helpfull extension
|
||||
* but might be insecure if the mime type is not matched. Consider
|
||||
* using guessExtension for a more safe version.
|
||||
*/
|
||||
public function getExtension(): string
|
||||
{
|
||||
return $this->guessExtension() ?: $this->getClientExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to determine the best file extension from the file's
|
||||
* mime type. In contrast to getExtension, this method will return
|
||||
* an empty string if it fails to determine an extension instead of
|
||||
* falling back to the unsecure clientExtension.
|
||||
*/
|
||||
public function guessExtension(): string
|
||||
{
|
||||
return Mimes::guessExtensionFromType($this->getMimeType(), $this->getClientExtension()) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original file extension, based on the file name that
|
||||
* was uploaded. This is NOT a trusted source.
|
||||
* For a trusted version, use guessExtension() instead.
|
||||
*/
|
||||
public function getClientExtension(): string
|
||||
{
|
||||
return pathinfo($this->originalName, PATHINFO_EXTENSION) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the file was uploaded successfully, based on whether
|
||||
* it was uploaded via HTTP and has no errors.
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return is_uploaded_file($this->path) && $this->error === UPLOAD_ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the uploaded file to a new location.
|
||||
*
|
||||
* By default, upload files are saved in writable/uploads directory. The YYYYMMDD folder
|
||||
* and random file name will be created.
|
||||
*
|
||||
* @param string|null $folderName the folder name to writable/uploads directory.
|
||||
* @param string|null $fileName the name to rename the file to.
|
||||
*
|
||||
* @return string file full path
|
||||
*/
|
||||
public function store(?string $folderName = null, ?string $fileName = null): string
|
||||
{
|
||||
$folderName = rtrim($folderName ?? date('Ymd'), '/') . '/';
|
||||
$fileName ??= $this->getRandomName();
|
||||
|
||||
// Move the uploaded file to a new location.
|
||||
$this->move(WRITEPATH . 'uploads/' . $folderName, $fileName);
|
||||
|
||||
return $folderName . $this->name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP\Files;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Value object representing a single file uploaded through an
|
||||
* HTTP request. Used by the IncomingRequest class to
|
||||
* provide files.
|
||||
*
|
||||
* Typically, implementors will extend the SplFileInfo class.
|
||||
*/
|
||||
interface UploadedFileInterface
|
||||
{
|
||||
/**
|
||||
* Accepts the file information as would be filled in from the $_FILES array.
|
||||
*
|
||||
* @param string $path The temporary location of the uploaded file.
|
||||
* @param string $originalName The client-provided filename.
|
||||
* @param string|null $mimeType The type of file as provided by PHP
|
||||
* @param int|null $size The size of the file, in bytes
|
||||
* @param int|null $error The error constant of the upload (one of PHP's UPLOADERRXXX constants)
|
||||
* @param string|null $clientPath The webkit relative path of the uploaded file.
|
||||
*/
|
||||
public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null);
|
||||
|
||||
/**
|
||||
* Move the uploaded file to a new location.
|
||||
*
|
||||
* $targetPath may be an absolute path, or a relative path. If it is a
|
||||
* relative path, resolution should be the same as used by PHP's rename()
|
||||
* function.
|
||||
*
|
||||
* The original file MUST be removed on completion.
|
||||
*
|
||||
* If this method is called more than once, any subsequent calls MUST raise
|
||||
* an exception.
|
||||
*
|
||||
* When used in an SAPI environment where $_FILES is populated, when writing
|
||||
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
|
||||
* used to ensure permissions and upload status are verified correctly.
|
||||
*
|
||||
* If you wish to move to a stream, use getStream(), as SAPI operations
|
||||
* cannot guarantee writing to stream destinations.
|
||||
*
|
||||
* @see http://php.net/is_uploaded_file
|
||||
* @see http://php.net/move_uploaded_file
|
||||
*
|
||||
* @param string $targetPath Path to which to move the uploaded file.
|
||||
* @param string|null $name the name to rename the file to.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws InvalidArgumentException if the $path specified is invalid.
|
||||
* @throws RuntimeException on the second or subsequent call to the method.
|
||||
*/
|
||||
public function move(string $targetPath, ?string $name = null);
|
||||
|
||||
/**
|
||||
* Returns whether the file has been moved or not. If it has,
|
||||
* the move() method will not work and certain properties, like
|
||||
* the tempName, will no longer be available.
|
||||
*/
|
||||
public function hasMoved(): bool;
|
||||
|
||||
/**
|
||||
* Retrieve the error associated with the uploaded file.
|
||||
*
|
||||
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
|
||||
*
|
||||
* If the file was uploaded successfully, this method MUST return
|
||||
* UPLOAD_ERR_OK.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "error" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @see http://php.net/manual/en/features.file-upload.errors.php
|
||||
*
|
||||
* @return int One of PHP's UPLOAD_ERR_XXX constants.
|
||||
*/
|
||||
public function getError(): int;
|
||||
|
||||
/**
|
||||
* Retrieve the filename sent by the client.
|
||||
*
|
||||
* Do not trust the value returned by this method. A client could send
|
||||
* a malicious filename with the intention to corrupt or hack your
|
||||
* application.
|
||||
*
|
||||
* Implementations SHOULD return the value stored in the "name" key of
|
||||
* the file in the $_FILES array.
|
||||
*
|
||||
* @return string The filename sent by the client or null if none
|
||||
* was provided.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Gets the temporary filename where the file was uploaded to.
|
||||
*/
|
||||
public function getTempName(): string;
|
||||
|
||||
/**
|
||||
* (PHP 8.1+)
|
||||
* Returns the webkit relative path of the uploaded file on directory uploads.
|
||||
*/
|
||||
public function getClientPath(): ?string;
|
||||
|
||||
/**
|
||||
* Returns the original file extension, based on the file name that
|
||||
* was uploaded. This is NOT a trusted source.
|
||||
* For a trusted version, use guessExtension() instead.
|
||||
*/
|
||||
public function getClientExtension(): string;
|
||||
|
||||
/**
|
||||
* Returns the mime type as provided by the client.
|
||||
* This is NOT a trusted value.
|
||||
* For a trusted version, use getMimeType() instead.
|
||||
*/
|
||||
public function getClientMimeType(): string;
|
||||
|
||||
/**
|
||||
* Returns whether the file was uploaded successfully, based on whether
|
||||
* it was uploaded via HTTP and has no errors.
|
||||
*/
|
||||
public function isValid(): bool;
|
||||
|
||||
/**
|
||||
* Returns the destination path for the move operation where overwriting is not expected.
|
||||
*
|
||||
* First, it checks whether the delimiter is present in the filename, if it is, then it checks whether the
|
||||
* last element is an integer as there may be cases that the delimiter may be present in the filename.
|
||||
* For the all other cases, it appends an integer starting from zero before the file's extension.
|
||||
*/
|
||||
public function getDestination(string $destination, string $delimiter = '_', int $i = 0): string;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Class Header
|
||||
*
|
||||
* Represents a single HTTP header.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\HeaderTest
|
||||
*/
|
||||
class Header implements Stringable
|
||||
{
|
||||
/**
|
||||
* The name of the header.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The value of the header. May have more than one
|
||||
* value. If so, will be an array of strings.
|
||||
* E.g.,
|
||||
* [
|
||||
* 'foo',
|
||||
* [
|
||||
* 'bar' => 'fizz',
|
||||
* ],
|
||||
* 'baz' => 'buzz',
|
||||
* ]
|
||||
*
|
||||
* @var array<int|string, array<string, string>|string>|string
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Header constructor. name is mandatory, if a value is provided, it will be set.
|
||||
*
|
||||
* @param array<int|string, array<string, string>|string>|string|null $value
|
||||
*/
|
||||
public function __construct(string $name, $value = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->setValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the header, in the same case it was set.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw value of the header. This may return either a string
|
||||
* or an array, depending on whether the header has multiple values or not.
|
||||
*
|
||||
* @return array<int|string, array<string, string>|string>|string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the header, overwriting any previous value.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the header, overwriting any previous value(s).
|
||||
*
|
||||
* @param array<int|string, array<string, string>|string>|string|null $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setValue($value = null)
|
||||
{
|
||||
$this->value = is_array($value) ? $value : (string) $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a value to the list of values for this header. If the
|
||||
* header is a single value string, it will be converted to an array.
|
||||
*
|
||||
* @param array<string, string>|string|null $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendValue($value = null)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (! is_array($this->value)) {
|
||||
$this->value = [$this->value];
|
||||
}
|
||||
|
||||
if (! in_array($value, $this->value, true)) {
|
||||
$this->value[] = is_array($value) ? $value : (string) $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends a value to the list of values for this header. If the
|
||||
* header is a single value string, it will be converted to an array.
|
||||
*
|
||||
* @param array<string, string>|string|null $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prependValue($value = null)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (! is_array($this->value)) {
|
||||
$this->value = [$this->value];
|
||||
}
|
||||
|
||||
array_unshift($this->value, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
||||
*/
|
||||
public function getValueLine(): string
|
||||
{
|
||||
if (is_string($this->value)) {
|
||||
return $this->value;
|
||||
}
|
||||
if (! is_array($this->value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$options = [];
|
||||
|
||||
foreach ($this->value as $key => $value) {
|
||||
if (is_string($key) && ! is_array($value)) {
|
||||
$options[] = $key . '=' . $value;
|
||||
} elseif (is_array($value)) {
|
||||
$key = key($value);
|
||||
$options[] = $key . '=' . $value[$key];
|
||||
} elseif (is_numeric($key)) {
|
||||
$options[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the entire header string, including
|
||||
* the header name and all values converted to the proper format.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name . ': ' . $this->getValueLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\HTTP\Files\FileCollection;
|
||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||
use Config\App;
|
||||
use Config\Services;
|
||||
use InvalidArgumentException;
|
||||
use Locale;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class IncomingRequest
|
||||
*
|
||||
* Represents an incoming, server-side HTTP request.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - HTTP method
|
||||
* - URI
|
||||
* - Headers
|
||||
* - Message body
|
||||
*
|
||||
* Additionally, it encapsulates all data as it has arrived to the
|
||||
* application from the CGI and/or PHP environment, including:
|
||||
*
|
||||
* - The values represented in $_SERVER.
|
||||
* - Any cookies provided (generally via $_COOKIE)
|
||||
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
|
||||
* - Upload files, if any (as represented by $_FILES)
|
||||
* - Deserialized body binds (generally from $_POST)
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\IncomingRequestTest
|
||||
*/
|
||||
class IncomingRequest extends Request
|
||||
{
|
||||
/**
|
||||
* The URI for this request.
|
||||
*
|
||||
* Note: This WILL NOT match the actual URL in the browser since for
|
||||
* everything this cares about (and the router, etc) is the portion
|
||||
* AFTER the baseURL. So, if hosted in a sub-folder this will
|
||||
* appear different than actual URI path. If you need that use getPath().
|
||||
*
|
||||
* @var URI
|
||||
*/
|
||||
protected $uri;
|
||||
|
||||
/**
|
||||
* The detected URI path (relative to the baseURL).
|
||||
*
|
||||
* Note: current_url() uses this to build its URI,
|
||||
* so this becomes the source for the "current URL"
|
||||
* when working with the share request instance.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* File collection
|
||||
*
|
||||
* @var FileCollection|null
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* Negotiator
|
||||
*
|
||||
* @var Negotiate|null
|
||||
*/
|
||||
protected $negotiator;
|
||||
|
||||
/**
|
||||
* The default Locale this request
|
||||
* should operate under.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultLocale;
|
||||
|
||||
/**
|
||||
* The current locale of the application.
|
||||
* Default value is set in app/Config/App.php
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $locale;
|
||||
|
||||
/**
|
||||
* Stores the valid locale codes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $validLocales = [];
|
||||
|
||||
/**
|
||||
* Holds the old data from a redirect.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $oldInput = [];
|
||||
|
||||
/**
|
||||
* The user agent this request is from.
|
||||
*
|
||||
* @var UserAgent
|
||||
*/
|
||||
protected $userAgent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param App $config
|
||||
* @param string|null $body
|
||||
*/
|
||||
public function __construct($config, ?URI $uri = null, $body = 'php://input', ?UserAgent $userAgent = null)
|
||||
{
|
||||
if (! $uri instanceof URI || ! $userAgent instanceof UserAgent) {
|
||||
throw new InvalidArgumentException('You must supply the parameters: uri, userAgent.');
|
||||
}
|
||||
|
||||
$this->populateHeaders();
|
||||
|
||||
if (
|
||||
$body === 'php://input'
|
||||
// php://input is not available with enctype="multipart/form-data".
|
||||
// See https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input
|
||||
&& ! str_contains($this->getHeaderLine('Content-Type'), 'multipart/form-data')
|
||||
&& (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize()
|
||||
) {
|
||||
// Get our body from php://input
|
||||
$body = file_get_contents('php://input');
|
||||
}
|
||||
|
||||
// If file_get_contents() returns false or empty string, set null.
|
||||
if ($body === false || $body === '') {
|
||||
$body = null;
|
||||
}
|
||||
|
||||
$this->uri = $uri;
|
||||
$this->body = $body;
|
||||
$this->userAgent = $userAgent;
|
||||
$this->validLocales = $config->supportedLocales;
|
||||
|
||||
parent::__construct($config);
|
||||
|
||||
if ($uri instanceof SiteURI) {
|
||||
$this->setPath($uri->getRoutePath());
|
||||
} else {
|
||||
$this->setPath($uri->getPath());
|
||||
}
|
||||
|
||||
$this->detectLocale($config);
|
||||
}
|
||||
|
||||
private function getPostMaxSize(): int
|
||||
{
|
||||
$postMaxSize = ini_get('post_max_size');
|
||||
|
||||
return match (strtoupper(substr($postMaxSize, -1))) {
|
||||
'G' => (int) str_replace('G', '', $postMaxSize) * 1024 ** 3,
|
||||
'M' => (int) str_replace('M', '', $postMaxSize) * 1024 ** 2,
|
||||
'K' => (int) str_replace('K', '', $postMaxSize) * 1024,
|
||||
default => (int) $postMaxSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting up the locale, perhaps auto-detecting through
|
||||
* content negotiation.
|
||||
*
|
||||
* @param App $config
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function detectLocale($config)
|
||||
{
|
||||
$this->locale = $this->defaultLocale = $config->defaultLocale;
|
||||
|
||||
if (! $config->negotiateLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLocale($this->negotiate('language', $config->supportedLocales));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up our URI object based on the information we have. This is
|
||||
* either provided by the user in the baseURL Config setting, or
|
||||
* determined from the environment as needed.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used.
|
||||
*/
|
||||
protected function detectURI(string $protocol, string $baseURL)
|
||||
{
|
||||
$this->setPath($this->detectPath($this->config->uriProtocol), $this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the relative path based on
|
||||
* the URIProtocol Config setting.
|
||||
*
|
||||
* @deprecated 4.4.0 Moved to SiteURIFactory.
|
||||
*/
|
||||
public function detectPath(string $protocol = ''): string
|
||||
{
|
||||
if ($protocol === '') {
|
||||
$protocol = 'REQUEST_URI';
|
||||
}
|
||||
|
||||
$this->path = match ($protocol) {
|
||||
'REQUEST_URI' => $this->parseRequestURI(),
|
||||
'QUERY_STRING' => $this->parseQueryString(),
|
||||
default => $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(),
|
||||
};
|
||||
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will parse the REQUEST_URI and automatically detect the URI from it,
|
||||
* fixing the query string if necessary.
|
||||
*
|
||||
* @return string The URI it found.
|
||||
*
|
||||
* @deprecated 4.4.0 Moved to SiteURIFactory.
|
||||
*/
|
||||
protected function parseRequestURI(): string
|
||||
{
|
||||
if (! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// parse_url() returns false if no host is present, but the path or query string
|
||||
// contains a colon followed by a number. So we attach a dummy host since
|
||||
// REQUEST_URI does not include the host. This allows us to parse out the query string and path.
|
||||
$parts = parse_url('http://dummy' . $_SERVER['REQUEST_URI']);
|
||||
$query = $parts['query'] ?? '';
|
||||
$uri = $parts['path'] ?? '';
|
||||
|
||||
// Strip the SCRIPT_NAME path from the URI
|
||||
if (
|
||||
$uri !== '' && isset($_SERVER['SCRIPT_NAME'][0])
|
||||
&& pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php'
|
||||
) {
|
||||
// Compare each segment, dropping them until there is no match
|
||||
$segments = $keep = explode('/', $uri);
|
||||
|
||||
foreach (explode('/', $_SERVER['SCRIPT_NAME']) as $i => $segment) {
|
||||
// If these segments are not the same then we're done
|
||||
if (! isset($segments[$i]) || $segment !== $segments[$i]) {
|
||||
break;
|
||||
}
|
||||
|
||||
array_shift($keep);
|
||||
}
|
||||
|
||||
$uri = implode('/', $keep);
|
||||
}
|
||||
|
||||
// This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct
|
||||
// URI is found, and also fixes the QUERY_STRING Server var and $_GET array.
|
||||
if (trim($uri, '/') === '' && str_starts_with($query, '/')) {
|
||||
$query = explode('?', $query, 2);
|
||||
$uri = $query[0];
|
||||
$_SERVER['QUERY_STRING'] = $query[1] ?? '';
|
||||
} else {
|
||||
$_SERVER['QUERY_STRING'] = $query;
|
||||
}
|
||||
|
||||
// Update our globals for values likely to been have changed
|
||||
parse_str($_SERVER['QUERY_STRING'], $_GET);
|
||||
$this->populateGlobals('server');
|
||||
$this->populateGlobals('get');
|
||||
|
||||
$uri = URI::removeDotSegments($uri);
|
||||
|
||||
return ($uri === '/' || $uri === '') ? '/' : ltrim($uri, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse QUERY_STRING
|
||||
*
|
||||
* Will parse QUERY_STRING and automatically detect the URI from it.
|
||||
*
|
||||
* @deprecated 4.4.0 Moved to SiteURIFactory.
|
||||
*/
|
||||
protected function parseQueryString(): string
|
||||
{
|
||||
$uri = $_SERVER['QUERY_STRING'] ?? @getenv('QUERY_STRING');
|
||||
|
||||
if (trim($uri, '/') === '') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (str_starts_with($uri, '/')) {
|
||||
$uri = explode('?', $uri, 2);
|
||||
$_SERVER['QUERY_STRING'] = $uri[1] ?? '';
|
||||
$uri = $uri[0];
|
||||
}
|
||||
|
||||
// Update our globals for values likely to been have changed
|
||||
parse_str($_SERVER['QUERY_STRING'], $_GET);
|
||||
$this->populateGlobals('server');
|
||||
$this->populateGlobals('get');
|
||||
|
||||
$uri = URI::removeDotSegments($uri);
|
||||
|
||||
return ($uri === '/' || $uri === '') ? '/' : ltrim($uri, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a convenient way to work with the Negotiate class
|
||||
* for content negotiation.
|
||||
*/
|
||||
public function negotiate(string $type, array $supported, bool $strictMatch = false): string
|
||||
{
|
||||
if ($this->negotiator === null) {
|
||||
$this->negotiator = Services::negotiator($this, true);
|
||||
}
|
||||
|
||||
return match (strtolower($type)) {
|
||||
'media' => $this->negotiator->media($supported, $strictMatch),
|
||||
'charset' => $this->negotiator->charset($supported),
|
||||
'encoding' => $this->negotiator->encoding($supported),
|
||||
'language' => $this->negotiator->language($supported),
|
||||
default => throw HTTPException::forInvalidNegotiationType($type),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks this request type.
|
||||
*
|
||||
* @param string $type HTTP verb or 'json' or 'ajax'
|
||||
* @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type
|
||||
*/
|
||||
public function is(string $type): bool
|
||||
{
|
||||
$valueUpper = strtoupper($type);
|
||||
|
||||
$httpMethods = Method::all();
|
||||
|
||||
if (in_array($valueUpper, $httpMethods, true)) {
|
||||
return $this->getMethod() === $valueUpper;
|
||||
}
|
||||
|
||||
if ($valueUpper === 'JSON') {
|
||||
return str_contains($this->getHeaderLine('Content-Type'), 'application/json');
|
||||
}
|
||||
|
||||
if ($valueUpper === 'AJAX') {
|
||||
return $this->isAJAX();
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown type: ' . $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this request was made from the command line (CLI).
|
||||
*/
|
||||
public function isCLI(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to see if a request contains the HTTP_X_REQUESTED_WITH header.
|
||||
*/
|
||||
public function isAJAX(): bool
|
||||
{
|
||||
return $this->hasHeader('X-Requested-With')
|
||||
&& strtolower($this->header('X-Requested-With')->getValue()) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect if the current connection is secure through
|
||||
* a few different methods.
|
||||
*/
|
||||
public function isSecure(): bool
|
||||
{
|
||||
if (! empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->hasHeader('X-Forwarded-Proto') && $this->header('X-Forwarded-Proto')->getValue() === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasHeader('Front-End-Https') && ! empty($this->header('Front-End-Https')->getValue()) && strtolower($this->header('Front-End-Https')->getValue()) !== 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI path relative to baseURL.
|
||||
*
|
||||
* Note: Since current_url() accesses the shared request
|
||||
* instance, this can be used to change the "current URL"
|
||||
* for testing.
|
||||
*
|
||||
* @param string $path URI path relative to baseURL
|
||||
* @param App|null $config Optional alternate config to use
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated 4.4.0 This method will be private. The parameter $config is deprecated. No longer used.
|
||||
*/
|
||||
public function setPath(string $path, ?App $config = null)
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI path relative to baseURL,
|
||||
* running detection as necessary.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale string for this request.
|
||||
*
|
||||
* @return IncomingRequest
|
||||
*/
|
||||
public function setLocale(string $locale)
|
||||
{
|
||||
// If it's not a valid locale, set it
|
||||
// to the default locale for the site.
|
||||
if (! in_array($locale, $this->validLocales, true)) {
|
||||
$locale = $this->defaultLocale;
|
||||
}
|
||||
|
||||
$this->locale = $locale;
|
||||
Locale::setDefault($locale);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the valid locales.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setValidLocales(array $locales)
|
||||
{
|
||||
$this->validLocales = $locales;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current locale, with a fallback to the default
|
||||
* locale if none is set.
|
||||
*/
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default locale as set in app/Config/App.php
|
||||
*/
|
||||
public function getDefaultLocale(): string
|
||||
{
|
||||
return $this->defaultLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from JSON input stream with fallback to $_REQUEST object. This is the simplest way
|
||||
* to grab data from the request object and can be used in lieu of the
|
||||
* other get* methods in most cases.
|
||||
*
|
||||
* @param array|string|null $index
|
||||
* @param int|null $filter Filter constant
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|stdClass|string|null
|
||||
*/
|
||||
public function getVar($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
if (
|
||||
str_contains($this->getHeaderLine('Content-Type'), 'application/json')
|
||||
&& $this->body !== null
|
||||
) {
|
||||
return $this->getJsonVar($index, false, $filter, $flags);
|
||||
}
|
||||
|
||||
return $this->fetchGlobal('request', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method that grabs the raw input stream and decodes
|
||||
* the JSON into an array.
|
||||
*
|
||||
* If $assoc == true, then all objects in the response will be converted
|
||||
* to associative arrays.
|
||||
*
|
||||
* @param bool $assoc Whether to return objects as associative arrays
|
||||
* @param int $depth How many levels deep to decode
|
||||
* @param int $options Bitmask of options
|
||||
*
|
||||
* @see http://php.net/manual/en/function.json-decode.php
|
||||
*
|
||||
* @return array|bool|float|int|stdClass|null
|
||||
*
|
||||
* @throws HTTPException When the body is invalid as JSON.
|
||||
*/
|
||||
public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0)
|
||||
{
|
||||
if ($this->body === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = json_decode($this->body, $assoc, $depth, $options);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw HTTPException::forInvalidJSON(json_last_error_msg());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific variable from a JSON input stream
|
||||
*
|
||||
* @param array|string|null $index The variable that you want which can use dot syntax for getting specific values.
|
||||
* @param bool $assoc If true, return the result as an associative array.
|
||||
* @param int|null $filter Filter Constant
|
||||
* @param array|int|null $flags Option
|
||||
*
|
||||
* @return array|bool|float|int|stdClass|string|null
|
||||
*/
|
||||
public function getJsonVar($index = null, bool $assoc = false, ?int $filter = null, $flags = null)
|
||||
{
|
||||
helper('array');
|
||||
|
||||
$data = $this->getJSON(true);
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($index)) {
|
||||
$data = dot_array_search($index, $data);
|
||||
} elseif (is_array($index)) {
|
||||
$result = [];
|
||||
|
||||
foreach ($index as $key) {
|
||||
$result[$key] = dot_array_search($key, $data);
|
||||
}
|
||||
|
||||
[$data, $result] = [$result, null];
|
||||
}
|
||||
|
||||
if ($data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filter ??= FILTER_DEFAULT;
|
||||
$flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);
|
||||
|
||||
if ($filter !== FILTER_DEFAULT
|
||||
|| (
|
||||
(is_numeric($flags) && $flags !== 0)
|
||||
|| is_array($flags) && $flags !== []
|
||||
)
|
||||
) {
|
||||
if (is_array($data)) {
|
||||
// Iterate over array and append filter and flags
|
||||
array_walk_recursive($data, static function (&$val) use ($filter, $flags): void {
|
||||
$valType = gettype($val);
|
||||
$val = filter_var($val, $filter, $flags);
|
||||
|
||||
if (in_array($valType, ['int', 'integer', 'float', 'double', 'bool', 'boolean'], true) && $val !== false) {
|
||||
settype($val, $valType);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$dataType = gettype($data);
|
||||
$data = filter_var($data, $filter, $flags);
|
||||
|
||||
if (in_array($dataType, ['int', 'integer', 'float', 'double', 'bool', 'boolean'], true) && $data !== false) {
|
||||
settype($data, $dataType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $assoc) {
|
||||
if (is_array($index)) {
|
||||
foreach ($data as &$val) {
|
||||
$val = is_array($val) ? json_decode(json_encode($val)) : $val;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return json_decode(json_encode($data));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes
|
||||
* the String into an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRawInput()
|
||||
{
|
||||
parse_str($this->body ?? '', $output);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific variable from raw input stream (send method in PUT, PATCH, DELETE).
|
||||
*
|
||||
* @param array|string|null $index The variable that you want which can use dot syntax for getting specific values.
|
||||
* @param int|null $filter Filter Constant
|
||||
* @param array|int|null $flags Option
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getRawInputVar($index = null, ?int $filter = null, $flags = null)
|
||||
{
|
||||
helper('array');
|
||||
|
||||
parse_str($this->body ?? '', $output);
|
||||
|
||||
if (is_string($index)) {
|
||||
$output = dot_array_search($index, $output);
|
||||
} elseif (is_array($index)) {
|
||||
$data = [];
|
||||
|
||||
foreach ($index as $key) {
|
||||
$data[$key] = dot_array_search($key, $output);
|
||||
}
|
||||
|
||||
[$output, $data] = [$data, null];
|
||||
}
|
||||
|
||||
$filter ??= FILTER_DEFAULT;
|
||||
$flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);
|
||||
|
||||
if (is_array($output)
|
||||
&& (
|
||||
$filter !== FILTER_DEFAULT
|
||||
|| (
|
||||
(is_numeric($flags) && $flags !== 0)
|
||||
|| is_array($flags) && $flags !== []
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Iterate over array and append filter and flags
|
||||
array_walk_recursive($output, static function (&$val) use ($filter, $flags): void {
|
||||
$val = filter_var($val, $filter, $flags);
|
||||
});
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
if (is_string($output)) {
|
||||
return filter_var($output, $filter, $flags);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from GET data.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_GET.
|
||||
* @param int|null $filter A filter name to apply.
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getGet($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->fetchGlobal('get', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from POST.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_POST.
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getPost($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->fetchGlobal('post', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from POST data with fallback to GET.
|
||||
*
|
||||
* @param array|string|null $index Index for item to fetch from $_POST or $_GET
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getPostGet($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
if ($index === null) {
|
||||
return array_merge($this->getGet($index, $filter, $flags), $this->getPost($index, $filter, $flags));
|
||||
}
|
||||
|
||||
// Use $_POST directly here, since filter_has_var only
|
||||
// checks the initial POST data, not anything that might
|
||||
// have been added since.
|
||||
return isset($_POST[$index])
|
||||
? $this->getPost($index, $filter, $flags)
|
||||
: (isset($_GET[$index]) ? $this->getGet($index, $filter, $flags) : $this->getPost($index, $filter, $flags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from GET data with fallback to POST.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_GET or $_POST
|
||||
* @param int|null $filter A filter name to apply
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getGetPost($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
if ($index === null) {
|
||||
return array_merge($this->getPost($index, $filter, $flags), $this->getGet($index, $filter, $flags));
|
||||
}
|
||||
|
||||
// Use $_GET directly here, since filter_has_var only
|
||||
// checks the initial GET data, not anything that might
|
||||
// have been added since.
|
||||
return isset($_GET[$index])
|
||||
? $this->getGet($index, $filter, $flags)
|
||||
: (isset($_POST[$index]) ? $this->getPost($index, $filter, $flags) : $this->getGet($index, $filter, $flags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from the COOKIE array.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_COOKIE
|
||||
* @param int|null $filter A filter name to be applied
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function getCookie($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->fetchGlobal('cookie', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user agent string
|
||||
*
|
||||
* @return UserAgent
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get old Input data that has been flashed to the session
|
||||
* with redirect_with_input(). It first checks for the data in the old
|
||||
* POST data, then the old GET data and finally check for dot arrays
|
||||
*
|
||||
* @return array|string|null
|
||||
*/
|
||||
public function getOldInput(string $key)
|
||||
{
|
||||
// If the session hasn't been started, we're done.
|
||||
if (! isset($_SESSION)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get previously saved in session
|
||||
$old = session('_ci_old_input');
|
||||
|
||||
// If no data was previously saved, we're done.
|
||||
if ($old === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for the value in the POST array first.
|
||||
if (isset($old['post'][$key])) {
|
||||
return $old['post'][$key];
|
||||
}
|
||||
|
||||
// Next check in the GET array.
|
||||
if (isset($old['get'][$key])) {
|
||||
return $old['get'][$key];
|
||||
}
|
||||
|
||||
helper('array');
|
||||
|
||||
// Check for an array value in POST.
|
||||
if (isset($old['post'])) {
|
||||
$value = dot_array_search($key, $old['post']);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for an array value in GET.
|
||||
if (isset($old['get'])) {
|
||||
$value = dot_array_search($key, $old['get']);
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// requested session key not found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all files that have been uploaded with this
|
||||
* request. Each file is represented by an UploadedFile instance.
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
if ($this->files === null) {
|
||||
$this->files = new FileCollection();
|
||||
}
|
||||
|
||||
return $this->files->all(); // return all files
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a file exist, by the name of the input field used to upload it, in the collection
|
||||
* of uploaded files and if is have been uploaded with multiple option.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getFileMultiple(string $fileID)
|
||||
{
|
||||
if ($this->files === null) {
|
||||
$this->files = new FileCollection();
|
||||
}
|
||||
|
||||
return $this->files->getFileMultiple($fileID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single file by the name of the input field used
|
||||
* to upload it.
|
||||
*
|
||||
* @return UploadedFile|null
|
||||
*/
|
||||
public function getFile(string $fileID)
|
||||
{
|
||||
if ($this->files === null) {
|
||||
$this->files = new FileCollection();
|
||||
}
|
||||
|
||||
return $this->files->getFile($fileID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* An HTTP message
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\MessageTest
|
||||
*/
|
||||
class Message implements MessageInterface
|
||||
{
|
||||
use MessageTrait;
|
||||
|
||||
/**
|
||||
* Protocol version
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $protocolVersion;
|
||||
|
||||
/**
|
||||
* List of valid protocol versions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $validProtocolVersions = [
|
||||
'1.0',
|
||||
'1.1',
|
||||
'2.0',
|
||||
'3.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* Message body
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
/**
|
||||
* Returns the Message's body.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all headers.
|
||||
*
|
||||
* @return array<string, Header> An array of the request headers
|
||||
*
|
||||
* @deprecated Use Message::headers() to make room for PSR-7
|
||||
*
|
||||
* @TODO Incompatible return value with PSR-7
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single header object. If multiple headers with the same
|
||||
* name exist, then will return an array of header objects.
|
||||
*
|
||||
* @return array|Header|null
|
||||
*
|
||||
* @deprecated Use Message::header() to make room for PSR-7
|
||||
*
|
||||
* @TODO Incompatible return value with PSR-7
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getHeader(string $name)
|
||||
{
|
||||
return $this->header($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a header exists.
|
||||
*/
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
return isset($this->headers[$origName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*/
|
||||
public function getHeaderLine(string $name): string
|
||||
{
|
||||
if ($this->hasMultipleHeaders($name)) {
|
||||
throw new InvalidArgumentException(
|
||||
'The header "' . $name . '" already has multiple headers.'
|
||||
. ' You cannot use getHeaderLine().'
|
||||
);
|
||||
}
|
||||
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
if (! array_key_exists($origName, $this->headers)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->headers[$origName]->getValueLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP Protocol Version.
|
||||
*/
|
||||
public function getProtocolVersion(): string
|
||||
{
|
||||
return $this->protocolVersion ?? '1.1';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
|
||||
/**
|
||||
* Expected behavior of an HTTP message
|
||||
*/
|
||||
interface MessageInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves the HTTP protocol version as a string.
|
||||
*
|
||||
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
|
||||
*
|
||||
* @return string HTTP protocol version.
|
||||
*/
|
||||
public function getProtocolVersion(): string;
|
||||
|
||||
/**
|
||||
* Sets the body of the current message.
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBody($data);
|
||||
|
||||
/**
|
||||
* Gets the body of the message.
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @TODO Incompatible return type with PSR-7
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Appends data to the body of the current message.
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendBody($data);
|
||||
|
||||
/**
|
||||
* Populates the $headers array with any headers the server knows about.
|
||||
*/
|
||||
public function populateHeaders(): void;
|
||||
|
||||
/**
|
||||
* Returns an array containing all Headers.
|
||||
*
|
||||
* @return array<string, Header|list<Header>> An array of the Header objects
|
||||
*/
|
||||
public function headers(): array;
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
*
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Returns a single Header object. If multiple headers with the same
|
||||
* name exist, then will return an array of header objects.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Header|list<Header>|null
|
||||
*/
|
||||
public function header($name);
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*/
|
||||
public function getHeaderLine(string $name): string;
|
||||
|
||||
/**
|
||||
* Sets a header and it's value.
|
||||
*
|
||||
* @param array|string|null $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHeader(string $name, $value);
|
||||
|
||||
/**
|
||||
* Removes a header from the list of headers we track.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeHeader(string $name);
|
||||
|
||||
/**
|
||||
* Adds an additional header value to any headers that accept
|
||||
* multiple values (i.e. are an array or implement ArrayAccess)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendHeader(string $name, ?string $value);
|
||||
|
||||
/**
|
||||
* Adds an additional header value to any headers that accept
|
||||
* multiple values (i.e. are an array or implement ArrayAccess)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prependHeader(string $name, string $value);
|
||||
|
||||
/**
|
||||
* Sets the HTTP protocol version.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws HTTPException For invalid protocols
|
||||
*/
|
||||
public function setProtocolVersion(string $version);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Message Trait
|
||||
* Additional methods to make a PSR-7 Message class
|
||||
* compliant with the framework's own MessageInterface.
|
||||
*
|
||||
* @see https://github.com/php-fig/http-message/blob/master/src/MessageInterface.php
|
||||
*/
|
||||
trait MessageTrait
|
||||
{
|
||||
/**
|
||||
* List of all HTTP request headers.
|
||||
*
|
||||
* [name => Header]
|
||||
* or
|
||||
* [name => [Header1, Header2]]
|
||||
*
|
||||
* @var array<string, Header|list<Header>>
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* Holds a map of lower-case header names
|
||||
* and their normal-case key as it is in $headers.
|
||||
* Used for case-insensitive header access.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headerMap = [];
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Body
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the body of the current message.
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBody($data): self
|
||||
{
|
||||
$this->body = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends data to the body of the current message.
|
||||
*
|
||||
* @param string $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendBody($data): self
|
||||
{
|
||||
$this->body .= (string) $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Headers
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Populates the $headers array with any headers the server knows about.
|
||||
*/
|
||||
public function populateHeaders(): void
|
||||
{
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? getenv('CONTENT_TYPE');
|
||||
if (! empty($contentType)) {
|
||||
$this->setHeader('Content-Type', $contentType);
|
||||
}
|
||||
unset($contentType);
|
||||
|
||||
foreach (array_keys($_SERVER) as $key) {
|
||||
if (sscanf($key, 'HTTP_%s', $header) === 1) {
|
||||
// take SOME_HEADER and turn it into Some-Header
|
||||
$header = str_replace('_', ' ', strtolower($header));
|
||||
$header = str_replace(' ', '-', ucwords($header));
|
||||
|
||||
$this->setHeader($header, $_SERVER[$key]);
|
||||
|
||||
// Add us to the header map, so we can find them case-insensitively
|
||||
$this->headerMap[strtolower($header)] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing all Headers.
|
||||
*
|
||||
* @return array<string, Header|list<Header>> An array of the Header objects
|
||||
*/
|
||||
public function headers(): array
|
||||
{
|
||||
// If no headers are defined, but the user is
|
||||
// requesting it, then it's likely they want
|
||||
// it to be populated so do that...
|
||||
if (empty($this->headers)) {
|
||||
$this->populateHeaders();
|
||||
}
|
||||
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single Header object. If multiple headers with the same
|
||||
* name exist, then will return an array of header objects.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Header|list<Header>|null
|
||||
*/
|
||||
public function header($name)
|
||||
{
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
return $this->headers[$origName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a header and it's value.
|
||||
*
|
||||
* @param array|string|null $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHeader(string $name, $value): self
|
||||
{
|
||||
$this->checkMultipleHeaders($name);
|
||||
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
if (
|
||||
isset($this->headers[$origName])
|
||||
&& is_array($this->headers[$origName]->getValue())
|
||||
) {
|
||||
if (! is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
foreach ($value as $v) {
|
||||
$this->appendHeader($origName, $v);
|
||||
}
|
||||
} else {
|
||||
$this->headers[$origName] = new Header($origName, $value);
|
||||
$this->headerMap[strtolower($origName)] = $origName;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function hasMultipleHeaders(string $name): bool
|
||||
{
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
return isset($this->headers[$origName]) && is_array($this->headers[$origName]);
|
||||
}
|
||||
|
||||
private function checkMultipleHeaders(string $name): void
|
||||
{
|
||||
if ($this->hasMultipleHeaders($name)) {
|
||||
throw new InvalidArgumentException(
|
||||
'The header "' . $name . '" already has multiple headers.'
|
||||
. ' You cannot change them. If you really need to change, remove the header first.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a header from the list of headers we track.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeHeader(string $name): self
|
||||
{
|
||||
$origName = $this->getHeaderName($name);
|
||||
unset($this->headers[$origName], $this->headerMap[strtolower($name)]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an additional header value to any headers that accept
|
||||
* multiple values (i.e. are an array or implement ArrayAccess)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendHeader(string $name, ?string $value): self
|
||||
{
|
||||
$this->checkMultipleHeaders($name);
|
||||
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
array_key_exists($origName, $this->headers)
|
||||
? $this->headers[$origName]->appendValue($value)
|
||||
: $this->setHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header (not a header value) with the same name.
|
||||
* Use this only when you set multiple headers with the same name,
|
||||
* typically, for `Set-Cookie`.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addHeader(string $name, string $value): static
|
||||
{
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
if (! isset($this->headers[$origName])) {
|
||||
$this->setHeader($name, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (! $this->hasMultipleHeaders($name) && isset($this->headers[$origName])) {
|
||||
$this->headers[$origName] = [$this->headers[$origName]];
|
||||
}
|
||||
|
||||
// Add the header.
|
||||
$this->headers[$origName][] = new Header($origName, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an additional header value to any headers that accept
|
||||
* multiple values (i.e. are an array or implement ArrayAccess)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prependHeader(string $name, string $value): self
|
||||
{
|
||||
$this->checkMultipleHeaders($name);
|
||||
|
||||
$origName = $this->getHeaderName($name);
|
||||
|
||||
$this->headers[$origName]->prependValue($value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a header name in any case, and returns the
|
||||
* normal-case version of the header.
|
||||
*/
|
||||
protected function getHeaderName(string $name): string
|
||||
{
|
||||
return $this->headerMap[strtolower($name)] ?? $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP protocol version.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws HTTPException For invalid protocols
|
||||
*/
|
||||
public function setProtocolVersion(string $version): self
|
||||
{
|
||||
if (! is_numeric($version)) {
|
||||
$version = substr($version, strpos($version, '/') + 1);
|
||||
}
|
||||
|
||||
// Make sure that version is in the correct format
|
||||
$version = number_format((float) $version, 1);
|
||||
|
||||
if (! in_array($version, $this->validProtocolVersions, true)) {
|
||||
throw HTTPException::forInvalidHTTPProtocol($version);
|
||||
}
|
||||
|
||||
$this->protocolVersion = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP Method List
|
||||
*/
|
||||
class Method
|
||||
{
|
||||
/**
|
||||
* Safe: No
|
||||
* Idempotent: No
|
||||
* Cacheable: No
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT
|
||||
*/
|
||||
public const CONNECT = 'CONNECT';
|
||||
|
||||
/**
|
||||
* Safe: No
|
||||
* Idempotent: Yes
|
||||
* Cacheable: No
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE
|
||||
*/
|
||||
public const DELETE = 'DELETE';
|
||||
|
||||
/**
|
||||
* Safe: Yes
|
||||
* Idempotent: Yes
|
||||
* Cacheable: Yes
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET
|
||||
*/
|
||||
public const GET = 'GET';
|
||||
|
||||
/**
|
||||
* Safe: Yes
|
||||
* Idempotent: Yes
|
||||
* Cacheable: Yes
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
|
||||
*/
|
||||
public const HEAD = 'HEAD';
|
||||
|
||||
/**
|
||||
* Safe: Yes
|
||||
* Idempotent: Yes
|
||||
* Cacheable: No
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
|
||||
*/
|
||||
public const OPTIONS = 'OPTIONS';
|
||||
|
||||
/**
|
||||
* Safe: No
|
||||
* Idempotent: No
|
||||
* Cacheable: Only if freshness information is included
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH
|
||||
*/
|
||||
public const PATCH = 'PATCH';
|
||||
|
||||
/**
|
||||
* Safe: No
|
||||
* Idempotent: No
|
||||
* Cacheable: Only if freshness information is included
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
|
||||
*/
|
||||
public const POST = 'POST';
|
||||
|
||||
/**
|
||||
* Safe: No
|
||||
* Idempotent: Yes
|
||||
* Cacheable: No
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
|
||||
*/
|
||||
public const PUT = 'PUT';
|
||||
|
||||
/**
|
||||
* Safe: Yes
|
||||
* Idempotent: Yes
|
||||
* Cacheable: No
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE
|
||||
*/
|
||||
public const TRACE = 'TRACE';
|
||||
|
||||
/**
|
||||
* Returns all HTTP methods.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
self::CONNECT,
|
||||
self::DELETE,
|
||||
self::GET,
|
||||
self::HEAD,
|
||||
self::OPTIONS,
|
||||
self::PATCH,
|
||||
self::POST,
|
||||
self::PUT,
|
||||
self::TRACE,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
|
||||
/**
|
||||
* Class Negotiate
|
||||
*
|
||||
* Provides methods to negotiate with the HTTP headers to determine the best
|
||||
* type match between what the application supports and what the requesting
|
||||
* server wants.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc7231#section-5.3
|
||||
* @see \CodeIgniter\HTTP\NegotiateTest
|
||||
*/
|
||||
class Negotiate
|
||||
{
|
||||
/**
|
||||
* Request
|
||||
*
|
||||
* @var IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(?RequestInterface $request = null)
|
||||
{
|
||||
if ($request !== null) {
|
||||
assert($request instanceof IncomingRequest);
|
||||
|
||||
$this->request = $request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the request instance to grab the headers from.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRequest(RequestInterface $request)
|
||||
{
|
||||
assert($request instanceof IncomingRequest);
|
||||
|
||||
$this->request = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the best content-type to use based on the $supported
|
||||
* types the application says it supports, and the types requested
|
||||
* by the client.
|
||||
*
|
||||
* If no match is found, the first, highest-ranking client requested
|
||||
* type is returned.
|
||||
*
|
||||
* @param bool $strictMatch If TRUE, will return an empty string when no match found.
|
||||
* If FALSE, will return the first supported element.
|
||||
*/
|
||||
public function media(array $supported, bool $strictMatch = false): string
|
||||
{
|
||||
return $this->getBestMatch($supported, $this->request->getHeaderLine('accept'), true, $strictMatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the best charset to use based on the $supported
|
||||
* types the application says it supports, and the types requested
|
||||
* by the client.
|
||||
*
|
||||
* If no match is found, the first, highest-ranking client requested
|
||||
* type is returned.
|
||||
*/
|
||||
public function charset(array $supported): string
|
||||
{
|
||||
$match = $this->getBestMatch(
|
||||
$supported,
|
||||
$this->request->getHeaderLine('accept-charset'),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
// If no charset is shown as a match, ignore the directive
|
||||
// as allowed by the RFC, and tell it a default value.
|
||||
if ($match === '') {
|
||||
return 'utf-8';
|
||||
}
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the best encoding type to use based on the $supported
|
||||
* types the application says it supports, and the types requested
|
||||
* by the client.
|
||||
*
|
||||
* If no match is found, the first, highest-ranking client requested
|
||||
* type is returned.
|
||||
*/
|
||||
public function encoding(array $supported = []): string
|
||||
{
|
||||
$supported[] = 'identity';
|
||||
|
||||
return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-encoding'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the best language to use based on the $supported
|
||||
* types the application says it supports, and the types requested
|
||||
* by the client.
|
||||
*
|
||||
* If no match is found, the first, highest-ranking client requested
|
||||
* type is returned.
|
||||
*/
|
||||
public function language(array $supported): string
|
||||
{
|
||||
return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Utility Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Does the grunt work of comparing any of the app-supported values
|
||||
* against a given Accept* header string.
|
||||
*
|
||||
* Portions of this code base on Aura.Accept library.
|
||||
*
|
||||
* @param array $supported App-supported values
|
||||
* @param string $header header string
|
||||
* @param bool $enforceTypes If TRUE, will compare media types and sub-types.
|
||||
* @param bool $strictMatch If TRUE, will return empty string on no match.
|
||||
* If FALSE, will return the first supported element.
|
||||
* @param bool $matchLocales If TRUE, will match locale sub-types to a broad type (fr-FR = fr)
|
||||
*
|
||||
* @return string Best match
|
||||
*/
|
||||
protected function getBestMatch(
|
||||
array $supported,
|
||||
?string $header = null,
|
||||
bool $enforceTypes = false,
|
||||
bool $strictMatch = false,
|
||||
bool $matchLocales = false
|
||||
): string {
|
||||
if ($supported === []) {
|
||||
throw HTTPException::forEmptySupportedNegotiations();
|
||||
}
|
||||
|
||||
if ($header === null || $header === '') {
|
||||
return $strictMatch ? '' : $supported[0];
|
||||
}
|
||||
|
||||
$acceptable = $this->parseHeader($header);
|
||||
|
||||
foreach ($acceptable as $accept) {
|
||||
// if acceptable quality is zero, skip it.
|
||||
if ($accept['q'] === 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if acceptable value is "anything", return the first available
|
||||
if ($accept['value'] === '*' || $accept['value'] === '*/*') {
|
||||
return $supported[0];
|
||||
}
|
||||
|
||||
// If an acceptable value is supported, return it
|
||||
foreach ($supported as $available) {
|
||||
if ($this->match($accept, $available, $enforceTypes, $matchLocales)) {
|
||||
return $available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matches? Return the first supported element.
|
||||
return $strictMatch ? '' : $supported[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Accept* header into it's multiple values.
|
||||
*
|
||||
* This is based on code from Aura.Accept library.
|
||||
*/
|
||||
public function parseHeader(string $header): array
|
||||
{
|
||||
$results = [];
|
||||
$acceptable = explode(',', $header);
|
||||
|
||||
foreach ($acceptable as $value) {
|
||||
$pairs = explode(';', $value);
|
||||
|
||||
$value = $pairs[0];
|
||||
|
||||
unset($pairs[0]);
|
||||
|
||||
$parameters = [];
|
||||
|
||||
foreach ($pairs as $pair) {
|
||||
if (preg_match(
|
||||
'/^(?P<name>.+?)=(?P<quoted>"|\')?(?P<value>.*?)(?:\k<quoted>)?$/',
|
||||
$pair,
|
||||
$param
|
||||
)) {
|
||||
$parameters[trim($param['name'])] = trim($param['value']);
|
||||
}
|
||||
}
|
||||
|
||||
$quality = 1.0;
|
||||
|
||||
if (array_key_exists('q', $parameters)) {
|
||||
$quality = $parameters['q'];
|
||||
unset($parameters['q']);
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'value' => trim($value),
|
||||
'q' => (float) $quality,
|
||||
'params' => $parameters,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort to get the highest results first
|
||||
usort($results, static function ($a, $b) {
|
||||
if ($a['q'] === $b['q']) {
|
||||
$aAst = substr_count($a['value'], '*');
|
||||
$bAst = substr_count($b['value'], '*');
|
||||
|
||||
// '*/*' has lower precedence than 'text/*',
|
||||
// and 'text/*' has lower priority than 'text/plain'
|
||||
//
|
||||
// This seems backwards, but needs to be that way
|
||||
// due to the way PHP7 handles ordering or array
|
||||
// elements created by reference.
|
||||
if ($aAst > $bAst) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If the counts are the same, but one element
|
||||
// has more params than another, it has higher precedence.
|
||||
//
|
||||
// This seems backwards, but needs to be that way
|
||||
// due to the way PHP7 handles ordering or array
|
||||
// elements created by reference.
|
||||
if ($aAst === $bAst) {
|
||||
return count($b['params']) - count($a['params']);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Still here? Higher q values have precedence.
|
||||
return ($a['q'] > $b['q']) ? -1 : 1;
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match-maker
|
||||
*
|
||||
* @param bool $matchLocales
|
||||
*/
|
||||
protected function match(array $acceptable, string $supported, bool $enforceTypes = false, $matchLocales = false): bool
|
||||
{
|
||||
$supported = $this->parseHeader($supported);
|
||||
if (count($supported) === 1) {
|
||||
$supported = $supported[0];
|
||||
}
|
||||
|
||||
// Is it an exact match?
|
||||
if ($acceptable['value'] === $supported['value']) {
|
||||
return $this->matchParameters($acceptable, $supported);
|
||||
}
|
||||
|
||||
// Do we need to compare types/sub-types? Only used
|
||||
// by negotiateMedia().
|
||||
if ($enforceTypes) {
|
||||
return $this->matchTypes($acceptable, $supported);
|
||||
}
|
||||
|
||||
// Do we need to match locales against broader locales?
|
||||
if ($matchLocales) {
|
||||
return $this->matchLocales($acceptable, $supported);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks two Accept values with matching 'values' to see if their
|
||||
* 'params' are the same.
|
||||
*/
|
||||
protected function matchParameters(array $acceptable, array $supported): bool
|
||||
{
|
||||
if (count($acceptable['params']) !== count($supported['params'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($supported['params'] as $label => $value) {
|
||||
if (! isset($acceptable['params'][$label])
|
||||
|| $acceptable['params'][$label] !== $value
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the types/subtypes of an acceptable Media type and
|
||||
* the supported string.
|
||||
*/
|
||||
public function matchTypes(array $acceptable, array $supported): bool
|
||||
{
|
||||
// PHPDocumentor v2 cannot parse yet the shorter list syntax,
|
||||
// causing no API generation for the file.
|
||||
[$aType, $aSubType] = explode('/', $acceptable['value']);
|
||||
[$sType, $sSubType] = explode('/', $supported['value']);
|
||||
|
||||
// If the types don't match, we're done.
|
||||
if ($aType !== $sType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there's an asterisk, we're cool
|
||||
if ($aSubType === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, subtypes must match also.
|
||||
return $aSubType === $sSubType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will match locales against their broader pairs, so that fr-FR would
|
||||
* match a supported localed of fr
|
||||
*/
|
||||
public function matchLocales(array $acceptable, array $supported): bool
|
||||
{
|
||||
$aBroad = mb_strpos($acceptable['value'], '-') > 0
|
||||
? mb_substr($acceptable['value'], 0, mb_strpos($acceptable['value'], '-'))
|
||||
: $acceptable['value'];
|
||||
$sBroad = mb_strpos($supported['value'], '-') > 0
|
||||
? mb_substr($supported['value'], 0, mb_strpos($supported['value'], '-'))
|
||||
: $supported['value'];
|
||||
|
||||
return strtolower($aBroad) === strtolower($sBroad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, client-side request.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\OutgoingRequestTest
|
||||
*/
|
||||
class OutgoingRequest extends Message implements OutgoingRequestInterface
|
||||
{
|
||||
/**
|
||||
* Request method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $method;
|
||||
|
||||
/**
|
||||
* A URI instance.
|
||||
*
|
||||
* @var URI|null
|
||||
*/
|
||||
protected $uri;
|
||||
|
||||
/**
|
||||
* @param string $method HTTP method
|
||||
* @param string|null $body
|
||||
*/
|
||||
public function __construct(
|
||||
string $method,
|
||||
?URI $uri = null,
|
||||
array $headers = [],
|
||||
$body = null,
|
||||
string $version = '1.1'
|
||||
) {
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
|
||||
foreach ($headers as $header => $value) {
|
||||
$this->setHeader($header, $value);
|
||||
}
|
||||
|
||||
$this->body = $body;
|
||||
$this->protocolVersion = $version;
|
||||
|
||||
if (! $this->hasHeader('Host') && $this->uri->getHost() !== '') {
|
||||
$this->setHeader('Host', $this->getHostFromUri($this->uri));
|
||||
}
|
||||
}
|
||||
|
||||
private function getHostFromUri(URI $uri): string
|
||||
{
|
||||
$host = $uri->getHost();
|
||||
|
||||
return $host . ($uri->getPort() ? ':' . $uri->getPort() : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP method of the request.
|
||||
*
|
||||
* @return string Returns the request method (always uppercase)
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request method. Used when spoofing the request.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated Use withMethod() instead for immutability
|
||||
*/
|
||||
public function setMethod(string $method)
|
||||
{
|
||||
$this->method = $method;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified method.
|
||||
*
|
||||
* @param string $method
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withMethod($method)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->method = $method;
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* @return URI|null
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the provided URI.
|
||||
*
|
||||
* @param URI $uri New request URI to use.
|
||||
* @param bool $preserveHost Preserve the original state of the Host header.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withUri(URI $uri, $preserveHost = false)
|
||||
{
|
||||
$request = clone $this;
|
||||
$request->uri = $uri;
|
||||
|
||||
if ($preserveHost) {
|
||||
if ($this->isHostHeaderMissingOrEmpty() && $uri->getHost() !== '') {
|
||||
$request->setHeader('Host', $this->getHostFromUri($uri));
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
if ($this->isHostHeaderMissingOrEmpty() && $uri->getHost() === '') {
|
||||
return $request;
|
||||
}
|
||||
|
||||
if (! $this->isHostHeaderMissingOrEmpty()) {
|
||||
return $request;
|
||||
}
|
||||
}
|
||||
|
||||
if ($uri->getHost() !== '') {
|
||||
$request->setHeader('Host', $this->getHostFromUri($uri));
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function isHostHeaderMissingOrEmpty(): bool
|
||||
{
|
||||
if (! $this->hasHeader('Host')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->header('Host')->getValue() === '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, client-side request.
|
||||
*
|
||||
* Corresponds to Psr7\RequestInterface.
|
||||
*/
|
||||
interface OutgoingRequestInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves the HTTP method of the request.
|
||||
*
|
||||
* @return string Returns the request method.
|
||||
*/
|
||||
public function getMethod(): string;
|
||||
|
||||
/**
|
||||
* Return an instance with the provided HTTP method.
|
||||
*
|
||||
* While HTTP method names are typically all uppercase characters, HTTP
|
||||
* method names are case-sensitive and thus implementations SHOULD NOT
|
||||
* modify the given string.
|
||||
*
|
||||
* 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
|
||||
* changed request method.
|
||||
*
|
||||
* @param string $method Case-sensitive method.
|
||||
*
|
||||
* @return static
|
||||
*
|
||||
* @throws InvalidArgumentException for invalid HTTP methods.
|
||||
*/
|
||||
public function withMethod($method);
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
*
|
||||
* @return URI
|
||||
*/
|
||||
public function getUri();
|
||||
|
||||
/**
|
||||
* Returns an instance with the provided URI.
|
||||
*
|
||||
* This method MUST update the Host header of the returned request by
|
||||
* default if the URI contains a host component. If the URI does not
|
||||
* contain a host component, any pre-existing Host header MUST be carried
|
||||
* over to the returned request.
|
||||
*
|
||||
* You can opt-in to preserving the original state of the Host header by
|
||||
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
|
||||
* `true`, this method interacts with the Host header in the following ways:
|
||||
*
|
||||
* - If the Host header is missing or empty, and the new URI contains
|
||||
* a host component, this method MUST update the Host header in the returned
|
||||
* request.
|
||||
* - If the Host header is missing or empty, and the new URI does not contain a
|
||||
* host component, this method MUST NOT update the Host header in the returned
|
||||
* request.
|
||||
* - If a Host header is present and non-empty, this method MUST NOT update
|
||||
* the Host header in the returned request.
|
||||
*
|
||||
* 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
|
||||
* new UriInterface instance.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.3
|
||||
*
|
||||
* @param URI $uri New request URI to use.
|
||||
* @param bool $preserveHost Preserve the original state of the Host header.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withUri(URI $uri, $preserveHost = false);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Cookie\CookieStore;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Handle a redirect response
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\RedirectResponseTest
|
||||
*/
|
||||
class RedirectResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Sets the URI to redirect to and, optionally, the HTTP status code to use.
|
||||
* If no code is provided it will be automatically determined.
|
||||
*
|
||||
* @param string $uri The URI path (relative to baseURL) to redirect to
|
||||
* @param int|null $code HTTP status code
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function to(string $uri, ?int $code = null, string $method = 'auto')
|
||||
{
|
||||
// If it appears to be a relative URL, then convert to full URL
|
||||
// for better security.
|
||||
if (! str_starts_with($uri, 'http')) {
|
||||
$uri = site_url($uri);
|
||||
}
|
||||
|
||||
return $this->redirect($uri, $method, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI to redirect to but as a reverse-routed or named route
|
||||
* instead of a raw URI.
|
||||
*
|
||||
* @param string $route Route name or Controller::method
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws HTTPException
|
||||
*/
|
||||
public function route(string $route, array $params = [], ?int $code = null, string $method = 'auto')
|
||||
{
|
||||
$namedRoute = $route;
|
||||
|
||||
$route = service('routes')->reverseRoute($route, ...$params);
|
||||
|
||||
if (! $route) {
|
||||
throw HTTPException::forInvalidRedirectRoute($namedRoute);
|
||||
}
|
||||
|
||||
return $this->redirect(site_url($route), $method, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return to previous page.
|
||||
*
|
||||
* Example:
|
||||
* return redirect()->back();
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function back(?int $code = null, string $method = 'auto')
|
||||
{
|
||||
service('session');
|
||||
|
||||
return $this->redirect(previous_url(), $method, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current $_GET and $_POST arrays in the session.
|
||||
* This also saves the validation errors.
|
||||
*
|
||||
* It will then be available via the 'old()' helper function.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withInput()
|
||||
{
|
||||
$session = service('session');
|
||||
$session->setFlashdata('_ci_old_input', [
|
||||
'get' => $_GET ?? [],
|
||||
'post' => $_POST ?? [],
|
||||
]);
|
||||
|
||||
$this->withErrors();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets validation errors in the session.
|
||||
*
|
||||
* If the validation has any errors, transmit those back
|
||||
* so they can be displayed when the validation is handled
|
||||
* within a method different than displaying the form.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
private function withErrors(): self
|
||||
{
|
||||
$validation = service('validation');
|
||||
|
||||
if ($validation->getErrors()) {
|
||||
$session = service('session');
|
||||
$session->setFlashdata('_ci_validation_errors', $validation->getErrors());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key and message to the session as Flashdata.
|
||||
*
|
||||
* @param array|string $message
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function with(string $key, $message)
|
||||
{
|
||||
service('session')->setFlashdata($key, $message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies any cookies from the global Response instance
|
||||
* into this RedirectResponse. Useful when you've just
|
||||
* set a cookie but need ensure that's actually sent
|
||||
* with the response instead of lost.
|
||||
*
|
||||
* @return $this|RedirectResponse
|
||||
*/
|
||||
public function withCookies()
|
||||
{
|
||||
$this->cookieStore = new CookieStore(Services::response()->getCookies());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies any headers from the global Response instance
|
||||
* into this RedirectResponse. Useful when you've just
|
||||
* set a header be need to ensure its actually sent
|
||||
* with the redirect response.
|
||||
*
|
||||
* @return $this|RedirectResponse
|
||||
*/
|
||||
public function withHeaders()
|
||||
{
|
||||
foreach (Services::response()->headers() as $name => $value) {
|
||||
if ($value instanceof Header) {
|
||||
$this->setHeader($name, $value->getValue());
|
||||
} else {
|
||||
foreach ($value as $header) {
|
||||
$this->addHeader($name, $header->getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use Config\App;
|
||||
|
||||
/**
|
||||
* Representation of an incoming, server-side HTTP request.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\RequestTest
|
||||
*/
|
||||
class Request extends OutgoingRequest implements RequestInterface
|
||||
{
|
||||
use RequestTrait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param App $config
|
||||
*/
|
||||
public function __construct($config = null)
|
||||
{
|
||||
$this->config = $config ?? config(App::class);
|
||||
|
||||
if (empty($this->method)) {
|
||||
$this->method = $this->getServer('REQUEST_METHOD') ?? Method::GET;
|
||||
}
|
||||
|
||||
if (empty($this->uri)) {
|
||||
$this->uri = new URI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request method. Used when spoofing the request.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated 4.0.5 Use withMethod() instead for immutability
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setMethod(string $method)
|
||||
{
|
||||
$this->method = $method;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified method.
|
||||
*
|
||||
* @param string $method
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function withMethod($method)
|
||||
{
|
||||
$request = clone $this;
|
||||
|
||||
$request->method = $method;
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the URI instance.
|
||||
*
|
||||
* @return URI
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
/**
|
||||
* Representation of an incoming, server-side HTTP request.
|
||||
*
|
||||
* Corresponds to Psr7\ServerRequestInterface.
|
||||
*/
|
||||
interface RequestInterface extends OutgoingRequestInterface
|
||||
{
|
||||
/**
|
||||
* Gets the user's IP address.
|
||||
* Supplied by RequestTrait.
|
||||
*
|
||||
* @return string IP address
|
||||
*/
|
||||
public function getIPAddress(): string;
|
||||
|
||||
/**
|
||||
* Fetch an item from the $_SERVER array.
|
||||
* Supplied by RequestTrait.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_SERVER
|
||||
* @param int|null $filter A filter name to be applied
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServer($index = null, $filter = null);
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\Validation\FormatRules;
|
||||
use Config\App;
|
||||
|
||||
/**
|
||||
* Request Trait
|
||||
*
|
||||
* Additional methods to make a PSR-7 Request class
|
||||
* compliant with the framework's own RequestInterface.
|
||||
*
|
||||
* @see https://github.com/php-fig/http-message/blob/master/src/RequestInterface.php
|
||||
*/
|
||||
trait RequestTrait
|
||||
{
|
||||
/**
|
||||
* Configuration settings.
|
||||
*
|
||||
* @var App
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* IP address of the current user.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @deprecated Will become private in a future release
|
||||
*/
|
||||
protected $ipAddress = '';
|
||||
|
||||
/**
|
||||
* Stores values we've retrieved from PHP globals.
|
||||
*
|
||||
* @var array{get?: array, post?: array, request?: array, cookie?: array, server?: array}
|
||||
*/
|
||||
protected $globals = [];
|
||||
|
||||
/**
|
||||
* Gets the user's IP address.
|
||||
*
|
||||
* @return string IP address if it can be detected.
|
||||
* If the IP address is not a valid IP address,
|
||||
* then will return '0.0.0.0'.
|
||||
*/
|
||||
public function getIPAddress(): string
|
||||
{
|
||||
if ($this->ipAddress) {
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
$ipValidator = [
|
||||
new FormatRules(),
|
||||
'valid_ip',
|
||||
];
|
||||
|
||||
$proxyIPs = $this->config->proxyIPs;
|
||||
|
||||
if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) {
|
||||
throw new ConfigException(
|
||||
'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->ipAddress = $this->getServer('REMOTE_ADDR');
|
||||
|
||||
// If this is a CLI request, $this->ipAddress is null.
|
||||
if ($this->ipAddress === null) {
|
||||
return $this->ipAddress = '0.0.0.0';
|
||||
}
|
||||
|
||||
// @TODO Extract all this IP address logic to another class.
|
||||
foreach ($proxyIPs as $proxyIP => $header) {
|
||||
// Check if we have an IP address or a subnet
|
||||
if (! str_contains($proxyIP, '/')) {
|
||||
// An IP address (and not a subnet) is specified.
|
||||
// We can compare right away.
|
||||
if ($proxyIP === $this->ipAddress) {
|
||||
$spoof = $this->getClientIP($header);
|
||||
|
||||
if ($spoof !== null) {
|
||||
$this->ipAddress = $spoof;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have a subnet ... now the heavy lifting begins
|
||||
if (! isset($separator)) {
|
||||
$separator = $ipValidator($this->ipAddress, 'ipv6') ? ':' : '.';
|
||||
}
|
||||
|
||||
// If the proxy entry doesn't match the IP protocol - skip it
|
||||
if (! str_contains($proxyIP, $separator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert the REMOTE_ADDR IP address to binary, if needed
|
||||
if (! isset($ip, $sprintf)) {
|
||||
if ($separator === ':') {
|
||||
// Make sure we're having the "full" IPv6 format
|
||||
$ip = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($this->ipAddress, ':')), $this->ipAddress));
|
||||
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$ip[$j] = intval($ip[$j], 16);
|
||||
}
|
||||
|
||||
$sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b';
|
||||
} else {
|
||||
$ip = explode('.', $this->ipAddress);
|
||||
$sprintf = '%08b%08b%08b%08b';
|
||||
}
|
||||
|
||||
$ip = vsprintf($sprintf, $ip);
|
||||
}
|
||||
|
||||
// Split the netmask length off the network address
|
||||
sscanf($proxyIP, '%[^/]/%d', $netaddr, $masklen);
|
||||
|
||||
// Again, an IPv6 address is most likely in a compressed form
|
||||
if ($separator === ':') {
|
||||
$netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr));
|
||||
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$netaddr[$i] = intval($netaddr[$i], 16);
|
||||
}
|
||||
} else {
|
||||
$netaddr = explode('.', $netaddr);
|
||||
}
|
||||
|
||||
// Convert to binary and finally compare
|
||||
if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) {
|
||||
$spoof = $this->getClientIP($header);
|
||||
|
||||
if ($spoof !== null) {
|
||||
$this->ipAddress = $spoof;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $ipValidator($this->ipAddress)) {
|
||||
return $this->ipAddress = '0.0.0.0';
|
||||
}
|
||||
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client IP address from the HTTP header.
|
||||
*/
|
||||
private function getClientIP(string $header): ?string
|
||||
{
|
||||
$ipValidator = [
|
||||
new FormatRules(),
|
||||
'valid_ip',
|
||||
];
|
||||
$spoof = null;
|
||||
$headerObj = $this->header($header);
|
||||
|
||||
if ($headerObj !== null) {
|
||||
$spoof = $headerObj->getValue();
|
||||
|
||||
// Some proxies typically list the whole chain of IP
|
||||
// addresses through which the client has reached us.
|
||||
// e.g. client_ip, proxy_ip1, proxy_ip2, etc.
|
||||
sscanf($spoof, '%[^,]', $spoof);
|
||||
|
||||
if (! $ipValidator($spoof)) {
|
||||
$spoof = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $spoof;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from the $_SERVER array.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_SERVER
|
||||
* @param int|null $filter A filter name to be applied
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getServer($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
return $this->fetchGlobal('server', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an item from the $_ENV array.
|
||||
*
|
||||
* @param array|string|null $index Index for item to be fetched from $_ENV
|
||||
* @param int|null $filter A filter name to be applied
|
||||
* @param array|int|null $flags
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @deprecated 4.4.4 This method does not work from the beginning. Use `env()`.
|
||||
*/
|
||||
public function getEnv($index = null, $filter = null, $flags = null)
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->fetchGlobal('env', $index, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows manually setting the value of PHP global, like $_GET, $_POST, etc.
|
||||
*
|
||||
* @param string $name Supergrlobal name (lowercase)
|
||||
* @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setGlobal(string $name, $value)
|
||||
{
|
||||
$this->globals[$name] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches one or more items from a global, like cookies, get, post, etc.
|
||||
* Can optionally filter the input when you retrieve it by passing in
|
||||
* a filter.
|
||||
*
|
||||
* If $type is an array, it must conform to the input allowed by the
|
||||
* filter_input_array method.
|
||||
*
|
||||
* http://php.net/manual/en/filter.filters.sanitize.php
|
||||
*
|
||||
* @param string $name Supergrlobal name (lowercase)
|
||||
* @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name
|
||||
* @param array|string|null $index
|
||||
* @param int|null $filter Filter constant
|
||||
* @param array|int|null $flags Options
|
||||
*
|
||||
* @return array|bool|float|int|object|string|null
|
||||
*/
|
||||
public function fetchGlobal(string $name, $index = null, ?int $filter = null, $flags = null)
|
||||
{
|
||||
if (! isset($this->globals[$name])) {
|
||||
$this->populateGlobals($name);
|
||||
}
|
||||
|
||||
// Null filters cause null values to return.
|
||||
$filter ??= FILTER_DEFAULT;
|
||||
$flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);
|
||||
|
||||
// Return all values when $index is null
|
||||
if ($index === null) {
|
||||
$values = [];
|
||||
|
||||
foreach ($this->globals[$name] as $key => $value) {
|
||||
$values[$key] = is_array($value)
|
||||
? $this->fetchGlobal($name, $key, $filter, $flags)
|
||||
: filter_var($value, $filter, $flags);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
// allow fetching multiple keys at once
|
||||
if (is_array($index)) {
|
||||
$output = [];
|
||||
|
||||
foreach ($index as $key) {
|
||||
$output[$key] = $this->fetchGlobal($name, $key, $filter, $flags);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
// Does the index contain array notation?
|
||||
if (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) {
|
||||
$value = $this->globals[$name];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$key = trim($matches[0][$i], '[]');
|
||||
|
||||
if ($key === '') { // Empty notation will return the value as array
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($value[$key])) {
|
||||
$value = $value[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($value)) {
|
||||
$value = $this->globals[$name][$index] ?? null;
|
||||
}
|
||||
|
||||
if (is_array($value)
|
||||
&& (
|
||||
$filter !== FILTER_DEFAULT
|
||||
|| (
|
||||
(is_numeric($flags) && $flags !== 0)
|
||||
|| is_array($flags) && $flags !== []
|
||||
)
|
||||
)
|
||||
) {
|
||||
// Iterate over array and append filter and flags
|
||||
array_walk_recursive($value, static function (&$val) use ($filter, $flags): void {
|
||||
$val = filter_var($val, $filter, $flags);
|
||||
});
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Cannot filter these types of data automatically...
|
||||
if (is_array($value) || is_object($value) || $value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return filter_var($value, $filter, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a copy of the current state of one of several PHP globals,
|
||||
* so we can retrieve them later.
|
||||
*
|
||||
* @param string $name Superglobal name (lowercase)
|
||||
* @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function populateGlobals(string $name)
|
||||
{
|
||||
if (! isset($this->globals[$name])) {
|
||||
$this->globals[$name] = [];
|
||||
}
|
||||
|
||||
// Don't populate ENV as it might contain
|
||||
// sensitive data that we don't want to get logged.
|
||||
switch ($name) {
|
||||
case 'get':
|
||||
$this->globals['get'] = $_GET;
|
||||
break;
|
||||
|
||||
case 'post':
|
||||
$this->globals['post'] = $_POST;
|
||||
break;
|
||||
|
||||
case 'request':
|
||||
$this->globals['request'] = $_REQUEST;
|
||||
break;
|
||||
|
||||
case 'cookie':
|
||||
$this->globals['cookie'] = $_COOKIE;
|
||||
break;
|
||||
|
||||
case 'server':
|
||||
$this->globals['server'] = $_SERVER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
interface ResponsableInterface
|
||||
{
|
||||
public function getResponse(): ResponseInterface;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
use CodeIgniter\Cookie\CookieStore;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use Config\App;
|
||||
use Config\Cookie as CookieConfig;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, server-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
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\ResponseTest
|
||||
*/
|
||||
class Response extends Message implements ResponseInterface
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
/**
|
||||
* 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 => 'Content Too Large', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
|
||||
414 => 'URI Too Long', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
|
||||
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 Content', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
|
||||
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 empty string, will use the default provided for the status code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $reason = '';
|
||||
|
||||
/**
|
||||
* The current status code for this response.
|
||||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $statusCode = 200;
|
||||
|
||||
/**
|
||||
* If true, will not write output. Useful during testing.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @internal Used for framework testing, should not be relied on otherwise
|
||||
*/
|
||||
protected $pretend = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param App $config
|
||||
*
|
||||
* @todo Recommend removing reliance on config injection
|
||||
*
|
||||
* @deprecated 4.5.0 The param $config is no longer used.
|
||||
*/
|
||||
public function __construct($config) // @phpstan-ignore-line
|
||||
{
|
||||
// Default to a non-caching page.
|
||||
// Also ensures that a Cache-control header exists.
|
||||
$this->noCache();
|
||||
|
||||
// We need CSP object even if not enabled to avoid calls to non existing methods
|
||||
$this->CSP = Services::csp();
|
||||
|
||||
$this->cookieStore = new CookieStore([]);
|
||||
|
||||
$cookie = config(CookieConfig::class);
|
||||
|
||||
Cookie::setDefaults($cookie);
|
||||
|
||||
// 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.
|
||||
*
|
||||
* Note that this is not a part of the interface so
|
||||
* should not be relied on outside of internal testing.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @internal For testing purposes only.
|
||||
* @testTag only available to test code
|
||||
*/
|
||||
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 server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return int Status code.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
if (empty($this->statusCode)) {
|
||||
throw HTTPException::forMissingResponseStatus();
|
||||
}
|
||||
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @deprecated Use getReasonPhrase()
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getReason(): string
|
||||
{
|
||||
return $this->getReasonPhrase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @see 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()
|
||||
{
|
||||
if ($this->reason === '') {
|
||||
return ! empty($this->statusCode) ? static::$statusCodes[$this->statusCode] : '';
|
||||
}
|
||||
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
use CodeIgniter\Cookie\CookieStore;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\Pager\PagerInterface;
|
||||
use DateTime;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Representation of an outgoing, server-side response.
|
||||
* Most of these methods are supplied by ResponseTrait.
|
||||
*
|
||||
* Per the HTTP specification, this interface includes properties for
|
||||
* each of the following:
|
||||
*
|
||||
* - Protocol version
|
||||
* - Status code and reason phrase
|
||||
* - Headers
|
||||
* - Message body
|
||||
*/
|
||||
interface ResponseInterface extends MessageInterface
|
||||
{
|
||||
/**
|
||||
* Constants for status codes.
|
||||
* From https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
||||
*/
|
||||
// Informational
|
||||
public const HTTP_CONTINUE = 100;
|
||||
public const HTTP_SWITCHING_PROTOCOLS = 101;
|
||||
public const HTTP_PROCESSING = 102;
|
||||
public const HTTP_EARLY_HINTS = 103;
|
||||
public const HTTP_OK = 200;
|
||||
public const HTTP_CREATED = 201;
|
||||
public const HTTP_ACCEPTED = 202;
|
||||
public const HTTP_NONAUTHORITATIVE_INFORMATION = 203;
|
||||
public const HTTP_NO_CONTENT = 204;
|
||||
public const HTTP_RESET_CONTENT = 205;
|
||||
public const HTTP_PARTIAL_CONTENT = 206;
|
||||
public const HTTP_MULTI_STATUS = 207;
|
||||
public const HTTP_ALREADY_REPORTED = 208;
|
||||
public const HTTP_IM_USED = 226;
|
||||
public const HTTP_MULTIPLE_CHOICES = 300;
|
||||
public const HTTP_MOVED_PERMANENTLY = 301;
|
||||
public const HTTP_FOUND = 302;
|
||||
public const HTTP_SEE_OTHER = 303;
|
||||
public const HTTP_NOT_MODIFIED = 304;
|
||||
public const HTTP_USE_PROXY = 305;
|
||||
public const HTTP_SWITCH_PROXY = 306;
|
||||
public const HTTP_TEMPORARY_REDIRECT = 307;
|
||||
public const HTTP_PERMANENT_REDIRECT = 308;
|
||||
public const HTTP_BAD_REQUEST = 400;
|
||||
public const HTTP_UNAUTHORIZED = 401;
|
||||
public const HTTP_PAYMENT_REQUIRED = 402;
|
||||
public const HTTP_FORBIDDEN = 403;
|
||||
public const HTTP_NOT_FOUND = 404;
|
||||
public const HTTP_METHOD_NOT_ALLOWED = 405;
|
||||
public const HTTP_NOT_ACCEPTABLE = 406;
|
||||
public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
|
||||
public const HTTP_REQUEST_TIMEOUT = 408;
|
||||
public const HTTP_CONFLICT = 409;
|
||||
public const HTTP_GONE = 410;
|
||||
public const HTTP_LENGTH_REQUIRED = 411;
|
||||
public const HTTP_PRECONDITION_FAILED = 412;
|
||||
public const HTTP_PAYLOAD_TOO_LARGE = 413;
|
||||
public const HTTP_URI_TOO_LONG = 414;
|
||||
public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
|
||||
public const HTTP_RANGE_NOT_SATISFIABLE = 416;
|
||||
public const HTTP_EXPECTATION_FAILED = 417;
|
||||
public const HTTP_IM_A_TEAPOT = 418;
|
||||
public const HTTP_MISDIRECTED_REQUEST = 421;
|
||||
public const HTTP_UNPROCESSABLE_ENTITY = 422;
|
||||
public const HTTP_LOCKED = 423;
|
||||
public const HTTP_FAILED_DEPENDENCY = 424;
|
||||
public const HTTP_TOO_EARLY = 425;
|
||||
public const HTTP_UPGRADE_REQUIRED = 426;
|
||||
public const HTTP_PRECONDITION_REQUIRED = 428;
|
||||
public const HTTP_TOO_MANY_REQUESTS = 429;
|
||||
public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
|
||||
public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
|
||||
public const HTTP_CLIENT_CLOSED_REQUEST = 499;
|
||||
public const HTTP_INTERNAL_SERVER_ERROR = 500;
|
||||
public const HTTP_NOT_IMPLEMENTED = 501;
|
||||
public const HTTP_BAD_GATEWAY = 502;
|
||||
public const HTTP_SERVICE_UNAVAILABLE = 503;
|
||||
public const HTTP_GATEWAY_TIMEOUT = 504;
|
||||
public const HTTP_HTTP_VERSION_NOT_SUPPORTED = 505;
|
||||
public const HTTP_VARIANT_ALSO_NEGOTIATES = 506;
|
||||
public const HTTP_INSUFFICIENT_STORAGE = 507;
|
||||
public const HTTP_LOOP_DETECTED = 508;
|
||||
public const HTTP_NOT_EXTENDED = 510;
|
||||
public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
|
||||
public const HTTP_NETWORK_CONNECT_TIMEOUT_ERROR = 599;
|
||||
|
||||
/**
|
||||
* 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(): int;
|
||||
|
||||
/**
|
||||
* 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 int $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 HTTPException For invalid status code arguments.
|
||||
*/
|
||||
public function setStatusCode(int $code, string $reason = '');
|
||||
|
||||
/**
|
||||
* Gets the 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
|
||||
*
|
||||
* @deprecated Use getReasonPhrase()
|
||||
*/
|
||||
public function getReason(): string;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc7231#section-6
|
||||
* @see 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();
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Convenience Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the date header
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDate(DateTime $date);
|
||||
|
||||
/**
|
||||
* 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 $this
|
||||
*/
|
||||
public function setLastModified($date);
|
||||
|
||||
/**
|
||||
* Set the Link Header
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc5988
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @todo Recommend moving to Pager
|
||||
*/
|
||||
public function setLink(PagerInterface $pager);
|
||||
|
||||
/**
|
||||
* Sets the Content Type header for this response with the mime type
|
||||
* and, optionally, the charset.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setContentType(string $mime, string $charset = 'UTF-8');
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Formatter Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Converts the $body into JSON and sets the Content Type header.
|
||||
*
|
||||
* @param array|string $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setJSON($body, bool $unencoded = false);
|
||||
|
||||
/**
|
||||
* Returns the current body, converted to JSON is it isn't already.
|
||||
*
|
||||
* @return bool|string|null
|
||||
*
|
||||
* @throws InvalidArgumentException If the body property is not array.
|
||||
*/
|
||||
public function getJSON();
|
||||
|
||||
/**
|
||||
* Converts $body into XML, and sets the correct Content-Type.
|
||||
*
|
||||
* @param array|string $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setXML($body);
|
||||
|
||||
/**
|
||||
* Retrieves the current body into XML and returns it.
|
||||
*
|
||||
* @return bool|string|null
|
||||
*
|
||||
* @throws InvalidArgumentException If the body property is not array.
|
||||
*/
|
||||
public function getXML();
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 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 $this
|
||||
*/
|
||||
public function noCache();
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache(array $options = []);
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Output Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sends the output to the browser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function send();
|
||||
|
||||
/**
|
||||
* Sends the headers of this HTTP request to the browser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendHeaders();
|
||||
|
||||
/**
|
||||
* Sends the Body of the message to the browser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendBody();
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Cookie Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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 array|Cookie|string $name Cookie name / array containing binds / Cookie object
|
||||
* @param string $value Cookie value
|
||||
* @param int $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 bool $secure Whether to only transfer cookies via SSL
|
||||
* @param bool $httponly Whether only make the cookie accessible via HTTP (no javascript)
|
||||
* @param string|null $samesite
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCookie(
|
||||
$name,
|
||||
$value = '',
|
||||
$expire = 0,
|
||||
$domain = '',
|
||||
$path = '/',
|
||||
$prefix = '',
|
||||
$secure = false,
|
||||
$httponly = false,
|
||||
$samesite = null
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks to see if the Response has a specified cookie or not.
|
||||
*/
|
||||
public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool;
|
||||
|
||||
/**
|
||||
* Returns the cookie
|
||||
*
|
||||
* @return array<string, Cookie>|Cookie|null
|
||||
*/
|
||||
public function getCookie(?string $name = null, string $prefix = '');
|
||||
|
||||
/**
|
||||
* Sets a cookie to be deleted when the response is sent.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '');
|
||||
|
||||
/**
|
||||
* Returns all cookies currently set.
|
||||
*
|
||||
* @return array<string, Cookie>
|
||||
*/
|
||||
public function getCookies();
|
||||
|
||||
/**
|
||||
* Returns the `CookieStore` instance.
|
||||
*
|
||||
* @return CookieStore
|
||||
*/
|
||||
public function getCookieStore();
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Response Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Perform a redirect to a new URL, in two flavors: header or location.
|
||||
*
|
||||
* @param string $uri The URI to redirect to
|
||||
* @param int $code The type of redirection, defaults to 302
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws HTTPException For invalid status code.
|
||||
*/
|
||||
public function redirect(string $uri, string $method = 'auto', ?int $code = null);
|
||||
|
||||
/**
|
||||
* 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 bool $setMime Whether to try and send the actual MIME type
|
||||
*
|
||||
* @return DownloadResponse|null
|
||||
*/
|
||||
public function download(string $filename = '', $data = '', bool $setMime = false);
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// CSP Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get Content Security Policy handler.
|
||||
*/
|
||||
public function getCSP(): ContentSecurityPolicy;
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\Cookie\Cookie;
|
||||
use CodeIgniter\Cookie\CookieStore;
|
||||
use CodeIgniter\Cookie\Exceptions\CookieException;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use CodeIgniter\Pager\PagerInterface;
|
||||
use CodeIgniter\Security\Exceptions\SecurityException;
|
||||
use Config\Cookie as CookieConfig;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Response Trait
|
||||
*
|
||||
* Additional methods to make a PSR-7 Response class
|
||||
* compliant with the framework's own ResponseInterface.
|
||||
*
|
||||
* @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php
|
||||
*/
|
||||
trait ResponseTrait
|
||||
{
|
||||
/**
|
||||
* Content security policy handler
|
||||
*
|
||||
* @var ContentSecurityPolicy
|
||||
*/
|
||||
protected $CSP;
|
||||
|
||||
/**
|
||||
* CookieStore instance.
|
||||
*
|
||||
* @var CookieStore
|
||||
*/
|
||||
protected $cookieStore;
|
||||
|
||||
/**
|
||||
* Type of format the body is in.
|
||||
* Valid: html, json, xml
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bodyFormat = 'html';
|
||||
|
||||
/**
|
||||
* 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 int $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 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) && ($reason === '')) {
|
||||
throw HTTPException::forUnkownStatusCode($code);
|
||||
}
|
||||
|
||||
$this->statusCode = $code;
|
||||
|
||||
$this->reason = ($reason !== '') ? $reason : static::$statusCodes[$code];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Convenience Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the date header
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc5988
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @todo Recommend moving to Pager
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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) && ($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|object|string $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setJSON($body, bool $unencoded = false)
|
||||
{
|
||||
$this->body = $this->formatBody($body, 'json' . ($unencoded ? '-unencoded' : ''));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current body, converted to JSON is it isn't already.
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @throws InvalidArgumentException If the body property is not array.
|
||||
*/
|
||||
public function getJSON()
|
||||
{
|
||||
$body = $this->body;
|
||||
|
||||
if ($this->bodyFormat !== 'json') {
|
||||
$body = service('format')->getFormatter('application/json')->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 bool|string|null
|
||||
*
|
||||
* @throws InvalidArgumentException If the body property is not array.
|
||||
*/
|
||||
public function getXML()
|
||||
{
|
||||
$body = $this->body;
|
||||
|
||||
if ($this->bodyFormat !== 'xml') {
|
||||
$body = service('format')->getFormatter('application/xml')->format($body);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles conversion of the data into the appropriate format,
|
||||
* and sets the correct Content-Type header for our response.
|
||||
*
|
||||
* @param array|object|string $body
|
||||
* @param string $format Valid: json, xml
|
||||
*
|
||||
* @return false|string
|
||||
*
|
||||
* @throws InvalidArgumentException If the body property is not string or array.
|
||||
*/
|
||||
protected function formatBody($body, string $format)
|
||||
{
|
||||
$this->bodyFormat = ($format === 'json-unencoded' ? 'json' : $format);
|
||||
$mime = "application/{$this->bodyFormat}";
|
||||
$this->setContentType($mime);
|
||||
|
||||
// Nothing much to do for a string...
|
||||
if (! is_string($body) || $format === 'json-unencoded') {
|
||||
$body = service('format')->getFormatter($mime)->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 $this
|
||||
*
|
||||
* @todo Recommend researching these directives, might need: 'private', 'no-transform', 'no-store', 'must-revalidate'
|
||||
*
|
||||
* @see DownloadResponse::noCache()
|
||||
*/
|
||||
public function noCache()
|
||||
{
|
||||
$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
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache(array $options = [])
|
||||
{
|
||||
if ($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 $this
|
||||
*/
|
||||
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 $this
|
||||
*/
|
||||
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->CSP->enabled()) {
|
||||
$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 response to the browser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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']) && PHP_SAPI !== 'cli-server') {
|
||||
$this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp()));
|
||||
}
|
||||
|
||||
// HTTP Status
|
||||
header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode());
|
||||
|
||||
// Send all of our headers
|
||||
foreach ($this->headers() as $name => $value) {
|
||||
if ($value instanceof Header) {
|
||||
header(
|
||||
$name . ': ' . $value->getValueLine(),
|
||||
false,
|
||||
$this->getStatusCode()
|
||||
);
|
||||
} else {
|
||||
foreach ($value as $header) {
|
||||
header(
|
||||
$name . ': ' . $header->getValueLine(),
|
||||
false,
|
||||
$this->getStatusCode()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the Body of the message to the browser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendBody()
|
||||
{
|
||||
echo $this->body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a redirect to a new URL, in two flavors: header or location.
|
||||
*
|
||||
* @param string $uri The URI to redirect to
|
||||
* @param int|null $code The type of redirection, defaults to 302
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws HTTPException For invalid status code.
|
||||
*/
|
||||
public function redirect(string $uri, string $method = 'auto', ?int $code = null)
|
||||
{
|
||||
// IIS environment likely? Use 'refresh' for better compatibility
|
||||
if (
|
||||
$method === 'auto'
|
||||
&& isset($_SERVER['SERVER_SOFTWARE'])
|
||||
&& str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')
|
||||
) {
|
||||
$method = 'refresh';
|
||||
} elseif ($method !== 'refresh' && $code === null) {
|
||||
// override status code for HTTP/1.1 & higher
|
||||
if (
|
||||
isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD'])
|
||||
&& $this->getProtocolVersion() >= 1.1
|
||||
) {
|
||||
if ($_SERVER['REQUEST_METHOD'] === Method::GET) {
|
||||
$code = 302;
|
||||
} elseif (in_array($_SERVER['REQUEST_METHOD'], [Method::POST, Method::PUT, Method::DELETE], true)) {
|
||||
// reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
|
||||
$code = 303;
|
||||
} else {
|
||||
$code = 307;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($code === null) {
|
||||
$code = 302;
|
||||
}
|
||||
|
||||
match ($method) {
|
||||
'refresh' => $this->setHeader('Refresh', '0;url=' . $uri),
|
||||
default => $this->setHeader('Location', $uri),
|
||||
};
|
||||
|
||||
$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 array|Cookie|string $name Cookie name / array containing binds / Cookie object
|
||||
* @param string $value Cookie value
|
||||
* @param int $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 ('': the default prefix)
|
||||
* @param bool|null $secure Whether to only transfer cookies via SSL
|
||||
* @param bool|null $httponly Whether only make the cookie accessible via HTTP (no javascript)
|
||||
* @param string|null $samesite
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCookie(
|
||||
$name,
|
||||
$value = '',
|
||||
$expire = 0,
|
||||
$domain = '',
|
||||
$path = '/',
|
||||
$prefix = '',
|
||||
$secure = null,
|
||||
$httponly = null,
|
||||
$samesite = null
|
||||
) {
|
||||
if ($name instanceof Cookie) {
|
||||
$this->cookieStore = $this->cookieStore->put($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$cookieConfig = config(CookieConfig::class);
|
||||
|
||||
$secure ??= $cookieConfig->secure;
|
||||
$httponly ??= $cookieConfig->httponly;
|
||||
$samesite ??= $cookieConfig->samesite;
|
||||
|
||||
if (is_array($name)) {
|
||||
// always leave 'name' in last place, as the loop will break otherwise, due to ${$item}
|
||||
foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) {
|
||||
if (isset($name[$item])) {
|
||||
${$item} = $name[$item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_numeric($expire)) {
|
||||
$expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0;
|
||||
}
|
||||
|
||||
$cookie = new Cookie($name, $value, [
|
||||
'expires' => $expire ?: 0,
|
||||
'domain' => $domain,
|
||||
'path' => $path,
|
||||
'prefix' => $prefix,
|
||||
'secure' => $secure,
|
||||
'httponly' => $httponly,
|
||||
'samesite' => $samesite ?? '',
|
||||
]);
|
||||
|
||||
$this->cookieStore = $this->cookieStore->put($cookie);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `CookieStore` instance.
|
||||
*
|
||||
* @return CookieStore
|
||||
*/
|
||||
public function getCookieStore()
|
||||
{
|
||||
return $this->cookieStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the Response has a specified cookie or not.
|
||||
*/
|
||||
public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool
|
||||
{
|
||||
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
|
||||
|
||||
return $this->cookieStore->has($name, $prefix, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cookie
|
||||
*
|
||||
* @param string $prefix Cookie prefix.
|
||||
* '': the default prefix
|
||||
*
|
||||
* @return array<string, Cookie>|Cookie|null
|
||||
*/
|
||||
public function getCookie(?string $name = null, string $prefix = '')
|
||||
{
|
||||
if ((string) $name === '') {
|
||||
return $this->cookieStore->display();
|
||||
}
|
||||
|
||||
try {
|
||||
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
|
||||
|
||||
return $this->cookieStore->get($name, $prefix);
|
||||
} catch (CookieException $e) {
|
||||
log_message('error', (string) $e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cookie to be deleted when the response is sent.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '')
|
||||
{
|
||||
if ($name === '') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
|
||||
|
||||
$prefixed = $prefix . $name;
|
||||
$store = $this->cookieStore;
|
||||
$found = false;
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
foreach ($store as $cookie) {
|
||||
if ($cookie->getPrefixedName() === $prefixed) {
|
||||
if ($domain !== $cookie->getDomain()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($path !== $cookie->getPath()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cookie = $cookie->withValue('')->withExpired();
|
||||
$found = true;
|
||||
|
||||
$this->cookieStore = $store->put($cookie);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $found) {
|
||||
$this->setCookie($name, '', 0, $domain, $path, $prefix);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cookies currently set.
|
||||
*
|
||||
* @return array<string, Cookie>
|
||||
*/
|
||||
public function getCookies()
|
||||
{
|
||||
return $this->cookieStore->display();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually sets the cookies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function sendCookies()
|
||||
{
|
||||
if ($this->pretend) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatchCookies();
|
||||
}
|
||||
|
||||
private function dispatchCookies(): void
|
||||
{
|
||||
/** @var IncomingRequest $request */
|
||||
$request = service('request');
|
||||
|
||||
foreach ($this->cookieStore->display() as $cookie) {
|
||||
if ($cookie->isSecure() && ! $request->isSecure()) {
|
||||
throw SecurityException::forInsecureCookie();
|
||||
}
|
||||
|
||||
$name = $cookie->getPrefixedName();
|
||||
$value = $cookie->getValue();
|
||||
$options = $cookie->getOptions();
|
||||
|
||||
if ($cookie->isRaw()) {
|
||||
$this->doSetRawCookie($name, $value, $options);
|
||||
} else {
|
||||
$this->doSetCookie($name, $value, $options);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cookieStore->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted call to `setrawcookie()` in order to run unit tests on it.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function doSetRawCookie(string $name, string $value, array $options): void
|
||||
{
|
||||
setrawcookie($name, $value, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted call to `setcookie()` in order to run unit tests on it.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function doSetCookie(string $name, string $value, array $options): void
|
||||
{
|
||||
setcookie($name, $value, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a download.
|
||||
*
|
||||
* Generates the headers that force a download to happen. And
|
||||
* sends the file to the browser.
|
||||
*
|
||||
* @param string $filename The name you want the downloaded file to be named
|
||||
* or the path to the file to send
|
||||
* @param string|null $data The data to be downloaded. Set null if the $filename is the file path
|
||||
* @param bool $setMime Whether to try and send the actual MIME type
|
||||
*
|
||||
* @return 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;
|
||||
}
|
||||
|
||||
public function getCSP(): ContentSecurityPolicy
|
||||
{
|
||||
return $this->CSP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use Config\App;
|
||||
|
||||
/**
|
||||
* URI for the application site
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\SiteURITest
|
||||
*/
|
||||
class SiteURI extends URI
|
||||
{
|
||||
/**
|
||||
* The current baseURL.
|
||||
*/
|
||||
private readonly URI $baseURL;
|
||||
|
||||
/**
|
||||
* The path part of baseURL.
|
||||
*
|
||||
* The baseURL "http://example.com/" → '/'
|
||||
* The baseURL "http://localhost:8888/ci431/public/" → '/ci431/public/'
|
||||
*/
|
||||
private string $basePathWithoutIndexPage;
|
||||
|
||||
/**
|
||||
* The Index File.
|
||||
*/
|
||||
private string $indexPage;
|
||||
|
||||
/**
|
||||
* List of URI segments in baseURL and indexPage.
|
||||
*
|
||||
* If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
|
||||
* and the baseURL is "http://localhost:8888/ci431/public/", then:
|
||||
* $baseSegments = [
|
||||
* 0 => 'ci431',
|
||||
* 1 => 'public',
|
||||
* 2 => 'index.php',
|
||||
* ];
|
||||
*/
|
||||
private array $baseSegments;
|
||||
|
||||
/**
|
||||
* List of URI segments after indexPage.
|
||||
*
|
||||
* The word "URI Segments" originally means only the URI path part relative
|
||||
* to the baseURL.
|
||||
*
|
||||
* If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
|
||||
* and the baseURL is "http://localhost:8888/ci431/public/", then:
|
||||
* $segments = [
|
||||
* 0 => 'test',
|
||||
* ];
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @deprecated This property will be private.
|
||||
*/
|
||||
protected $segments;
|
||||
|
||||
/**
|
||||
* URI path relative to baseURL.
|
||||
*
|
||||
* If the baseURL contains sub folders, this value will be different from
|
||||
* the current URI path.
|
||||
*
|
||||
* This value never starts with '/'.
|
||||
*/
|
||||
private string $routePath;
|
||||
|
||||
/**
|
||||
* @param string $relativePath URI path relative to baseURL. May include
|
||||
* queries or fragments.
|
||||
* @param string|null $host Optional current hostname.
|
||||
* @param string|null $scheme Optional scheme. 'http' or 'https'.
|
||||
* @phpstan-param 'http'|'https'|null $scheme
|
||||
*/
|
||||
public function __construct(
|
||||
App $configApp,
|
||||
string $relativePath = '',
|
||||
?string $host = null,
|
||||
?string $scheme = null
|
||||
) {
|
||||
$this->indexPage = $configApp->indexPage;
|
||||
|
||||
$this->baseURL = $this->determineBaseURL($configApp, $host, $scheme);
|
||||
|
||||
$this->setBasePath();
|
||||
|
||||
// Fix routePath, query, fragment
|
||||
[$routePath, $query, $fragment] = $this->parseRelativePath($relativePath);
|
||||
|
||||
// Fix indexPage and routePath
|
||||
$indexPageRoutePath = $this->getIndexPageRoutePath($routePath);
|
||||
|
||||
// Fix the current URI
|
||||
$uri = $this->baseURL . $indexPageRoutePath;
|
||||
|
||||
// applyParts
|
||||
$parts = parse_url($uri);
|
||||
if ($parts === false) {
|
||||
throw HTTPException::forUnableToParseURI($uri);
|
||||
}
|
||||
$parts['query'] = $query;
|
||||
$parts['fragment'] = $fragment;
|
||||
$this->applyParts($parts);
|
||||
|
||||
$this->setRoutePath($routePath);
|
||||
}
|
||||
|
||||
private function parseRelativePath(string $relativePath): array
|
||||
{
|
||||
$parts = parse_url('http://dummy/' . $relativePath);
|
||||
if ($parts === false) {
|
||||
throw HTTPException::forUnableToParseURI($relativePath);
|
||||
}
|
||||
|
||||
$routePath = $relativePath === '/' ? '/' : ltrim($parts['path'], '/');
|
||||
|
||||
$query = $parts['query'] ?? '';
|
||||
$fragment = $parts['fragment'] ?? '';
|
||||
|
||||
return [$routePath, $query, $fragment];
|
||||
}
|
||||
|
||||
private function determineBaseURL(
|
||||
App $configApp,
|
||||
?string $host,
|
||||
?string $scheme
|
||||
): URI {
|
||||
$baseURL = $this->normalizeBaseURL($configApp);
|
||||
|
||||
$uri = new URI($baseURL);
|
||||
|
||||
// Update scheme
|
||||
if ($scheme !== null && $scheme !== '') {
|
||||
$uri->setScheme($scheme);
|
||||
} elseif ($configApp->forceGlobalSecureRequests) {
|
||||
$uri->setScheme('https');
|
||||
}
|
||||
|
||||
// Update host
|
||||
if ($host !== null) {
|
||||
$uri->setHost($host);
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function getIndexPageRoutePath(string $routePath): string
|
||||
{
|
||||
// Remove starting slash unless it is `/`.
|
||||
if ($routePath !== '' && $routePath[0] === '/' && $routePath !== '/') {
|
||||
$routePath = ltrim($routePath, '/');
|
||||
}
|
||||
|
||||
// Check for an index page
|
||||
$indexPage = '';
|
||||
if ($this->indexPage !== '') {
|
||||
$indexPage = $this->indexPage;
|
||||
|
||||
// Check if we need a separator
|
||||
if ($routePath !== '' && $routePath[0] !== '/' && $routePath[0] !== '?') {
|
||||
$indexPage .= '/';
|
||||
}
|
||||
}
|
||||
|
||||
$indexPageRoutePath = $indexPage . $routePath;
|
||||
|
||||
if ($indexPageRoutePath === '/') {
|
||||
$indexPageRoutePath = '';
|
||||
}
|
||||
|
||||
return $indexPageRoutePath;
|
||||
}
|
||||
|
||||
private function normalizeBaseURL(App $configApp): string
|
||||
{
|
||||
// It's possible the user forgot a trailing slash on their
|
||||
// baseURL, so let's help them out.
|
||||
$baseURL = rtrim($configApp->baseURL, '/ ') . '/';
|
||||
|
||||
// Validate baseURL
|
||||
if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) {
|
||||
throw new ConfigException(
|
||||
'Config\App::$baseURL "' . $baseURL . '" is not a valid URL.'
|
||||
);
|
||||
}
|
||||
|
||||
return $baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets basePathWithoutIndexPage and baseSegments.
|
||||
*/
|
||||
private function setBasePath(): void
|
||||
{
|
||||
$this->basePathWithoutIndexPage = $this->baseURL->getPath();
|
||||
|
||||
$this->baseSegments = $this->convertToSegments($this->basePathWithoutIndexPage);
|
||||
|
||||
if ($this->indexPage !== '') {
|
||||
$this->baseSegments[] = $this->indexPage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function setBaseURL(string $baseURL): void
|
||||
{
|
||||
throw new BadMethodCallException('Cannot use this method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function setURI(?string $uri = null)
|
||||
{
|
||||
throw new BadMethodCallException('Cannot use this method.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the baseURL.
|
||||
*
|
||||
* @interal
|
||||
*/
|
||||
public function getBaseURL(): string
|
||||
{
|
||||
return (string) $this->baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI path relative to baseURL.
|
||||
*
|
||||
* @return string The Route path.
|
||||
*/
|
||||
public function getRoutePath(): string
|
||||
{
|
||||
return $this->routePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the URI as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return static::createURIString(
|
||||
$this->getScheme(),
|
||||
$this->getAuthority(),
|
||||
$this->getPath(),
|
||||
$this->getQuery(),
|
||||
$this->getFragment()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the route path (and segments).
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPath(string $path)
|
||||
{
|
||||
$this->setRoutePath($path);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the route path (and segments).
|
||||
*/
|
||||
private function setRoutePath(string $routePath): void
|
||||
{
|
||||
$routePath = $this->filterPath($routePath);
|
||||
|
||||
$indexPageRoutePath = $this->getIndexPageRoutePath($routePath);
|
||||
|
||||
$this->path = $this->basePathWithoutIndexPage . $indexPageRoutePath;
|
||||
|
||||
$this->routePath = ltrim($routePath, '/');
|
||||
|
||||
$this->segments = $this->convertToSegments($this->routePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts path to segments
|
||||
*/
|
||||
private function convertToSegments(string $path): array
|
||||
{
|
||||
$tempPath = trim($path, '/');
|
||||
|
||||
return ($tempPath === '') ? [] : explode('/', $tempPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the path portion of the URI based on segments.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated This method will be private.
|
||||
*/
|
||||
public function refreshPath()
|
||||
{
|
||||
$allSegments = array_merge($this->baseSegments, $this->segments);
|
||||
$this->path = '/' . $this->filterPath(implode('/', $allSegments));
|
||||
|
||||
if ($this->routePath === '/' && $this->path !== '/') {
|
||||
$this->path .= '/';
|
||||
}
|
||||
|
||||
$this->routePath = $this->filterPath(implode('/', $this->segments));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves our parts from a parse_url() call.
|
||||
*/
|
||||
protected function applyParts(array $parts): void
|
||||
{
|
||||
if (! empty($parts['host'])) {
|
||||
$this->host = $parts['host'];
|
||||
}
|
||||
if (! empty($parts['user'])) {
|
||||
$this->user = $parts['user'];
|
||||
}
|
||||
if (isset($parts['path']) && $parts['path'] !== '') {
|
||||
$this->path = $this->filterPath($parts['path']);
|
||||
}
|
||||
if (! empty($parts['query'])) {
|
||||
$this->setQuery($parts['query']);
|
||||
}
|
||||
if (! empty($parts['fragment'])) {
|
||||
$this->fragment = $parts['fragment'];
|
||||
}
|
||||
|
||||
// Scheme
|
||||
if (isset($parts['scheme'])) {
|
||||
$this->setScheme(rtrim($parts['scheme'], ':/'));
|
||||
} else {
|
||||
$this->setScheme('http');
|
||||
}
|
||||
|
||||
// Port
|
||||
if (isset($parts['port']) && $parts['port'] !== null) {
|
||||
// Valid port numbers are enforced by earlier parse_url() or setPort()
|
||||
$this->port = $parts['port'];
|
||||
}
|
||||
|
||||
if (isset($parts['pass'])) {
|
||||
$this->password = $parts['pass'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For base_url() helper.
|
||||
*
|
||||
* @param array|string $relativePath URI string or array of URI segments.
|
||||
* @param string|null $scheme URI scheme. E.g., http, ftp. If empty
|
||||
* string '' is set, a protocol-relative
|
||||
* link is returned.
|
||||
*/
|
||||
public function baseUrl($relativePath = '', ?string $scheme = null): string
|
||||
{
|
||||
$relativePath = $this->stringifyRelativePath($relativePath);
|
||||
|
||||
$config = clone config(App::class);
|
||||
$config->indexPage = '';
|
||||
|
||||
$host = $this->getHost();
|
||||
|
||||
$uri = new self($config, $relativePath, $host, $scheme);
|
||||
|
||||
// Support protocol-relative links
|
||||
if ($scheme === '') {
|
||||
return substr((string) $uri, strlen($uri->getScheme()) + 1);
|
||||
}
|
||||
|
||||
return (string) $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $relativePath URI string or array of URI segments
|
||||
*/
|
||||
private function stringifyRelativePath($relativePath): string
|
||||
{
|
||||
if (is_array($relativePath)) {
|
||||
$relativePath = implode('/', $relativePath);
|
||||
}
|
||||
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* For site_url() helper.
|
||||
*
|
||||
* @param array|string $relativePath URI string or array of URI segments.
|
||||
* @param string|null $scheme URI scheme. E.g., http, ftp. If empty
|
||||
* string '' is set, a protocol-relative
|
||||
* link is returned.
|
||||
* @param App|null $config Alternate configuration to use.
|
||||
*/
|
||||
public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config = null): string
|
||||
{
|
||||
$relativePath = $this->stringifyRelativePath($relativePath);
|
||||
|
||||
// Check current host.
|
||||
$host = $config === null ? $this->getHost() : null;
|
||||
|
||||
$config ??= config(App::class);
|
||||
|
||||
$uri = new self($config, $relativePath, $host, $scheme);
|
||||
|
||||
// Support protocol-relative links
|
||||
if ($scheme === '') {
|
||||
return substr((string) $uri, strlen($uri->getScheme()) + 1);
|
||||
}
|
||||
|
||||
return (string) $uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
||||
use CodeIgniter\Superglobals;
|
||||
use Config\App;
|
||||
|
||||
/**
|
||||
* Creates SiteURI using superglobals.
|
||||
*
|
||||
* This class also updates superglobal $_SERVER and $_GET.
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\SiteURIFactoryTest
|
||||
*/
|
||||
final class SiteURIFactory
|
||||
{
|
||||
public function __construct(private readonly App $appConfig, private readonly Superglobals $superglobals)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the current URI object from superglobals.
|
||||
*
|
||||
* This method updates superglobal $_SERVER and $_GET.
|
||||
*/
|
||||
public function createFromGlobals(): SiteURI
|
||||
{
|
||||
$routePath = $this->detectRoutePath();
|
||||
|
||||
return $this->createURIFromRoutePath($routePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the SiteURI object from URI string.
|
||||
*
|
||||
* @internal Used for testing purposes only.
|
||||
* @testTag
|
||||
*/
|
||||
public function createFromString(string $uri): SiteURI
|
||||
{
|
||||
// Validate URI
|
||||
if (filter_var($uri, FILTER_VALIDATE_URL) === false) {
|
||||
throw HTTPException::forUnableToParseURI($uri);
|
||||
}
|
||||
|
||||
$parts = parse_url($uri);
|
||||
|
||||
if ($parts === false) {
|
||||
throw HTTPException::forUnableToParseURI($uri);
|
||||
}
|
||||
|
||||
$query = $fragment = '';
|
||||
if (isset($parts['query'])) {
|
||||
$query = '?' . $parts['query'];
|
||||
}
|
||||
if (isset($parts['fragment'])) {
|
||||
$fragment = '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
$relativePath = ($parts['path'] ?? '') . $query . $fragment;
|
||||
$host = $this->getValidHost($parts['host']);
|
||||
|
||||
return new SiteURI($this->appConfig, $relativePath, $host, $parts['scheme']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the current URI path relative to baseURL based on the URIProtocol
|
||||
* Config setting.
|
||||
*
|
||||
* @param string $protocol URIProtocol
|
||||
*
|
||||
* @return string The route path
|
||||
*
|
||||
* @internal Used for testing purposes only.
|
||||
* @testTag
|
||||
*/
|
||||
public function detectRoutePath(string $protocol = ''): string
|
||||
{
|
||||
if ($protocol === '') {
|
||||
$protocol = $this->appConfig->uriProtocol;
|
||||
}
|
||||
|
||||
$routePath = match ($protocol) {
|
||||
'REQUEST_URI' => $this->parseRequestURI(),
|
||||
'QUERY_STRING' => $this->parseQueryString(),
|
||||
default => $this->superglobals->server($protocol) ?? $this->parseRequestURI(),
|
||||
};
|
||||
|
||||
return ($routePath === '/' || $routePath === '') ? '/' : ltrim($routePath, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Will parse the REQUEST_URI and automatically detect the URI from it,
|
||||
* fixing the query string if necessary.
|
||||
*
|
||||
* This method updates superglobal $_SERVER and $_GET.
|
||||
*
|
||||
* @return string The route path (before normalization).
|
||||
*/
|
||||
private function parseRequestURI(): string
|
||||
{
|
||||
if (
|
||||
$this->superglobals->server('REQUEST_URI') === null
|
||||
|| $this->superglobals->server('SCRIPT_NAME') === null
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// parse_url() returns false if no host is present, but the path or query
|
||||
// string contains a colon followed by a number. So we attach a dummy
|
||||
// host since REQUEST_URI does not include the host. This allows us to
|
||||
// parse out the query string and path.
|
||||
$parts = parse_url('http://dummy' . $this->superglobals->server('REQUEST_URI'));
|
||||
$query = $parts['query'] ?? '';
|
||||
$path = $parts['path'] ?? '';
|
||||
|
||||
// Strip the SCRIPT_NAME path from the URI
|
||||
if (
|
||||
$path !== '' && $this->superglobals->server('SCRIPT_NAME') !== ''
|
||||
&& pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php'
|
||||
) {
|
||||
// Compare each segment, dropping them until there is no match
|
||||
$segments = $keep = explode('/', $path);
|
||||
|
||||
foreach (explode('/', $this->superglobals->server('SCRIPT_NAME')) as $i => $segment) {
|
||||
// If these segments are not the same then we're done
|
||||
if (! isset($segments[$i]) || $segment !== $segments[$i]) {
|
||||
break;
|
||||
}
|
||||
|
||||
array_shift($keep);
|
||||
}
|
||||
|
||||
$path = implode('/', $keep);
|
||||
}
|
||||
|
||||
// This section ensures that even on servers that require the URI to
|
||||
// contain the query string (Nginx) a correct URI is found, and also
|
||||
// fixes the QUERY_STRING Server var and $_GET array.
|
||||
if (trim($path, '/') === '' && str_starts_with($query, '/')) {
|
||||
$parts = explode('?', $query, 2);
|
||||
$path = $parts[0];
|
||||
$newQuery = $query[1] ?? '';
|
||||
|
||||
$this->superglobals->setServer('QUERY_STRING', $newQuery);
|
||||
} else {
|
||||
$this->superglobals->setServer('QUERY_STRING', $query);
|
||||
}
|
||||
|
||||
// Update our global GET for values likely to have been changed
|
||||
parse_str($this->superglobals->server('QUERY_STRING'), $get);
|
||||
$this->superglobals->setGetArray($get);
|
||||
|
||||
return URI::removeDotSegments($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will parse QUERY_STRING and automatically detect the URI from it.
|
||||
*
|
||||
* This method updates superglobal $_SERVER and $_GET.
|
||||
*
|
||||
* @return string The route path (before normalization).
|
||||
*/
|
||||
private function parseQueryString(): string
|
||||
{
|
||||
$query = $this->superglobals->server('QUERY_STRING') ?? (string) getenv('QUERY_STRING');
|
||||
|
||||
if (trim($query, '/') === '') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (str_starts_with($query, '/')) {
|
||||
$parts = explode('?', $query, 2);
|
||||
$path = $parts[0];
|
||||
$newQuery = $parts[1] ?? '';
|
||||
|
||||
$this->superglobals->setServer('QUERY_STRING', $newQuery);
|
||||
} else {
|
||||
$path = $query;
|
||||
}
|
||||
|
||||
// Update our global GET for values likely to have been changed
|
||||
parse_str($this->superglobals->server('QUERY_STRING'), $get);
|
||||
$this->superglobals->setGetArray($get);
|
||||
|
||||
return URI::removeDotSegments($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create current URI object.
|
||||
*
|
||||
* @param string $routePath URI path relative to baseURL
|
||||
*/
|
||||
private function createURIFromRoutePath(string $routePath): SiteURI
|
||||
{
|
||||
$query = $this->superglobals->server('QUERY_STRING') ?? '';
|
||||
|
||||
$relativePath = $query !== '' ? $routePath . '?' . $query : $routePath;
|
||||
|
||||
return new SiteURI($this->appConfig, $relativePath, $this->getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null The current hostname. Returns null if no valid host.
|
||||
*/
|
||||
private function getHost(): ?string
|
||||
{
|
||||
$httpHostPort = $this->superglobals->server('HTTP_HOST') ?? null;
|
||||
|
||||
if ($httpHostPort !== null) {
|
||||
[$httpHost] = explode(':', $httpHostPort, 2);
|
||||
|
||||
return $this->getValidHost($httpHost);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null The valid hostname. Returns null if not valid.
|
||||
*/
|
||||
private function getValidHost(string $host): ?string
|
||||
{
|
||||
if (in_array($host, $this->appConfig->allowedHostnames, true)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1185
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\HTTP;
|
||||
|
||||
use Config\UserAgents;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Abstraction for an HTTP user agent
|
||||
*
|
||||
* @see \CodeIgniter\HTTP\UserAgentTest
|
||||
*/
|
||||
class UserAgent implements Stringable
|
||||
{
|
||||
/**
|
||||
* Current user-agent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $agent = '';
|
||||
|
||||
/**
|
||||
* Flag for if the user-agent belongs to a browser
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isBrowser = false;
|
||||
|
||||
/**
|
||||
* Flag for if the user-agent is a robot
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isRobot = false;
|
||||
|
||||
/**
|
||||
* Flag for if the user-agent is a mobile browser
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isMobile = false;
|
||||
|
||||
/**
|
||||
* Holds the config file contents.
|
||||
*
|
||||
* @var UserAgents
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Current user-agent platform
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $platform = '';
|
||||
|
||||
/**
|
||||
* Current user-agent browser
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $browser = '';
|
||||
|
||||
/**
|
||||
* Current user-agent version
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $version = '';
|
||||
|
||||
/**
|
||||
* Current user-agent mobile name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $mobile = '';
|
||||
|
||||
/**
|
||||
* Current user-agent robot name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $robot = '';
|
||||
|
||||
/**
|
||||
* HTTP Referer
|
||||
*
|
||||
* @var bool|string|null
|
||||
*/
|
||||
protected $referrer;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Sets the User Agent and runs the compilation routine
|
||||
*/
|
||||
public function __construct(?UserAgents $config = null)
|
||||
{
|
||||
$this->config = $config ?? config(UserAgents::class);
|
||||
|
||||
if (isset($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$this->agent = trim($_SERVER['HTTP_USER_AGENT']);
|
||||
$this->compileData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Browser
|
||||
*/
|
||||
public function isBrowser(?string $key = null): bool
|
||||
{
|
||||
if (! $this->isBrowser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No need to be specific, it's a browser
|
||||
if ($key === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for a specific browser
|
||||
return isset($this->config->browsers[$key]) && $this->browser === $this->config->browsers[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Robot
|
||||
*/
|
||||
public function isRobot(?string $key = null): bool
|
||||
{
|
||||
if (! $this->isRobot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No need to be specific, it's a robot
|
||||
if ($key === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for a specific robot
|
||||
return isset($this->config->robots[$key]) && $this->robot === $this->config->robots[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Mobile
|
||||
*/
|
||||
public function isMobile(?string $key = null): bool
|
||||
{
|
||||
if (! $this->isMobile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No need to be specific, it's a mobile
|
||||
if ($key === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for a specific robot
|
||||
return isset($this->config->mobiles[$key]) && $this->mobile === $this->config->mobiles[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a referral from another site?
|
||||
*/
|
||||
public function isReferral(): bool
|
||||
{
|
||||
if (! isset($this->referrer)) {
|
||||
if (empty($_SERVER['HTTP_REFERER'])) {
|
||||
$this->referrer = false;
|
||||
} else {
|
||||
$refererHost = @parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
|
||||
$ownHost = parse_url(\base_url(), PHP_URL_HOST);
|
||||
|
||||
$this->referrer = ($refererHost && $refererHost !== $ownHost);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->referrer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent String
|
||||
*/
|
||||
public function getAgentString(): string
|
||||
{
|
||||
return $this->agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Platform
|
||||
*/
|
||||
public function getPlatform(): string
|
||||
{
|
||||
return $this->platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Browser Name
|
||||
*/
|
||||
public function getBrowser(): string
|
||||
{
|
||||
return $this->browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Browser Version
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get The Robot Name
|
||||
*/
|
||||
public function getRobot(): string
|
||||
{
|
||||
return $this->robot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Mobile Device
|
||||
*/
|
||||
public function getMobile(): string
|
||||
{
|
||||
return $this->mobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the referrer
|
||||
*/
|
||||
public function getReferrer(): string
|
||||
{
|
||||
return empty($_SERVER['HTTP_REFERER']) ? '' : trim($_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a custom user-agent string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parse(string $string)
|
||||
{
|
||||
// Reset values
|
||||
$this->isBrowser = false;
|
||||
$this->isRobot = false;
|
||||
$this->isMobile = false;
|
||||
$this->browser = '';
|
||||
$this->version = '';
|
||||
$this->mobile = '';
|
||||
$this->robot = '';
|
||||
|
||||
// Set the new user-agent string and parse it, unless empty
|
||||
$this->agent = $string;
|
||||
|
||||
if ($string !== '') {
|
||||
$this->compileData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the User Agent Data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function compileData()
|
||||
{
|
||||
$this->setPlatform();
|
||||
|
||||
foreach (['setRobot', 'setBrowser', 'setMobile'] as $function) {
|
||||
if ($this->{$function}() === true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Platform
|
||||
*/
|
||||
protected function setPlatform(): bool
|
||||
{
|
||||
if (is_array($this->config->platforms) && $this->config->platforms) {
|
||||
foreach ($this->config->platforms as $key => $val) {
|
||||
if (preg_match('|' . preg_quote($key, '|') . '|i', $this->agent)) {
|
||||
$this->platform = $val;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->platform = 'Unknown Platform';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Browser
|
||||
*/
|
||||
protected function setBrowser(): bool
|
||||
{
|
||||
if (is_array($this->config->browsers) && $this->config->browsers) {
|
||||
foreach ($this->config->browsers as $key => $val) {
|
||||
if (preg_match('|' . $key . '.*?([0-9\.]+)|i', $this->agent, $match)) {
|
||||
$this->isBrowser = true;
|
||||
$this->version = $match[1];
|
||||
$this->browser = $val;
|
||||
$this->setMobile();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Robot
|
||||
*/
|
||||
protected function setRobot(): bool
|
||||
{
|
||||
if (is_array($this->config->robots) && $this->config->robots) {
|
||||
foreach ($this->config->robots as $key => $val) {
|
||||
if (preg_match('|' . preg_quote($key, '|') . '|i', $this->agent)) {
|
||||
$this->isRobot = true;
|
||||
$this->robot = $val;
|
||||
$this->setMobile();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mobile Device
|
||||
*/
|
||||
protected function setMobile(): bool
|
||||
{
|
||||
if (is_array($this->config->mobiles) && $this->config->mobiles) {
|
||||
foreach ($this->config->mobiles as $key => $val) {
|
||||
if (false !== (stripos($this->agent, $key))) {
|
||||
$this->isMobile = true;
|
||||
$this->mobile = $val;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the original Agent String when cast as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getAgentString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user