Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/** This file is part of fruitcake/php-cors and was originally part of asm89/stack-cors** (c) Alexander <iam.asm89@gmail.com>* (c) Barryvdh <barryvdh@gmail.com>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace Fruitcake\Cors;use Fruitcake\Cors\Exceptions\InvalidOptionException;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;/*** @phpstan-type CorsInputOptions array{* 'allowedOrigins'?: string[],* 'allowedOriginsPatterns'?: string[],* 'supportsCredentials'?: bool,* 'allowedHeaders'?: string[],* 'allowedMethods'?: string[],* 'exposedHeaders'?: string[]|false,* 'maxAge'?: int|bool|null,* 'allowed_origins'?: string[],* 'allowed_origins_patterns'?: string[],* 'supports_credentials'?: bool,* 'allowed_headers'?: string[],* 'allowed_methods'?: string[],* 'exposed_headers'?: string[]|false,* 'max_age'?: int|bool|null* }**/class CorsService{/** @var string[] */private array $allowedOrigins = [];/** @var string[] */private array $allowedOriginsPatterns = [];/** @var string[] */private array $allowedMethods = [];/** @var string[] */private array $allowedHeaders = [];/** @var string[] */private array $exposedHeaders = [];private bool $supportsCredentials = false;private ?int $maxAge = 0;private bool $allowAllOrigins = false;private bool $allowAllMethods = false;private bool $allowAllHeaders = false;/*** @param CorsInputOptions $options*/public function __construct(array $options = []){if ($options) {$this->setOptions($options);}}/*** @param CorsInputOptions $options*/public function setOptions(array $options): void{$this->allowedOrigins = $options['allowedOrigins'] ?? $options['allowed_origins'] ?? $this->allowedOrigins;$this->allowedOriginsPatterns =$options['allowedOriginsPatterns'] ?? $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns;$this->allowedMethods = $options['allowedMethods'] ?? $options['allowed_methods'] ?? $this->allowedMethods;$this->allowedHeaders = $options['allowedHeaders'] ?? $options['allowed_headers'] ?? $this->allowedHeaders;$this->supportsCredentials =$options['supportsCredentials'] ?? $options['supports_credentials'] ?? $this->supportsCredentials;$maxAge = $this->maxAge;if (array_key_exists('maxAge', $options)) {$maxAge = $options['maxAge'];} elseif (array_key_exists('max_age', $options)) {$maxAge = $options['max_age'];}$this->maxAge = $maxAge === null ? null : (int)$maxAge;$exposedHeaders = $options['exposedHeaders'] ?? $options['exposed_headers'] ?? $this->exposedHeaders;$this->exposedHeaders = $exposedHeaders === false ? [] : $exposedHeaders;$this->normalizeOptions();}private function normalizeOptions(): void{// Normalize case$this->allowedHeaders = array_map('strtolower', $this->allowedHeaders);$this->allowedMethods = array_map('strtoupper', $this->allowedMethods);// Normalize ['*'] to true$this->allowAllOrigins = in_array('*', $this->allowedOrigins);$this->allowAllHeaders = in_array('*', $this->allowedHeaders);$this->allowAllMethods = in_array('*', $this->allowedMethods);// Transform wildcard patternif (!$this->allowAllOrigins) {foreach ($this->allowedOrigins as $origin) {if (strpos($origin, '*') !== false) {$this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin);}}}}/*** Create a pattern for a wildcard, based on Str::is() from Laravel** @see https://github.com/laravel/framework/blob/5.5/src/Illuminate/Support/Str.php* @param string $pattern* @return string*/private function convertWildcardToPattern($pattern){$pattern = preg_quote($pattern, '#');// Asterisks are translated into zero-or-more regular expression wildcards// to make it convenient to check if the strings starts with the given// pattern such as "*.example.com", making any string check convenient.$pattern = str_replace('\*', '.*', $pattern);return '#^' . $pattern . '\z#u';}public function isCorsRequest(Request $request): bool{return $request->headers->has('Origin');}public function isPreflightRequest(Request $request): bool{return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method');}public function handlePreflightRequest(Request $request): Response{$response = new Response();$response->setStatusCode(204);return $this->addPreflightRequestHeaders($response, $request);}public function addPreflightRequestHeaders(Response $response, Request $request): Response{$this->configureAllowedOrigin($response, $request);if ($response->headers->has('Access-Control-Allow-Origin')) {$this->configureAllowCredentials($response, $request);$this->configureAllowedMethods($response, $request);$this->configureAllowedHeaders($response, $request);$this->configureMaxAge($response, $request);}return $response;}public function isOriginAllowed(Request $request): bool{if ($this->allowAllOrigins === true) {return true;}$origin = (string) $request->headers->get('Origin');if (in_array($origin, $this->allowedOrigins)) {return true;}foreach ($this->allowedOriginsPatterns as $pattern) {if (preg_match($pattern, $origin)) {return true;}}return false;}public function addActualRequestHeaders(Response $response, Request $request): Response{$this->configureAllowedOrigin($response, $request);if ($response->headers->has('Access-Control-Allow-Origin')) {$this->configureAllowCredentials($response, $request);$this->configureExposedHeaders($response, $request);}return $response;}private function configureAllowedOrigin(Response $response, Request $request): void{if ($this->allowAllOrigins === true && !$this->supportsCredentials) {// Safe+cacheable, allow everything$response->headers->set('Access-Control-Allow-Origin', '*');} elseif ($this->isSingleOriginAllowed()) {// Single origins can be safely set$response->headers->set('Access-Control-Allow-Origin', array_values($this->allowedOrigins)[0]);} else {// For dynamic headers, set the requested Origin header when set and allowedif ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {$response->headers->set('Access-Control-Allow-Origin', (string) $request->headers->get('Origin'));}$this->varyHeader($response, 'Origin');}}private function isSingleOriginAllowed(): bool{if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) {return false;}return count($this->allowedOrigins) === 1;}private function configureAllowedMethods(Response $response, Request $request): void{if ($this->allowAllMethods === true) {$allowMethods = strtoupper((string) $request->headers->get('Access-Control-Request-Method'));$this->varyHeader($response, 'Access-Control-Request-Method');} else {$allowMethods = implode(', ', $this->allowedMethods);}$response->headers->set('Access-Control-Allow-Methods', $allowMethods);}private function configureAllowedHeaders(Response $response, Request $request): void{if ($this->allowAllHeaders === true) {$allowHeaders = (string) $request->headers->get('Access-Control-Request-Headers');$this->varyHeader($response, 'Access-Control-Request-Headers');} else {$allowHeaders = implode(', ', $this->allowedHeaders);}$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);}private function configureAllowCredentials(Response $response, Request $request): void{if ($this->supportsCredentials) {$response->headers->set('Access-Control-Allow-Credentials', 'true');}}private function configureExposedHeaders(Response $response, Request $request): void{if ($this->exposedHeaders) {$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->exposedHeaders));}}private function configureMaxAge(Response $response, Request $request): void{if ($this->maxAge !== null) {$response->headers->set('Access-Control-Max-Age', (string) $this->maxAge);}}public function varyHeader(Response $response, string $header): Response{if (!$response->headers->has('Vary')) {$response->headers->set('Vary', $header);} elseif (!in_array($header, explode(', ', (string) $response->headers->get('Vary')))) {$response->headers->set('Vary', ((string) $response->headers->get('Vary')) . ', ' . $header);}return $response;}}