diff --git a/core/core.services.yml b/core/core.services.yml index c5a13a4..3a53466 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1022,6 +1022,11 @@ services: tags: - { name: event_subscriber } arguments: ['@router', '@request_stack', '@router.request_context', NULL] + options_request_listener: + class: Drupal\Core\EventSubscriber\OptionsRequestListener + arguments: ['@router.route_provider'] + tags: + - { name: event_subscriber } bare_html_page_renderer: class: Drupal\Core\Render\BareHtmlPageRenderer arguments: ['@renderer', '@html_response.attachments_processor'] diff --git a/core/lib/Drupal/Core/EventSubscriber/OptionsRequestListener.php b/core/lib/Drupal/Core/EventSubscriber/OptionsRequestListener.php new file mode 100644 index 0000000..47727cc --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/OptionsRequestListener.php @@ -0,0 +1,71 @@ +routeProvider = $routeProvider; + } + + /** + * Tries to handle the options request. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The request event. + */ + public function onRequest(GetResponseEvent $event) { + if ($event->getRequest()->isMethod('OPTIONS')) { + $routes = $this->routeProvider->getRouteCollectionForRequest($event->getRequest()); + // In case we don't have any routes, a 403 should be thrown by the normal + // request handling. + if (count($routes) > 0) { + $methods = array_map(function (Route $route) { + return $route->getMethods(); + }, $routes->all()); + // Flatten and unique the available methods. + $methods = array_unique(call_user_func_array('array_merge', $methods)); + $response = new Response('', 200, ['Allow' => implode(', ', $methods)]); + $event->setResponse($response); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = ['onRequest', 1000] ; + return $events; + } + +} diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/OptionsRequestListenerTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/OptionsRequestListenerTest.php new file mode 100644 index 0000000..7bba0c1 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/OptionsRequestListenerTest.php @@ -0,0 +1,114 @@ +prophesize(HttpKernelInterface::class); + $request = Request::create('/example', 'GET'); + + $route_provider = $this->prophesize(RouteProviderInterface::class); + $route_provider->getRouteCollectionForRequest($request)->shouldNotBeCalled(); + + $subscriber = new OptionsRequestListener($route_provider->reveal()); + $event = new GetResponseEvent($kernel->reveal(), $request, HttpKernelInterface::MASTER_REQUEST); + $subscriber->onRequest($event); + + $this->assertFalse($event->hasResponse()); + } + + /** + * @covers ::onRequest + */ + public function testWithoutMatchingRoutes() { + $kernel = $this->prophesize(HttpKernelInterface::class); + $request = Request::create('/example', 'OPTIONS'); + + $route_provider = $this->prophesize(RouteProviderInterface::class); + $route_provider->getRouteCollectionForRequest($request)->willReturn(new RouteCollection())->shouldBeCalled(); + + $subscriber = new OptionsRequestListener($route_provider->reveal()); + $event = new GetResponseEvent($kernel->reveal(), $request, HttpKernelInterface::MASTER_REQUEST); + $subscriber->onRequest($event); + + $this->assertFalse($event->hasResponse()); + } + + /** + * @covers ::onRequest + * @dataProvider providerTestOnRequestWithOptionsRequest + */ + public function testWithOptionsRequest(RouteCollection $collection, $expected_header) { + $kernel = $this->prophesize(HttpKernelInterface::class); + $request = Request::create('/example', 'OPTIONS'); + + $route_provider = $this->prophesize(RouteProviderInterface::class); + $route_provider->getRouteCollectionForRequest($request)->willReturn($collection)->shouldBeCalled(); + + $subscriber = new OptionsRequestListener($route_provider->reveal()); + $event = new GetResponseEvent($kernel->reveal(), $request, HttpKernelInterface::MASTER_REQUEST); + $subscriber->onRequest($event); + + $this->assertTrue($event->hasResponse()); + $response = $event->getResponse(); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals($expected_header, $response->headers->get('Allow')); + } + + public function providerTestOnRequestWithOptionsRequest() { + $data = []; + + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method) { + $collection = new RouteCollection(); + $collection->add('example.1', new Route('/example', [], [], [], '', [], [$method])); + $data['one_route_' . $method] = [$collection, $method]; + } + + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method_a) { + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method_b) { + if ($method_a != $method_b) { + $collection = new RouteCollection(); + $collection->add('example.1', new Route('/example', [], [], [], '', [], [$method_a, $method_b])); + $data['one_route_' . $method_a . '_' . $method_b] = [$collection, $method_a . ', ' . $method_b]; + } + } + } + + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method_a) { + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method_b) { + foreach (['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as $method_c) { + $collection = new RouteCollection(); + $collection->add('example.1', new Route('/example', [], [], [], '', [], [$method_a])); + $collection->add('example.2', new Route('/example', [], [], [], '', [], [$method_a, $method_b])); + $collection->add('example.3', new Route('/example', [], [], [], '', [], [$method_b, $method_c])); + $methods = array_unique([$method_a, $method_b, $method_c]); + $data['multiple_routes_' . $method_a . '_' . $method_b . '_' . $method_c] = [$collection, implode(', ', $methods)]; + } + } + } + + return $data; + } + +}