| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- <?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 pattern
- if (!$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 allowed
- if ($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);
- } else {
- $varyHeaders = $response->getVary();
- if (!in_array($header, $varyHeaders, true)) {
- if (count($response->headers->all('Vary')) === 1) {
- $response->setVary(((string)$response->headers->get('Vary')) . ', ' . $header);
- } else {
- $response->setVary($header, false);
- }
- }
- }
- return $response;
- }
- }
|