diff --git a/composer.json b/composer.json index 5292e6e..149d36e 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "require": { "php": "^7.2|^8.0", "symfony/http-foundation": "^4|^5|^6", - "symfony/http-kernel": "^4|^5|^6" + "symfony/http-kernel": "^4|^5|^6", + "fruitcake/php-cors": "^1" }, "require-dev": { "phpunit/phpunit": "^7|^9", @@ -37,7 +38,9 @@ }, "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "3.0-dev" } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/src/Cors.php b/src/Cors.php index ded903e..ada60ff 100644 --- a/src/Cors.php +++ b/src/Cors.php @@ -11,6 +11,7 @@ namespace Asm89\Stack; +use Fruitcake\Cors\CorsService; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -23,7 +24,7 @@ class Cors implements HttpKernelInterface private $app; /** - * @var \Asm89\Stack\CorsService + * @var \Fruitcake\Cors\CorsService */ private $cors; diff --git a/src/CorsService.php b/src/CorsService.php deleted file mode 100644 index b8c3de9..0000000 --- a/src/CorsService.php +++ /dev/null @@ -1,225 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Asm89\Stack; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -class CorsService -{ - private $options; - - public function __construct(array $options = []) - { - $this->options = $this->normalizeOptions($options); - } - - private function normalizeOptions(array $options = []): array - { - $options += [ - 'allowedOrigins' => [], - 'allowedOriginsPatterns' => [], - 'supportsCredentials' => false, - 'allowedHeaders' => [], - 'exposedHeaders' => [], - 'allowedMethods' => [], - 'maxAge' => 0, - ]; - - // normalize array('*') to true - if (in_array('*', $options['allowedOrigins'])) { - $options['allowedOrigins'] = true; - } - if (in_array('*', $options['allowedHeaders'])) { - $options['allowedHeaders'] = true; - } else { - $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']); - } - - if (in_array('*', $options['allowedMethods'])) { - $options['allowedMethods'] = true; - } else { - $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); - } - - return $options; - } - - /** - * @deprecated use isOriginAllowed - */ - public function isActualRequestAllowed(Request $request): bool - { - return $this->isOriginAllowed($request); - } - - 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->options['allowedOrigins'] === true) { - return true; - } - - if (!$request->headers->has('Origin')) { - return false; - } - - $origin = $request->headers->get('Origin'); - - if (in_array($origin, $this->options['allowedOrigins'])) { - return true; - } - - foreach ($this->options['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) - { - if ($this->options['allowedOrigins'] === true && !$this->options['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->options['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', $request->headers->get('Origin')); - } - - $this->varyHeader($response, 'Origin'); - } - } - - private function isSingleOriginAllowed(): bool - { - if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) { - return false; - } - - return count($this->options['allowedOrigins']) === 1; - } - - private function configureAllowedMethods(Response $response, Request $request) - { - if ($this->options['allowedMethods'] === true) { - $allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method')); - $this->varyHeader($response, 'Access-Control-Request-Method'); - } else { - $allowMethods = implode(', ', $this->options['allowedMethods']); - } - - $response->headers->set('Access-Control-Allow-Methods', $allowMethods); - } - - private function configureAllowedHeaders(Response $response, Request $request) - { - if ($this->options['allowedHeaders'] === true) { - $allowHeaders = $request->headers->get('Access-Control-Request-Headers'); - $this->varyHeader($response, 'Access-Control-Request-Headers'); - } else { - $allowHeaders = implode(', ', $this->options['allowedHeaders']); - } - $response->headers->set('Access-Control-Allow-Headers', $allowHeaders); - } - - private function configureAllowCredentials(Response $response, Request $request) - { - if ($this->options['supportsCredentials']) { - $response->headers->set('Access-Control-Allow-Credentials', 'true'); - } - } - - private function configureExposedHeaders(Response $response, Request $request) - { - if ($this->options['exposedHeaders']) { - $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); - } - } - - private function configureMaxAge(Response $response, Request $request) - { - if ($this->options['maxAge'] !== null) { - $response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']); - } - } - - public function varyHeader(Response $response, $header): Response - { - if (!$response->headers->has('Vary')) { - $response->headers->set('Vary', $header); - } elseif (!in_array($header, explode(', ', $response->headers->get('Vary')))) { - $response->headers->set('Vary', $response->headers->get('Vary') . ', ' . $header); - } - - return $response; - } - - private function isSameHost(Request $request): bool - { - return $request->headers->get('Origin') === $request->getSchemeAndHttpHost(); - } -} diff --git a/tests/CorsServiceTest.php b/tests/CorsServiceTest.php deleted file mode 100644 index 00ab50b..0000000 --- a/tests/CorsServiceTest.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Asm89\Stack\Tests; - -use Asm89\Stack\CorsService; -use PHPUnit\Framework\TestCase; - -class CorsServiceTest extends TestCase -{ - /** - * @test - */ - public function it_can_have_options() - { - $service = new CorsService([ - 'allowedOrigins' => ['*'] - ]); - - $this->assertInstanceOf(CorsService::class, $service); - } - - /** - * @test - */ - public function it_can_have_no_options() - { - $service = new CorsService(); - $this->assertInstanceOf(CorsService::class, $service); - - } - - /** - * @test - */ - public function it_can_have_empty_options() - { - $service = new CorsService([]); - $this->assertInstanceOf(CorsService::class, $service); - - } -} diff --git a/tests/CorsTest.php b/tests/CorsTest.php index d63d004..83101e9 100644 --- a/tests/CorsTest.php +++ b/tests/CorsTest.php @@ -12,7 +12,7 @@ namespace Asm89\Stack\Tests; use Asm89\Stack\Cors; -use Asm89\Stack\CorsService; +use Fruitcake\Cors\CorsService; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response;