diff --git a/core/core.services.yml b/core/core.services.yml index e5d8a263133ac60af129ac473a6df295049f376d..657920409ce7a2be1212842bf55f5e1b7b6bcacf 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -949,6 +949,14 @@ services: tags: - { name: event_subscriber } arguments: ['@path.alias_manager', '@path_processor_manager', '@path.current'] + route_access_response_subscriber: + class: Drupal\Core\EventSubscriber\RouteAccessResponseSubscriber + tags: + - { name: event_subscriber } + client_error_response_subscriber: + class: Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber + tags: + - { name: event_subscriber } anonymous_user_response_subscriber: class: Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber tags: diff --git a/core/lib/Drupal/Core/Cache/CacheableMetadata.php b/core/lib/Drupal/Core/Cache/CacheableMetadata.php index d21bd650a082e7b19218ee012f63a1fe36182b36..0a22f56b420c4d03feeaaa96a76531a8511891a5 100644 --- a/core/lib/Drupal/Core/Cache/CacheableMetadata.php +++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php @@ -178,7 +178,10 @@ public static function createFromRenderArray(array $build) { * Creates a CacheableMetadata object from a depended object. * * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object - * The object whose cacheability metadata to retrieve. + * The object whose cacheability metadata to retrieve. If it implements + * CacheableDependencyInterface, its cacheability metadata will be used, + * otherwise, the passed in object must be assumed to be uncacheable, so + * max-age 0 is set. * * @return static */ diff --git a/core/lib/Drupal/Core/Cache/CacheableResponse.php b/core/lib/Drupal/Core/Cache/CacheableResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..2acac21c350bac3986c8862f574493d4cbb7b8d9 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheableResponse.php @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Cache\CacheableResponse. + */ + +namespace Drupal\Core\Cache; + +use Symfony\Component\HttpFoundation\Response; + +/** + * A response that contains and can expose cacheability metadata. + * + * Supports Drupal's caching concepts: cache tags for invalidation and cache + * contexts for variations. + * + * @see \Drupal\Core\Cache\Cache + * @see \Drupal\Core\Cache\CacheableMetadata + * @see \Drupal\Core\Cache\CacheableResponseTrait + */ +class CacheableResponse extends Response implements CacheableResponseInterface { + + use CacheableResponseTrait; + +} diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..0bca8d1fcff15af6c012b567949c24946b4f4a3a --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Cache\CacheableResponseInterface. + */ + +namespace Drupal\Core\Cache; + +/** + * Defines an interface for responses that can expose cacheability metadata. + * + * @see \Drupal\Core\Cache\CacheableResponseTrait + */ +interface CacheableResponseInterface { + + /** + * Adds a dependency on an object: merges its cacheability metadata. + * + * E.g. when a response depends on some configuration, an entity, or an access + * result, we must make sure their cacheability metadata is present on the + * response. This method makes doing that simple. + * + * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency + * The dependency. If the object implements CacheableDependencyInterface, + * then its cacheability metadata will be used. Otherwise, the passed in + * object must be assumed to be uncacheable, so max-age 0 is set. + * + * @return $this + * + * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject() + */ + public function addCacheableDependency($dependency); + + /** + * Returns the cacheability metadata for this response. + * + * @return \Drupal\Core\Cache\CacheableMetadata + */ + public function getCacheableMetadata(); + +} diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..2940c1309736486cfd0205823098872172a28dff --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\Core\Cache; + +/** + * Provides an implementation of CacheableResponseInterface. + * + * @see \Drupal\Core\Cache\CacheableResponseInterface + */ +trait CacheableResponseTrait { + + /** + * The cacheability metadata. + * + * @var \Drupal\Core\Cache\CacheableMetadata + */ + protected $cacheabilityMetadata; + + /** + * {@inheritdoc} + */ + public function addCacheableDependency($dependency) { + // A trait doesn't have a constructor, so initialize the cacheability + // metadata if that hasn't happened yet. + if (!isset($this->cacheabilityMetadata)) { + $this->cacheabilityMetadata = new CacheableMetadata(); + } + + $this->cacheabilityMetadata = $this->cacheabilityMetadata->merge(CacheableMetadata::createFromObject($dependency)); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata() { + // A trait doesn't have a constructor, so initialize the cacheability + // metadata if that hasn't happened yet. + if (!isset($this->cacheabilityMetadata)) { + $this->cacheabilityMetadata = new CacheableMetadata(); + } + + return $this->cacheabilityMetadata; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php index fea9d9c36cbd262737991646aaaef2e31fd04a4a..ae4b46daed0311d6e6ea5f4c739fe08aaceef1bf 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php @@ -169,13 +169,13 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt // Associate the cache tags for the field definition & field storage // definition. $field_definition = $this->getFieldDefinition($name); - $this->renderer->addDependency($form[$name], $field_definition); - $this->renderer->addDependency($form[$name], $field_definition->getFieldStorageDefinition()); + $this->renderer->addCacheableDependency($form[$name], $field_definition); + $this->renderer->addCacheableDependency($form[$name], $field_definition->getFieldStorageDefinition()); } } // Associate the cache tags for the form display. - $this->renderer->addDependency($form, $this); + $this->renderer->addCacheableDependency($form, $this); // Add a process callback so we can assign weights and hide extra fields. $form['#process'][] = array($this, 'processForm'); diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php index 6712fb57146aa3dffd2d3e8fcf43c86807dcdd50..5b28f764f72bcb19df60e83350f78ac6010c851d 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php @@ -242,7 +242,7 @@ public function buildMultiple(array $entities) { $field_access = $items->access('view', NULL, TRUE); $build_list[$id][$name] = $field_access->isAllowed() ? $formatter->view($items) : []; // Apply the field access cacheability metadata to the render array. - $this->renderer->addDependency($build_list[$id][$name], $field_access); + $this->renderer->addCacheableDependency($build_list[$id][$name], $field_access); } } } diff --git a/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php index 3270d07b8f2a52768b9b050a2dff2f8489885d0b..0e646986c25fdbf2c5ed494666c6fd6ae7311b5b 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php @@ -8,6 +8,8 @@ namespace Drupal\Core\EventSubscriber; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -52,6 +54,9 @@ public function onRespond(FilterResponseEvent $event) { } $response = $event->getResponse(); + if (!$response instanceof CacheableResponseInterface) { + return; + } // The 'user.permissions' cache context ensures that if the permissions for // a role are modified, users are not served stale render cache content. @@ -60,14 +65,10 @@ public function onRespond(FilterResponseEvent $event) { // be invalidated. Therefore, when varying by permissions and the current // user is the anonymous user, also add the cache tag for the 'anonymous' // role. - $cache_contexts = $response->headers->get('X-Drupal-Cache-Contexts'); - if ($cache_contexts && in_array('user.permissions', explode(' ', $cache_contexts))) { - $cache_tags = ['config:user.role.anonymous']; - if ($response->headers->get('X-Drupal-Cache-Tags')) { - $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); - $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags); - } - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags)); + if (in_array('user.permissions', $response->getCacheableMetadata()->getCacheContexts())) { + $per_permissions_response_for_anon = new CacheableMetadata(); + $per_permissions_response_for_anon->setCacheTags(['config:user.role.anonymous']); + $response->addCacheableDependency($per_permissions_response_for_anon); } } @@ -78,7 +79,10 @@ public function onRespond(FilterResponseEvent $event) { * An array of event listener definitions. */ public static function getSubscribedEvents() { - $events[KernelEvents::RESPONSE][] = ['onRespond', -5]; + // Priority 5, so that it runs before FinishResponseSubscriber, but after + // event subscribers that add the associated cacheability metadata (which + // have priority 10). This one is conditional, so must run after those. + $events[KernelEvents::RESPONSE][] = ['onRespond', 5]; return $events; } diff --git a/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..5b4f0bdc6d1422396500a3cb1596e895e100c217 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\CacheableResponseSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheableResponseInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Response subscriber to set the '4xx-response' cache tag on 4xx responses. + */ +class ClientErrorResponseSubscriber implements EventSubscriberInterface { + + /** + * Sets the '4xx-response' cache tag on 4xx responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $response = $event->getResponse(); + if (!$response instanceof CacheableResponseInterface) { + return; + } + + if ($response->isClientError()) { + $http_4xx_response_cacheability = new CacheableMetadata(); + $http_4xx_response_cacheability->setCacheTags(['4xx-response']); + $response->addCacheableDependency($http_4xx_response_cacheability); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Priority 10, so that it runs before FinishResponseSubscriber, which will + // expose the cacheability metadata in the form of headers. + $events[KernelEvents::RESPONSE][] = ['onRespond', 10]; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 7fd88700b4b5784d8ca192afe8e8330067b35f0e..66283d8653f5ecfedfadf5a3761eceeb928a1c34 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -10,6 +10,8 @@ use Drupal\Component\Datetime\DateTimePlus; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Cache\CacheContextsManager; use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactoryInterface; @@ -133,19 +135,12 @@ public function onRespond(FilterResponseEvent $event) { $response->headers->set($name, $value, FALSE); } - // Apply the request's access result cacheability metadata, if it has any. - $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT); - if ($access_result instanceof CacheableDependencyInterface) { - $this->updateDrupalCacheHeaders($response, $access_result); - } - // Add a cache tag to any 4xx response. - if ($response->isClientError()) { - $cache_tags = ['4xx-response']; - if ($response->headers->has('X-Drupal-Cache-Tags')) { - $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); - $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags); - } - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags)); + // Expose the cache contexts and cache tags associated with this page in a + // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. + if ($response instanceof CacheableResponseInterface) { + $response_cacheability = $response->getCacheableMetadata(); + $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); + $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); } $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); @@ -167,30 +162,6 @@ public function onRespond(FilterResponseEvent $event) { } } - /** - * Updates Drupal's cache headers using the route's cacheable access result. - * - * @param \Symfony\Component\HttpFoundation\Response $response - * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheable_access_result - */ - protected function updateDrupalCacheHeaders(Response $response, CacheableDependencyInterface $cacheable_access_result) { - // X-Drupal-Cache-Tags - $cache_tags = $cacheable_access_result->getCacheTags(); - if ($response->headers->has('X-Drupal-Cache-Tags')) { - $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); - $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags); - } - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags)); - - // X-Drupal-Cache-Contexts - $cache_contexts = $cacheable_access_result->getCacheContexts(); - if ($response->headers->has('X-Drupal-Cache-Contexts')) { - $existing_cache_contexts = explode(' ', $response->headers->get('X-Drupal-Cache-Contexts')); - $cache_contexts = Cache::mergeContexts($existing_cache_contexts, $cache_contexts); - } - $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($cache_contexts))); - } - /** * Determine whether the given response has a custom Cache-Control header. * diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..76784e742fef17ef6fdbf062eca3686ef0b246c2 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php @@ -0,0 +1,62 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\CacheableResponseSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Cache\CacheableResponseInterface; +use Drupal\Core\Routing\AccessAwareRouterInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Response subscriber to bubble the route's access result's cacheability. + * + * During routing, access checking is performed. The corresponding access result + * is stored in the Request object's attributes, just like the matching route + * object is. In case of a cacheable response, the route's access result also + * determined the content of the response, and therefore the cacheability of the + * route's access result should also be applied to the resulting response. + * + * @see \Drupal\Core\Routing\AccessAwareRouterInterface::ACCESS_RESULT + * @see \Drupal\Core\Routing\AccessAwareRouter::matchRequest() + * @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess() + */ +class RouteAccessResponseSubscriber implements EventSubscriberInterface { + + /** + * Bubbles the route's access result' cacheability metadata. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $response = $event->getResponse(); + if (!$response instanceof CacheableResponseInterface) { + return; + } + + $request = $event->getRequest(); + $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT); + $response->addCacheableDependency($access_result); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Priority 10, so that it runs before FinishResponseSubscriber, which will + // expose the cacheability metadata in the form of headers. + $events[KernelEvents::RESPONSE][] = ['onRespond', 10]; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index 9f73f1176abc9e43c799e78e5c7ec7bce2f0f482..4d8da4de8db7ec34521f31d5752bb5293df1ffe9 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -10,6 +10,8 @@ use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Cache\CacheContextsManager; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Display\PageVariantInterface; @@ -161,27 +163,27 @@ public function renderResponse(array $main_content, Request $request, RouteMatch } $content = $this->renderer->render($html); - // Expose the cache contexts and cache tags associated with this page in a - // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. Also - // associate the "rendered" cache tag. This allows us to invalidate the - // entire render cache, regardless of the cache bin. - $cache_contexts = []; - $cache_tags = ['rendered']; + // Set the generator in the HTTP header. + list($version) = explode('.', \Drupal::VERSION, 2); + + $response = new CacheableResponse($content, 200,[ + 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)' + ]); + + // Bubble the cacheability metadata associated with the rendered render + // arrays to the response. foreach (['page_top', 'page', 'page_bottom'] as $region) { if (isset($html[$region])) { - $cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']); - $cache_tags = Cache::mergeTags($cache_tags, $html[$region]['#cache']['tags']); + $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($html[$region])); } } - // Set the generator in the HTTP header. - list($version) = explode('.', \Drupal::VERSION, 2); + // Also associate the "rendered" cache tag. This allows us to invalidate the + // entire render cache, regardless of the cache bin. + $default = new CacheableMetadata(); + $default->setCacheTags(['rendered']); + $response->addCacheableDependency($default); - $response = new Response($content, 200,[ - 'X-Drupal-Cache-Tags' => implode(' ', $cache_tags), - 'X-Drupal-Cache-Contexts' => implode(' ', $this->cacheContextsManager->optimizeTokens($cache_contexts)), - 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)' - ]); return $response; } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index ac1bd6e7905311c05d623c84898e82d11816f9c7..b969a518b48d0eaeecf512148dbf9d7544c11074 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -497,7 +497,7 @@ public function mergeBubbleableMetadata(array $a, array $b) { /** * {@inheritdoc} */ - public function addDependency(array &$elements, $dependency) { + public function addCacheableDependency(array &$elements, $dependency) { $meta_a = CacheableMetadata::createFromRenderArray($elements); $meta_b = CacheableMetadata::createFromObject($dependency); $meta_a->merge($meta_b)->applyTo($elements); diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 7e97526d88262bff00c8fca9af8b0c23a9a3ba19..0af066e28a32b1f41c6b5ae804f43fd18b58fe31 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -334,9 +334,13 @@ public function mergeBubbleableMetadata(array $a, array $b); * @param array &$elements * The render array to update. * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency - * The dependency. + * The dependency. If the object implements CacheableDependencyInterface, + * then its cacheability metadata will be used. Otherwise, the passed in + * object must be assumed to be uncacheable, so max-age 0 is set. + * + * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject() */ - public function addDependency(array &$elements, $dependency); + public function addCacheableDependency(array &$elements, $dependency); /** * Merges two attachments arrays (which live under the '#attached' key). diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php index 41daf56383155cccaea5c0bc9afaf56f101e400c..aab9611f8017b56ef9645c9c76065b7fafe9c513 100644 --- a/core/modules/book/src/BookManager.php +++ b/core/modules/book/src/BookManager.php @@ -362,7 +362,7 @@ protected function addParentSelectFormElements(array $book_link) { '#suffix' => '</div>', ); } - $this->renderer->addDependency($form, $config); + $this->renderer->addCacheableDependency($form, $config); return $form; } diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php index 10c2152d34a935bbfe5bed5c0fbb9541a996dfb1..9f4751923da07e36e2164a0a449a0c463a817301 100644 --- a/core/modules/comment/src/CommentForm.php +++ b/core/modules/comment/src/CommentForm.php @@ -225,9 +225,9 @@ public function form(array $form, FormStateInterface $form_state) { '#access' => $is_admin, ); - $this->renderer->addDependency($form, $config); + $this->renderer->addCacheableDependency($form, $config); // The form depends on the field definition. - $this->renderer->addDependency($form, $field_definition->getConfig($entity->bundle())); + $this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle())); return parent::form($form, $form_state, $comment); } diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php index 44d23df46fa0824c8453631ae08344b7f69ba24a..7233bb8747931871085369398defc763b79f8e4b 100644 --- a/core/modules/contact/src/Controller/ContactController.php +++ b/core/modules/contact/src/Controller/ContactController.php @@ -89,7 +89,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) { $form = $this->entityFormBuilder()->getForm($message); $form['#title'] = SafeMarkup::checkPlain($contact_form->label()); $form['#cache']['contexts'][] = 'user.permissions'; - $this->renderer->addDependency($form, $config); + $this->renderer->addCacheableDependency($form, $config); return $form; } diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php index 17c0454791e53f976cdc4c392e876d955116ca21..20a13de4aa6ca54887402bd05f56bb9efd4eab54 100644 --- a/core/modules/forum/src/Controller/ForumController.php +++ b/core/modules/forum/src/Controller/ForumController.php @@ -203,7 +203,7 @@ protected function build($forums, TermInterface $term, $topics = array(), $paren if (empty($term->forum_container->value)) { $build['#attached']['feed'][] = array('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->getName()); } - $this->renderer->addDependency($build, $config); + $this->renderer->addCacheableDependency($build, $config); return [ 'action' => $this->buildActionLinks($config->get('vocabulary'), $term), diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 2c5eea96d4c0b15aa5cb25df9db7b91eecfa707e..448a353b66124b1736163001d0a4004b17451ebe 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -55,7 +55,11 @@ public function get(EntityInterface $entity) { } } - return new ResourceResponse($entity, 200, ['X-Drupal-Cache-Tags' => implode(' ', $entity->getCacheTags())]); + $response = new ResourceResponse($entity, 200); + // Make the response use the entity's cacheability metadata. + // @todo include access cacheability metadata, for the access checks above. + $response->addCacheableDependency($entity); + return $response; } /** diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index b962b3266f10b8341a49b0270143deb37ca73d55..f8f408f26b9e8af933be00e9010c6d37c4ddf463 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -109,14 +109,8 @@ public function handle(RouteMatchInterface $route_match, Request $request) { $output = $serializer->serialize($data, $format); $response->setContent($output); $response->headers->set('Content-Type', $request->getMimeType($format)); - // Add cache tags, but do not overwrite any that exist already on the - // response object. - $cache_tags = $this->container->get('config.factory')->get('rest.settings')->getCacheTags(); - if ($response->headers->has('X-Drupal-Cache-Tags')) { - $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); - $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags); - } - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags)); + // Add rest settings config's cache tags. + $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings')); } return $response; } diff --git a/core/modules/rest/src/ResourceResponse.php b/core/modules/rest/src/ResourceResponse.php index 23e9d47cc7ba65a2ac87ca8951d8d917455eeab6..2919fb16cde27458dd69ce129b273bfbd210fbde 100644 --- a/core/modules/rest/src/ResourceResponse.php +++ b/core/modules/rest/src/ResourceResponse.php @@ -7,6 +7,8 @@ namespace Drupal\rest; +use Drupal\Core\Cache\CacheableResponseInterface; +use Drupal\Core\Cache\CacheableResponseTrait; use Symfony\Component\HttpFoundation\Response; /** @@ -17,7 +19,9 @@ * string or an object with a __toString() method, which is not a requirement * for data used here. */ -class ResourceResponse extends Response { +class ResourceResponse extends Response implements CacheableResponseInterface { + + use CacheableResponseTrait; /** * Response data that should be serialized. diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index e0a4f3407ed65d92c341199e156ed5160c8220ee..e7bc5f058938d49783fe16bbb217297639a30e1f 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -71,11 +71,21 @@ public function testFinishResponseSubscriber() { // 3. controller result: Response object, globally cacheable route access. $this->drupalGet('router_test/test1'); $headers = $this->drupalGetHeaders(); - $this->assertEqual($headers['x-drupal-cache-contexts'], ''); - $this->assertEqual($headers['x-drupal-cache-tags'], ''); + $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); + $this->assertFalse(isset($headers['x-drupal-cache-tags'])); // 4. controller result: Response object, per-role cacheable route access. $this->drupalGet('router_test/test20'); $headers = $this->drupalGetHeaders(); + $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); + $this->assertFalse(isset($headers['x-drupal-cache-tags'])); + // 5. controller result: CacheableResponse object, globally cacheable route access. + $this->drupalGet('router_test/test21'); + $headers = $this->drupalGetHeaders(); + $this->assertEqual($headers['x-drupal-cache-contexts'], ''); + $this->assertEqual($headers['x-drupal-cache-tags'], ''); + // 6. controller result: CacheableResponse object, per-role cacheable route access. + $this->drupalGet('router_test/test22'); + $headers = $this->drupalGetHeaders(); $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles'); $this->assertEqual($headers['x-drupal-cache-tags'], ''); } diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml index 08dd057379ae04bcd5e4b4348570afffede545af..5debfc219c08ed961254a96ead4b39d0b89a86b5 100644 --- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml @@ -127,6 +127,20 @@ router_test.20: requirements: _role: 'anonymous' +router_test.21: + path: '/router_test/test21' + defaults: + _controller: '\Drupal\router_test\TestControllers::test21' + requirements: + _access: 'TRUE' + +router_test.22: + path: '/router_test/test22' + defaults: + _controller: '\Drupal\router_test\TestControllers::test21' + requirements: + _role: 'anonymous' + router_test.hierarchy_parent: path: '/menu-test/parent' defaults: diff --git a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php index f2a1fd201dcc84e0884f43534df7bda6c78fc8c3..ae3b050290e12fdc3458a322bcb1b907d219c05c 100644 --- a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php +++ b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php @@ -7,6 +7,7 @@ namespace Drupal\router_test; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\user\UserInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; @@ -97,6 +98,10 @@ public function test18() { ]; } + public function test21() { + return new CacheableResponse('test21'); + } + /** * Throws an exception. * diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index 87d813d9b22eca943aa9ddcac699b9d8b4152b8f..5ac7ca01c6de99d9913a860b1d7415c0333c587f 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -287,7 +287,7 @@ public function permissionDependentContent() { // The content depends on the access result. $access = AccessResult::allowedIfHasPermission($this->currentUser, 'pet llamas'); - $this->renderer->addDependency($build, $access); + $this->renderer->addCacheableDependency($build, $access); // Build the content. if ($access->isAllowed()) { diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php index 76736122f300f6946bb148a60247e69a20f7b5ef..94ec9cff04cbe0f2854bc242689dfc38ef8d4988 100644 --- a/core/modules/user/src/Form/UserLoginForm.php +++ b/core/modules/user/src/Form/UserLoginForm.php @@ -124,7 +124,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['#validate'][] = '::validateAuthentication'; $form['#validate'][] = '::validateFinal'; - $this->renderer->addDependency($form, $config); + $this->renderer->addCacheableDependency($form, $config); return $form; } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 617a44986457c851755237a47859f79c3eca441e..e8368b230dd56afbb062177a903803ee5992034b 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -622,16 +622,16 @@ public function providerTestRenderCacheMaxAge() { } /** - * @covers ::addDependency + * @covers ::addCacheableDependency * - * @dataProvider providerTestAddDependency + * @dataProvider providerTestAddCacheableDependency */ - public function testAddDependency(array $build, $object, array $expected) { - $this->renderer->addDependency($build, $object); + public function testAddCacheableDependency(array $build, $object, array $expected) { + $this->renderer->addCacheableDependency($build, $object); $this->assertEquals($build, $expected); } - public function providerTestAddDependency() { + public function providerTestAddCacheableDependency() { return [ // Empty render array, typical default cacheability. [