2024-03-27 19:20:44 +09:00

231 lines
7.0 KiB
PHP

<?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'])
);
}
}
}