diff --git a/composer.json b/composer.json index 974dc0e..694993e 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "symfony/serializer": "2.5.*", "symfony/validator": "2.5.*", "symfony/yaml": "dev-master#499f7d7aa96747ad97940089bd7a1fb24ad8182a", + "asm89/stack-cors": "0.2.*", "twig/twig": "1.16.*", "doctrine/common": "dev-master#a45d110f71c323e29f41eb0696fa230e3fa1b1b5", "doctrine/annotations": "1.2.*", diff --git a/composer.lock b/composer.lock index 4849c75..48c0f0d 100644 --- a/composer.lock +++ b/composer.lock @@ -7,6 +7,49 @@ "hash": "2bd5814ec010b13a6997359736b58695", "packages": [ { + "name": "asm89/stack-cors", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/asm89/stack-cors.git", + "reference": "2d77e77251a434e4527315313a672f5801b29fa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/2d77e77251a434e4527315313a672f5801b29fa2", + "reference": "2d77e77251a434e4527315313a672f5801b29fa2", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/http-foundation": "~2.1", + "symfony/http-kernel": "~2.1" + }, + "type": "library", + "autoload": { + "psr-0": { + "Asm89\\Stack": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "description": "Cross-origin resource sharing library and stack middleware", + "homepage": "https://github.com/asm89/stack-cors", + "keywords": [ + "cors", + "stack" + ], + "time": "2014-07-28 07:22:35" + }, + { "name": "doctrine/annotations", "version": "v1.2.1", "source": { diff --git a/core/core.services.yml b/core/core.services.yml index 9c3f606..452f210 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1,5 +1,6 @@ parameters: twig.config: {} + cors.config: {} factory.keyvalue: default: keyvalue.database factory.keyvalue.expirable: @@ -412,6 +413,11 @@ services: arguments: ['@kernel'] tags: - { name: http_middleware, priority: 100 } + http_middleware.cors: + class: Asm89\Stack\Cors + arguments: ['%cors.config%'] + tags: + - { name: http_middleware } language_manager: class: Drupal\Core\Language\LanguageManager arguments: ['@language.default'] diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index db54f91..862c30d 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\CacheContextsPass; use Drupal\Core\Cache\ListCacheBinsPass; use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass; +use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass; use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass; use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass; use Drupal\Core\DependencyInjection\ServiceProviderInterface; @@ -52,6 +53,8 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new BackendCompilerPass()); + $container->addCompilerPass(new CorsCompilerPass()); + $container->addCompilerPass(new StackedKernelPass()); // Collect tagged handler services as method calls on consumer services. diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php new file mode 100644 index 0000000..3c66ad6 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php @@ -0,0 +1,36 @@ +getParameter('cors.config')) { + $enabled = !empty($cors_config['enabled']); + } + + // Remove the CORS middleware completly in case it was not enabled. + if (!$enabled) { + $container->removeDefinition('http_middleware.cors'); + } + } + +} diff --git a/core/modules/system/src/Tests/HttpKernel/CorsIntegrationTest.php b/core/modules/system/src/Tests/HttpKernel/CorsIntegrationTest.php new file mode 100755 index 0000000..d201998 --- /dev/null +++ b/core/modules/system/src/Tests/HttpKernel/CorsIntegrationTest.php @@ -0,0 +1,56 @@ +container->getParameter('cors.config'); + $this->assertFalse($cors_config['enabled']); + $this->assertEqual([], $cors_config['allowedHeaders']); + $this->assertEqual([], $cors_config['allowedMethods']); + $this->assertEqual(['*'], $cors_config['allowedOrigins']); + + $this->assertEqual(FALSE, $cors_config['allowedHeaders']); + $this->assertEqual(FALSE, $cors_config['allowedMethods']); + $this->assertEqual(FALSE, $cors_config['supportsCredentials']); + + // Configure the CORS stack to allow a specific set of origins, but don't + // specify an origin header. + $request = Request::create('/'); + $request->headers->set('Origin', ''); + $cors_config['enabled'] = TRUE; + $cors_config['allowedOrigins'] = array('http://example.com'); + $this->setContainerParameter('cors.config', $cors_config); + $this->rebuildContainer(); + + $response = $this->container->get('http_kernel')->handle($request); + $this->assertEqual(Response::HTTP_FORBIDDEN, $response->getStatusCode()); + $this->assertEqual('Not allowed.', $response->getContent()); + + // Specify a valid origin. + $request->headers->set('Origin', 'http://example.com'); + $response = $this->container->get('http_kernel')->handle($request); + $this->assertEqual(Response::HTTP_OK, $response->getStatusCode()); + } + +} diff --git a/core/vendor/asm89/stack-cors/.travis.yml b/core/vendor/asm89/stack-cors/.travis.yml new file mode 100644 index 0000000..ddb1058 --- /dev/null +++ b/core/vendor/asm89/stack-cors/.travis.yml @@ -0,0 +1,10 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + +before_script: composer install --dev + +script: phpunit diff --git a/core/vendor/asm89/stack-cors/README.md b/core/vendor/asm89/stack-cors/README.md new file mode 100644 index 0000000..dea18a8 --- /dev/null +++ b/core/vendor/asm89/stack-cors/README.md @@ -0,0 +1,59 @@ +# Stack/Cors + +Library and middleware enabling cross-origin resource sharing for your +http-{foundation,kernel} using application. It attempts to implement the +[W3C Candidate Recommendation] for cross-origin resource sharing. + +[W3C Candidate Recommendation]: http://www.w3.org/TR/cors/ + +Master [![Build Status](https://secure.travis-ci.org/asm89/stack-cors.png?branch=master)](http://travis-ci.org/asm89/stack-cors) +Develop [![Build Status](https://secure.travis-ci.org/asm89/stack-cors.png?branch=develop)](http://travis-ci.org/asm89/stack-cors) + +## Installation + +Require `asm89/stack-cors` using composer. + +## Usage + +Stack middleware: + +```php + array('x-allowed-header', 'x-other-allowed-header'), + // you can use array('*') to allow any methods + 'allowedMethods' => array('DELETE', 'GET', 'POST', 'PUT'), + // you can use array('*') to allow requests from any origin + 'allowedOrigins' => array('localhost'), + 'exposedHeaders' => false, + 'maxAge' => false, + 'supportsCredentials' => false, +)); +``` + +Or use the library: + +```php + array('x-allowed-header', 'x-other-allowed-header'), + 'allowedMethods' => array('DELETE', 'GET', 'POST', 'PUT'), + 'allowedOrigins' => array('localhost'), + 'exposedHeaders' => false, + 'maxAge' => false, + 'supportsCredentials' => false, +)); + +$cors->addActualRequestHeaders(Response $response, $origin); +$cors->handlePreflightRequest(Request $request); +$cors->isActualRequestAllowed(Request $request); +$cors->isCorsRequest(Request $request); +$cors->isPreflightRequest(Request $request); +``` diff --git a/core/vendor/asm89/stack-cors/composer.json b/core/vendor/asm89/stack-cors/composer.json new file mode 100644 index 0000000..f8be507 --- /dev/null +++ b/core/vendor/asm89/stack-cors/composer.json @@ -0,0 +1,22 @@ +{ + "name": "asm89/stack-cors", + "description": "Cross-origin resource sharing library and stack middleware", + "keywords": ["stack", "cors"], + "homepage": "https://github.com/asm89/stack-cors", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "require": { + "php": ">=5.3.2", + "symfony/http-foundation": "~2.1", + "symfony/http-kernel": "~2.1" + }, + "autoload": { + "psr-0": { "Asm89\\Stack": "src/" } + } +} diff --git a/core/vendor/asm89/stack-cors/phpunit.xml.dist b/core/vendor/asm89/stack-cors/phpunit.xml.dist new file mode 100644 index 0000000..1959e53 --- /dev/null +++ b/core/vendor/asm89/stack-cors/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + ./test/Asm89/ + + + + + + ./src/ + + + + diff --git a/core/vendor/asm89/stack-cors/src/Asm89/Stack/Cors.php b/core/vendor/asm89/stack-cors/src/Asm89/Stack/Cors.php new file mode 100755 index 0000000..a5e81ce --- /dev/null +++ b/core/vendor/asm89/stack-cors/src/Asm89/Stack/Cors.php @@ -0,0 +1,55 @@ + array(), + 'allowedMethods' => array(), + 'allowedOrigins' => array(), + 'exposedHeaders' => false, + 'maxAge' => false, + 'supportsCredentials' => false, + ); + + public function __construct(HttpKernelInterface $app, array $options = array()) + { + $this->app = $app; + $this->cors = new CorsService(array_merge($this->defaultOptions, $options)); + + } + + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + if ( ! $this->cors->isCorsRequest($request)) { + return $this->app->handle($request, $type, $catch); + } + + if ($this->cors->isPreflightRequest($request)) { + return $this->cors->handlePreflightRequest($request); + } + + if ( ! $this->cors->isActualRequestAllowed($request)) { + return new Response('Not allowed.', 403); + } + + $response = $this->app->handle($request, $type, $catch); + + return $this->cors->addActualRequestHeaders($response, $request); + } +} diff --git a/core/vendor/asm89/stack-cors/src/Asm89/Stack/CorsService.php b/core/vendor/asm89/stack-cors/src/Asm89/Stack/CorsService.php new file mode 100755 index 0000000..339256b --- /dev/null +++ b/core/vendor/asm89/stack-cors/src/Asm89/Stack/CorsService.php @@ -0,0 +1,178 @@ +options = $this->normalizeOptions($options); + } + + private function normalizeOptions(array $options = array()) + { + + $options += array( + 'allowedOrigins' => array(), + 'supportsCredentials' => false, + 'allowedHeaders' => array(), + 'exposedHeaders' => array(), + 'allowedMethods' => array(), + '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; + } + + public function isActualRequestAllowed(Request $request) + { + return $this->checkOrigin($request); + } + + public function isCorsRequest(Request $request) + { + return $request->headers->has('Origin'); + } + + public function isPreflightRequest(Request $request) + { + return $this->isCorsRequest($request) + &&$request->getMethod() === 'OPTIONS' + && $request->headers->has('Access-Control-Request-Method'); + } + + public function addActualRequestHeaders(Response $response, Request $request) + { + if ( ! $this->checkOrigin($request)) { + return $response; + } + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + if ( ! $response->headers->has('Vary')) { + $response->headers->set('Vary', 'Origin'); + } else { + $response->headers->set('Vary', $response->headers->get('Vary') . ', Origin'); + } + + if ($this->options['supportsCredentials']) { + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + } + + if ($this->options['exposedHeaders']) { + $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); + } + + return $response; + } + + public function handlePreflightRequest(Request $request) + { + if (true !== $check = $this->checkPreflightRequestConditions($request)) { + return $check; + } + + return $this->buildPreflightCheckResponse($request); + } + + private function buildPreflightCheckResponse(Request $request) + { + $response = new Response(); + + if ($this->options['supportsCredentials']) { + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + } + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + if ($this->options['maxAge']) { + $response->headers->set('Access-Control-Max-Age', $this->options['maxAge']); + } + + $allowMethods = $this->options['allowedMethods'] === true + ? strtoupper($request->headers->get('Access-Control-Request-Method')) + : implode(', ', $this->options['allowedMethods']); + $response->headers->set('Access-Control-Allow-Methods', $allowMethods); + + $allowHeaders = $this->options['allowedHeaders'] === true + ? strtoupper($request->headers->get('Access-Control-Request-Headers')) + : implode(', ', $this->options['allowedHeaders']); + $response->headers->set('Access-Control-Allow-Headers', $allowHeaders); + + return $response; + } + + private function checkPreflightRequestConditions(Request $request) + { + if ( ! $this->checkOrigin($request)) { + return $this->createBadRequestResponse(403, 'Origin not allowed'); + } + + if ( ! $this->checkMethod($request)) { + return $this->createBadRequestResponse(405, 'Method not allowed'); + } + + $requestHeaders = array(); + // if allowedHeaders has been set to true ('*' allow all flag) just skip this check + if ($this->options['allowedHeaders'] !== true && $request->headers->has('Access-Control-Request-Headers')) { + $headers = strtolower($request->headers->get('Access-Control-Request-Headers')); + $requestHeaders = explode(',', $headers); + + foreach ($requestHeaders as $header) { + if ( ! in_array(trim($header), $this->options['allowedHeaders'])) { + return $this->createBadRequestResponse(403, 'Header not allowed'); + } + } + } + + return true; + } + + private function createBadRequestResponse($code, $reason = '') + { + return new Response($reason, $code); + } + + private function checkOrigin(Request $request) { + if ($this->options['allowedOrigins'] === true) { + // allow all '*' flag + return true; + } + $origin = $request->headers->get('Origin'); + + return in_array($origin, $this->options['allowedOrigins']); + } + + private function checkMethod(Request $request) { + if ($this->options['allowedMethods'] === true) { + // allow all '*' flag + return true; + } + + $requestMethod = strtoupper($request->headers->get('Access-Control-Request-Method')); + return in_array($requestMethod, $this->options['allowedMethods']); + } + +} diff --git a/core/vendor/asm89/stack-cors/test/Asm89/Stack/CorsTest.php b/core/vendor/asm89/stack-cors/test/Asm89/Stack/CorsTest.php new file mode 100755 index 0000000..1493c46 --- /dev/null +++ b/core/vendor/asm89/stack-cors/test/Asm89/Stack/CorsTest.php @@ -0,0 +1,395 @@ +createStackedApp(); + $unmodifiedResponse = new Response(); + + $response = $app->handle(new Request()); + + $this->assertEquals($unmodifiedResponse->headers, $response->headers); + } + + /** + * @test + */ + public function it_returns_403_on_valid_actual_request_with_origin_not_allowed() + { + $app = $this->createStackedApp(array('allowedOrigins' => array('notlocalhost'))); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertEquals(403, $response->getStatusCode()); + } + + /** + * @test + */ + public function it_returns_allow_origin_header_on_valid_actual_request() + { + $app = $this->createStackedApp(); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Origin')); + $this->assertEquals('localhost', $response->headers->get('Access-Control-Allow-Origin')); + } + + /** + * @test + */ + public function it_returns_allow_origin_header_on_allow_all_origin_request() + { + $app = $this->createStackedApp(array('allowedOrigins' => array('*'))); + $request = new Request(); + $request->headers->set('Origin', 'http://localhost'); + + $response = $app->handle($request); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertTrue($response->headers->has('Access-Control-Allow-Origin')); + $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin')); + } + + /** + * @test + */ + public function it_returns_allow_headers_header_on_allow_all_headers_request() + { + $app = $this->createStackedApp(array('allowedHeaders' => array('*'))); + $request = $this->createValidPreflightRequest(); + $request->headers->set('Access-Control-Request-Headers', 'Foo, BAR'); + + $response = $app->handle($request); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('FOO, BAR', $response->headers->get('Access-Control-Allow-Headers')); + } + + /** + * @test + */ + public function it_does_not_return_allow_origin_header_on_valid_actual_request_with_origin_not_allowed() + { + $app = $this->createStackedApp(array('allowedOrigins' => array('notlocalhost'))); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertFalse($response->headers->has('Access-Control-Allow-Origin')); + } + + /** + * @test + */ + public function it_sets_allow_credentials_header_when_flag_is_set_on_valid_actual_request() + { + $app = $this->createStackedApp(array('supportsCredentials' => true)); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Credentials')); + $this->assertEquals('true', $response->headers->get('Access-Control-Allow-Credentials')); + } + + /** + * @test + */ + public function it_does_not_set_allow_credentials_header_when_flag_is_not_set_on_valid_actual_request() + { + $app = $this->createStackedApp(); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertFalse($response->headers->has('Access-Control-Allow-Credentials')); + } + + /** + * @test + */ + public function it_sets_exposed_headers_when_configured_on_actual_request() + { + $app = $this->createStackedApp(array('exposedHeaders' => array('x-exposed-header', 'x-another-exposed-header'))); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Expose-Headers')); + $this->assertEquals('x-exposed-header, x-another-exposed-header', $response->headers->get('Access-Control-Expose-Headers')); + } + + /** + * @test + * @see http://www.w3.org/TR/cors/index.html#resource-implementation + */ + public function it_adds_a_vary_header() + { + $app = $this->createStackedApp(); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Vary')); + $this->assertEquals('Origin', $response->headers->get('Vary')); + } + + /** + * @test + * @see http://www.w3.org/TR/cors/index.html#resource-implementation + */ + public function it_appends_an_existing_vary_header() + { + $app = $this->createStackedApp(array(), array('Vary' => 'Content-Type')); + $request = $this->createValidActualRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Vary')); + $this->assertEquals('Content-Type, Origin', $response->headers->get('Vary')); + } + + /** + * @test + */ + public function it_returns_access_control_headers_on_cors_request() + { + $app = $this->createStackedApp(); + $request = new Request(); + $request->headers->set('Origin', 'localhost'); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Origin')); + $this->assertEquals('localhost', $response->headers->get('Access-Control-Allow-Origin')); + } + + /** + * @test + */ + public function it_returns_access_control_headers_on_valid_preflight_request() + { + $app = $this->createStackedApp(); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Origin')); + $this->assertEquals('localhost', $response->headers->get('Access-Control-Allow-Origin')); + } + + /** + * @test + */ + public function it_returns_403_on_valid_preflight_request_with_origin_not_allowed() + { + $app = $this->createStackedApp(array('allowedOrigins' => array('notlocalhost'))); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertEquals(403, $response->getStatusCode()); + } + + /** + * @test + */ + public function it_does_not_modify_request_with_origin_not_allowed() + { + $passedOptions = array( + 'allowedOrigins' => array('notlocalhost'), + ); + + $service = new CorsService($passedOptions); + $request = $this->createValidActualRequest(); + $response = new Response(); + $service->addActualRequestHeaders($response, $request); + + $this->assertEquals($response, new Response()); + } + + /** + * @test + */ + public function it_returns_405_on_valid_preflight_request_with_method_not_allowed() + { + $app = $this->createStackedApp(array('allowedMethods' => array('put'))); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertEquals(405, $response->getStatusCode()); + } + + /** + * @test + */ + public function it_allow_methods_on_valid_preflight_request() + { + $app = $this->createStackedApp(array('allowedMethods' => array('get', 'put'))); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Methods')); + // it will uppercase the methods + $this->assertEquals('GET, PUT', $response->headers->get('Access-Control-Allow-Methods')); + } + + /** + * @test + */ + public function it_returns_valid_preflight_request_with_allow_methods_all() + { + $app = $this->createStackedApp(array('allowedMethods' => array('*'))); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Methods')); + // it will return the Access-Control-Request-Method pass in the request + $this->assertEquals('GET', $response->headers->get('Access-Control-Allow-Methods')); + } + + /** + * @test + */ + public function it_returns_403_on_valid_preflight_request_with_one_of_the_requested_headers_not_allowed() + { + $app = $this->createStackedApp(); + $request = $this->createValidPreflightRequest(); + $request->headers->set('Access-Control-Request-Headers', 'x-not-allowed-header'); + + $response = $app->handle($request); + + $this->assertEquals(403, $response->getStatusCode()); + } + + /** + * @test + */ + public function it_returns_ok_on_valid_preflight_request_with_requested_headers_allowed() + { + $app = $this->createStackedApp(); + $requestHeaders = 'X-Allowed-Header, x-other-allowed-header'; + $request = $this->createValidPreflightRequest(); + $request->headers->set('Access-Control-Request-Headers', $requestHeaders); + + $response = $app->handle($request); + + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Headers')); + // the response will have the "allowedHeaders" value passed to Cors rather than the request one + $this->assertEquals('x-allowed-header, x-other-allowed-header', $response->headers->get('Access-Control-Allow-Headers')); + } + + /** + * @test + */ + public function it_sets_allow_credentials_header_when_flag_is_set_on_valid_preflight_request() + { + $app = $this->createStackedApp(array('supportsCredentials' => true)); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Allow-Credentials')); + $this->assertEquals('true', $response->headers->get('Access-Control-Allow-Credentials')); + } + + /** + * @test + */ + public function it_does_not_set_allow_credentials_header_when_flag_is_not_set_on_valid_preflight_request() + { + $app = $this->createStackedApp(); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertFalse($response->headers->has('Access-Control-Allow-Credentials')); + } + + /** + * @test + */ + public function it_sets_max_age_when_set() + { + $app = $this->createStackedApp(array('maxAge' => 42)); + $request = $this->createValidPreflightRequest(); + + $response = $app->handle($request); + + $this->assertTrue($response->headers->has('Access-Control-Max-Age')); + $this->assertEquals(42, $response->headers->get('Access-Control-Max-Age')); + } + + private function createValidActualRequest() + { + $request = new Request(); + $request->headers->set('Origin', 'localhost'); + + return $request; + } + + private function createValidPreflightRequest() + { + $request = new Request(); + $request->headers->set('Origin', 'localhost'); + $request->headers->set('Access-Control-Request-Method', 'get'); + $request->setMethod('OPTIONS'); + + return $request; + } + + private function createStackedApp(array $options = array(), array $responseHeaders = array()) + { + $passedOptions = array_merge(array( + 'allowedHeaders' => array('x-allowed-header', 'x-other-allowed-header'), + 'allowedMethods' => array('delete', 'get', 'post', 'put'), + 'allowedOrigins' => array('localhost'), + 'exposedHeaders' => false, + 'maxAge' => false, + 'supportsCredentials' => false, + ), + $options + ); + + return new Cors(new MockApp($responseHeaders), $passedOptions); + } +} + +class MockApp implements HttpKernelInterface +{ + private $responseHeaders; + + public function __construct(array $responseHeaders) + { + $this->responseHeaders = $responseHeaders; + } + + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + $response = new Response(); + + $response->headers->add($this->responseHeaders); + + return $response; + } +} diff --git a/core/vendor/asm89/stack-cors/test/bootstrap.php b/core/vendor/asm89/stack-cors/test/bootstrap.php new file mode 100644 index 0000000..deb617f --- /dev/null +++ b/core/vendor/asm89/stack-cors/test/bootstrap.php @@ -0,0 +1,10 @@ +add('Asm89\Stack', __DIR__); + $loader->add('Asm89\Stack', __DIR__ . '/../src'); +} else { + throw new RuntimeException('Install dependencies to run test suite.'); +} + diff --git a/core/vendor/composer/autoload_namespaces.php b/core/vendor/composer/autoload_namespaces.php index 54e2998..76a5c48 100644 --- a/core/vendor/composer/autoload_namespaces.php +++ b/core/vendor/composer/autoload_namespaces.php @@ -36,4 +36,5 @@ 'Doctrine\\Common\\Cache\\' => array($vendorDir . '/doctrine/cache/lib'), 'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib'), 'Doctrine\\Common\\' => array($vendorDir . '/doctrine/common/lib'), + 'Asm89\\Stack' => array($vendorDir . '/asm89/stack-cors/src'), ); diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index 06b603a..5cc5d36 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -2645,5 +2645,109 @@ "rest", "web service" ] + }, + { + "name": "twig/twig", + "version": "v1.16.2", + "version_normalized": "1.16.2.0", + "source": { + "type": "git", + "url": "https://github.com/fabpot/Twig.git", + "reference": "42f758d9fe2146d1f0470604fc05ee43580873fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fabpot/Twig/zipball/42f758d9fe2146d1f0470604fc05ee43580873fc", + "reference": "42f758d9fe2146d1f0470604fc05ee43580873fc", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "time": "2014-10-17 12:53:44", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.16-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Twig_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "https://github.com/fabpot/Twig/graphs/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "http://twig.sensiolabs.org", + "keywords": [ + "templating" + ] + }, + { + "name": "asm89/stack-cors", + "version": "0.2.1", + "version_normalized": "0.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/asm89/stack-cors.git", + "reference": "2d77e77251a434e4527315313a672f5801b29fa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/2d77e77251a434e4527315313a672f5801b29fa2", + "reference": "2d77e77251a434e4527315313a672f5801b29fa2", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/http-foundation": "~2.1", + "symfony/http-kernel": "~2.1" + }, + "time": "2014-07-28 07:22:35", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Asm89\\Stack": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "description": "Cross-origin resource sharing library and stack middleware", + "homepage": "https://github.com/asm89/stack-cors", + "keywords": [ + "cors", + "stack" + ] } ] diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml old mode 100644 new mode 100755 index a5307ec..31ab8bb --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -51,3 +51,20 @@ parameters: # Default key/value expirable storage service to use. # @default keyvalue.database.expirable # default: keyvalue.database.expirable + + # Allow to configure Cross-Site HTTP requests (CORS). + # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + # for more information about the topic in general. + # Note: By default the configuration is done in a way that no behaviour is + # changed. + cors.config: + enabled: false + # Specify allowed headers, like 'x-allowed-header'. + allowedHeaders: [] + # Specify allowed request methods, specify '*' to allow all possible ones. + allowedMethods: [] + # Configure requests allowed from specific origins. + allowedOrigins: ['*'] + exposedHeaders: false + maxAge: false + supportsCredentials: false