Loading core/core.services.yml +2 −2 Original line number Diff line number Diff line Loading @@ -1353,8 +1353,8 @@ services: arguments: ['@main_content_renderer.ajax', '@current_route_match', '@callable_resolver'] Drupal\Core\Form\FormAjaxResponseBuilderInterface: '@form_ajax_response_builder' router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener arguments: ['@router', '@request_stack', '@router.request_context', NULL] class: Drupal\Core\Http\EventListener\RouterListener arguments: ['@router', '@request_stack', '@router.request_context'] options_request_listener: class: Drupal\Core\EventSubscriber\OptionsRequestSubscriber arguments: ['@router.route_provider'] Loading core/lib/Drupal/Core/Http/EventListener/RouterListener.php 0 → 100644 +170 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Http\EventListener; use Drupal\Core\Http\Exception\CacheableNotFoundHttpException; use Drupal\Core\Routing\Exception\CacheableResourceNotFoundException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; use Symfony\Component\Routing\RequestContext; /** * Sets request attributes based on a matching route. * * Also initializes the context from the request. * * In contrast to Symfony's RouterListener this converts a * CacheableResourceNoFoundException (thrown by Drupal's Router) into a * CacheableNotFoundHttpException. (Symfony already does the conversion of * ResourceNoFoundException into NotFoundHttpException.) Additionally, the * following functional things are changed (in addition to the code-style) to * reduce the complexity: * - It does not support URL matchers, only request matchers * - It does not support logging * - It does not return a welcome response * * @see \Symfony\Component\HttpKernel\EventListener\RouterListener * @see \Drupal\Core\Routing\Exception\CacheableResourceNotFoundException * @see \Drupal\Core\Routing\Router::matchRequest() */ class RouterListener implements EventSubscriberInterface { public function __construct( protected RequestMatcherInterface $matcher, protected RequestStack $requestStack, protected RequestContext $context, ) {} /** * Sets the request on the request context. * * @param ?\Symfony\Component\HttpFoundation\Request $request * The request. */ protected function setCurrentRequest(?Request $request): void { if ($request !== NULL) { try { $this->context->fromRequest($request); } catch (\UnexpectedValueException $e) { throw new BadRequestHttpException($e->getMessage(), $e, $e->getCode()); } } } /** * Resets the routing context after a sub-request is done. */ public function onKernelFinishRequest(): void { $this->setCurrentRequest($this->requestStack->getParentRequest()); } /** * Sets request attributes based on a matching route. * * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event * The request event. */ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $this->setCurrentRequest($request); if ($request->attributes->has('_controller')) { // Routing is already done. return; } // Add routing attributes based on the request. try { $parameters = $this->matcher->matchRequest($request); $attributes = $parameters; $mapping = $parameters['_route_mapping'] ?? FALSE; if ($mapping) { unset($parameters['_route_mapping']); $mappedAttributes = []; $attributes = []; foreach ($parameters as $parameter => $value) { if (!isset($mapping[$parameter])) { $attribute = $parameter; } elseif (is_array($mapping[$parameter])) { [$attribute, $parameter] = $mapping[$parameter]; $mappedAttributes[$attribute] = ''; } else { $attribute = $mapping[$parameter]; } if (!isset($mappedAttributes[$attribute])) { $attributes[$attribute] = $value; $mappedAttributes[$attribute] = $parameter; } elseif ($mappedAttributes[$attribute] !== '') { $attributes[$attribute] = [ $mappedAttributes[$attribute] => $attributes[$attribute], $parameter => $value, ]; $mappedAttributes[$attribute] = ''; } else { $attributes[$attribute][$parameter] = $value; } } $attributes['_route_mapping'] = $mapping; } $request->attributes->add($attributes); unset($parameters['_route'], $parameters['_controller']); $request->attributes->set('_route_params', $parameters); } catch (CacheableResourceNotFoundException $exception) { $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); $referer = $request->headers->get('referer'); if ($referer) { $message .= sprintf(' (from "%s")', $referer); } throw new CacheableNotFoundHttpException($exception, $message, $exception); } catch (ResourceNotFoundException $exception) { $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); $referer = $request->headers->get('referer'); if ($referer) { $message .= sprintf(' (from "%s")', $referer); } throw new NotFoundHttpException($message, $exception); } catch (MethodNotAllowedException $exception) { $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getUriForPath($request->getPathInfo()), implode(', ', $exception->getAllowedMethods())); throw new MethodNotAllowedHttpException($exception->getAllowedMethods(), $message, $exception); } } /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [['onKernelRequest', 32]], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]], ]; } } core/lib/Drupal/Core/Routing/Exception/CacheableResourceNotFoundException.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Routing\Exception; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableDependencyTrait; use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * A cacheable ResourceNotFoundException. */ class CacheableResourceNotFoundException extends ResourceNotFoundException implements CacheableDependencyInterface { use CacheableDependencyTrait; /** * {@inheritdoc} */ public function __construct(CacheableDependencyInterface $cacheability, $message = '', $code = 0, ?\Throwable $previous = NULL) { $this->setCacheability($cacheability); parent::__construct($message, $code, $previous); } } core/lib/Drupal/Core/Routing/Router.php +12 −1 Original line number Diff line number Diff line Loading @@ -2,8 +2,10 @@ namespace Drupal\Core\Routing; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; use Drupal\Core\Routing\Exception\CacheableResourceNotFoundException; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\MethodNotAllowedException; Loading Loading @@ -111,7 +113,16 @@ public function match($pathinfo): array { public function matchRequest(Request $request): array { $collection = $this->getInitialRouteCollection($request); if ($collection->count() === 0) { throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath())); // Allow caching the empty route lookup. Note that even though this result // inherently depends on the path we do not bubble any cache contexts // because: // 1. This is outside the scope of Dynamic Page Cache since we have not // actually resolved a route // 2. Page Cache does not use cache contexts (and caches per URL anyway) throw new CacheableResourceNotFoundException( new CacheableMetadata(), sprintf('No routes found for "%s".', $this->currentPath->getPath()), ); } $collection = $this->applyRouteFilters($collection, $request); $collection = $this->applyFitOrder($collection); Loading core/modules/page_cache/tests/src/Functional/PageCacheTest.php +6 −1 Original line number Diff line number Diff line Loading @@ -386,7 +386,9 @@ protected function testPageCacheAnonymous403404(): void { $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', '4xx-response'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', '4xx-response config:user.role.anonymous http_response rendered'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Contexts', 'languages:language_interface theme url.query_args:_wrapper_format user.permissions user.roles:authenticated'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)'); $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); Loading Loading @@ -443,6 +445,9 @@ protected function testPageCacheAnonymous403404(): void { $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', '4xx-response config:user.role.anonymous http_response rendered'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Contexts', 'languages:language_interface theme url.query_args:_wrapper_format user.permissions user.roles:authenticated'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)'); } // Restore 403 and 404 caching. $settings['settings']['cache_ttl_4xx'] = (object) [ Loading Loading
core/core.services.yml +2 −2 Original line number Diff line number Diff line Loading @@ -1353,8 +1353,8 @@ services: arguments: ['@main_content_renderer.ajax', '@current_route_match', '@callable_resolver'] Drupal\Core\Form\FormAjaxResponseBuilderInterface: '@form_ajax_response_builder' router_listener: class: Symfony\Component\HttpKernel\EventListener\RouterListener arguments: ['@router', '@request_stack', '@router.request_context', NULL] class: Drupal\Core\Http\EventListener\RouterListener arguments: ['@router', '@request_stack', '@router.request_context'] options_request_listener: class: Drupal\Core\EventSubscriber\OptionsRequestSubscriber arguments: ['@router.route_provider'] Loading
core/lib/Drupal/Core/Http/EventListener/RouterListener.php 0 → 100644 +170 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Http\EventListener; use Drupal\Core\Http\Exception\CacheableNotFoundHttpException; use Drupal\Core\Routing\Exception\CacheableResourceNotFoundException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; use Symfony\Component\Routing\RequestContext; /** * Sets request attributes based on a matching route. * * Also initializes the context from the request. * * In contrast to Symfony's RouterListener this converts a * CacheableResourceNoFoundException (thrown by Drupal's Router) into a * CacheableNotFoundHttpException. (Symfony already does the conversion of * ResourceNoFoundException into NotFoundHttpException.) Additionally, the * following functional things are changed (in addition to the code-style) to * reduce the complexity: * - It does not support URL matchers, only request matchers * - It does not support logging * - It does not return a welcome response * * @see \Symfony\Component\HttpKernel\EventListener\RouterListener * @see \Drupal\Core\Routing\Exception\CacheableResourceNotFoundException * @see \Drupal\Core\Routing\Router::matchRequest() */ class RouterListener implements EventSubscriberInterface { public function __construct( protected RequestMatcherInterface $matcher, protected RequestStack $requestStack, protected RequestContext $context, ) {} /** * Sets the request on the request context. * * @param ?\Symfony\Component\HttpFoundation\Request $request * The request. */ protected function setCurrentRequest(?Request $request): void { if ($request !== NULL) { try { $this->context->fromRequest($request); } catch (\UnexpectedValueException $e) { throw new BadRequestHttpException($e->getMessage(), $e, $e->getCode()); } } } /** * Resets the routing context after a sub-request is done. */ public function onKernelFinishRequest(): void { $this->setCurrentRequest($this->requestStack->getParentRequest()); } /** * Sets request attributes based on a matching route. * * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event * The request event. */ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $this->setCurrentRequest($request); if ($request->attributes->has('_controller')) { // Routing is already done. return; } // Add routing attributes based on the request. try { $parameters = $this->matcher->matchRequest($request); $attributes = $parameters; $mapping = $parameters['_route_mapping'] ?? FALSE; if ($mapping) { unset($parameters['_route_mapping']); $mappedAttributes = []; $attributes = []; foreach ($parameters as $parameter => $value) { if (!isset($mapping[$parameter])) { $attribute = $parameter; } elseif (is_array($mapping[$parameter])) { [$attribute, $parameter] = $mapping[$parameter]; $mappedAttributes[$attribute] = ''; } else { $attribute = $mapping[$parameter]; } if (!isset($mappedAttributes[$attribute])) { $attributes[$attribute] = $value; $mappedAttributes[$attribute] = $parameter; } elseif ($mappedAttributes[$attribute] !== '') { $attributes[$attribute] = [ $mappedAttributes[$attribute] => $attributes[$attribute], $parameter => $value, ]; $mappedAttributes[$attribute] = ''; } else { $attributes[$attribute][$parameter] = $value; } } $attributes['_route_mapping'] = $mapping; } $request->attributes->add($attributes); unset($parameters['_route'], $parameters['_controller']); $request->attributes->set('_route_params', $parameters); } catch (CacheableResourceNotFoundException $exception) { $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); $referer = $request->headers->get('referer'); if ($referer) { $message .= sprintf(' (from "%s")', $referer); } throw new CacheableNotFoundHttpException($exception, $message, $exception); } catch (ResourceNotFoundException $exception) { $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); $referer = $request->headers->get('referer'); if ($referer) { $message .= sprintf(' (from "%s")', $referer); } throw new NotFoundHttpException($message, $exception); } catch (MethodNotAllowedException $exception) { $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getUriForPath($request->getPathInfo()), implode(', ', $exception->getAllowedMethods())); throw new MethodNotAllowedHttpException($exception->getAllowedMethods(), $message, $exception); } } /** * {@inheritdoc} */ public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [['onKernelRequest', 32]], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]], ]; } }
core/lib/Drupal/Core/Routing/Exception/CacheableResourceNotFoundException.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Routing\Exception; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableDependencyTrait; use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * A cacheable ResourceNotFoundException. */ class CacheableResourceNotFoundException extends ResourceNotFoundException implements CacheableDependencyInterface { use CacheableDependencyTrait; /** * {@inheritdoc} */ public function __construct(CacheableDependencyInterface $cacheability, $message = '', $code = 0, ?\Throwable $previous = NULL) { $this->setCacheability($cacheability); parent::__construct($message, $code, $previous); } }
core/lib/Drupal/Core/Routing/Router.php +12 −1 Original line number Diff line number Diff line Loading @@ -2,8 +2,10 @@ namespace Drupal\Core\Routing; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; use Drupal\Core\Routing\Exception\CacheableResourceNotFoundException; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\MethodNotAllowedException; Loading Loading @@ -111,7 +113,16 @@ public function match($pathinfo): array { public function matchRequest(Request $request): array { $collection = $this->getInitialRouteCollection($request); if ($collection->count() === 0) { throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $this->currentPath->getPath())); // Allow caching the empty route lookup. Note that even though this result // inherently depends on the path we do not bubble any cache contexts // because: // 1. This is outside the scope of Dynamic Page Cache since we have not // actually resolved a route // 2. Page Cache does not use cache contexts (and caches per URL anyway) throw new CacheableResourceNotFoundException( new CacheableMetadata(), sprintf('No routes found for "%s".', $this->currentPath->getPath()), ); } $collection = $this->applyRouteFilters($collection, $request); $collection = $this->applyFitOrder($collection); Loading
core/modules/page_cache/tests/src/Functional/PageCacheTest.php +6 −1 Original line number Diff line number Diff line Loading @@ -386,7 +386,9 @@ protected function testPageCacheAnonymous403404(): void { $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', '4xx-response'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', '4xx-response config:user.role.anonymous http_response rendered'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Contexts', 'languages:language_interface theme url.query_args:_wrapper_format user.permissions user.roles:authenticated'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)'); $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT'); Loading Loading @@ -443,6 +445,9 @@ protected function testPageCacheAnonymous403404(): void { $this->drupalGet($content_url); $this->assertSession()->statusCodeEquals($code); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', '4xx-response config:user.role.anonymous http_response rendered'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Contexts', 'languages:language_interface theme url.query_args:_wrapper_format user.permissions user.roles:authenticated'); $this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)'); } // Restore 403 and 404 caching. $settings['settings']['cache_ttl_4xx'] = (object) [ Loading