Commit 172e9e38 authored by alexpott's avatar alexpott

Issue #2815845 by dawehner, Wim Leers, alexpott, tedbow, Berdir, swentel,...

Issue #2815845 by dawehner, Wim Leers, alexpott, tedbow, Berdir, swentel, webflo: Importing (deploying) REST resource config entities should automatically do the necessary route rebuilding
parent 7f3c710f
......@@ -16,8 +16,8 @@ class CacheRouterRebuildSubscriber implements EventSubscriberInterface {
*/
public function onRouterFinished() {
// Requested URLs that formerly gave a 403/404 may now be valid.
// Also invalidate all cached routing.
Cache::invalidateTags(['4xx-response', 'route_match']);
// Also invalidate all cached routing as well as every HTTP response.
Cache::invalidateTags(['4xx-response', 'route_match', 'http_response']);
}
/**
......
......@@ -88,6 +88,21 @@ public function __construct(LanguageManagerInterface $language_manager, ConfigFa
$this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers;
}
/**
* Sets extra headers on any responses, also subrequest ones.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onAllResponds(FilterResponseEvent $event) {
$response = $event->getResponse();
// Always add the 'http_response' cache tag to be able to invalidate every
// response, for example after rebuilding routes.
if ($response instanceof CacheableResponseInterface) {
$response->getCacheableMetadata()->addCacheTags(['http_response']);
}
}
/**
* Sets extra headers on successful responses.
*
......@@ -284,6 +299,9 @@ protected function setExpiresNoCache(Response $response) {
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('onRespond');
// There is no specific reason for choosing 16 beside it should be executed
// before ::onRespond().
$events[KernelEvents::RESPONSE][] = array('onAllResponds', 16);
return $events;
}
......
......@@ -385,6 +385,7 @@ public function testBlockCacheTags() {
'block_view',
'config:block.block.powered',
'config:user.role.anonymous',
'http_response',
'rendered',
);
sort($expected_cache_tags);
......@@ -426,6 +427,7 @@ public function testBlockCacheTags() {
'config:block.block.powered',
'config:block.block.powered-2',
'config:user.role.anonymous',
'http_response',
'rendered',
);
sort($expected_cache_tags);
......
......@@ -262,7 +262,7 @@ public function testBlockRendering() {
$result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
$this->assertTrue(empty($result), 'The title is not visible.');
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' , 'rendered']));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
}
/**
......@@ -288,7 +288,7 @@ public function testBlockEmptyRendering() {
$this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
// Ensure that the view cachability metadata is propagated even, for an
// empty block.
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
$this->assertCacheContexts(['url.query_args:_wrapper_format']);
// Add a header displayed on empty result.
......@@ -306,7 +306,7 @@ public function testBlockEmptyRendering() {
$this->drupalGet($url);
$this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'http_response', 'rendered']));
$this->assertCacheContexts(['url.query_args:_wrapper_format']);
// Hide the header on empty results.
......@@ -324,7 +324,7 @@ public function testBlockEmptyRendering() {
$this->drupalGet($url);
$this->assertEqual(0, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
$this->assertCacheContexts(['url.query_args:_wrapper_format']);
// Add an empty text.
......@@ -341,7 +341,7 @@ public function testBlockEmptyRendering() {
$this->drupalGet($url);
$this->assertEqual(1, count($this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]')));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block' , 'rendered']));
$this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
$this->assertCacheContexts(['url.query_args:_wrapper_format']);
}
......
......@@ -46,6 +46,7 @@ public function testMenuBlock() {
// Verify a cache hit, but also the presence of the correct cache tags.
$expected_tags = array(
'http_response',
'rendered',
'block_view',
'config:block_list',
......@@ -107,7 +108,7 @@ public function testMenuBlock() {
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'rendered']);
$this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'http_response', 'rendered']);
}
}
......@@ -265,7 +265,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
$render_cache_tags
);
$expected_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags);
$expected_tags = Cache::mergeTags($expected_tags, ['rendered', 'config:user.role.anonymous', 'config:system.site']);
$expected_tags = Cache::mergeTags($expected_tags, ['http_response', 'rendered', 'config:user.role.anonymous', 'config:system.site']);
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
......@@ -331,7 +331,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
$this->assertPageCacheContextsAndTags(
Url::fromRoute('view.frontpage.page_1'),
$cache_contexts,
Cache::mergeTags($first_page_output_cache_tags, ['rendered', 'config:user.role.anonymous'])
Cache::mergeTags($first_page_output_cache_tags, ['http_response', 'rendered', 'config:user.role.anonymous'])
);
// Second page.
......@@ -350,6 +350,7 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
'node_view',
'user_view',
'user:0',
'http_response',
'rendered',
// FinishResponseSubscriber adds this cache tag to responses that have the
// 'user.permissions' cache context for anonymous users.
......
......@@ -80,6 +80,7 @@ function testPageCacheTags() {
// Full node page 1.
$this->assertPageCacheContextsAndTags($node_1->urlInfo(), $cache_contexts, array(
'http_response',
'rendered',
'block_view',
'config:block_list',
......@@ -120,6 +121,7 @@ function testPageCacheTags() {
// Full node page 2.
$this->assertPageCacheContextsAndTags($node_2->urlInfo(), $cache_contexts, array(
'http_response',
'rendered',
'block_view',
'config:block_list',
......
......@@ -63,6 +63,7 @@ function testPageCacheTags() {
sort($cache_entry->tags);
$expected_tags = array(
'config:user.role.anonymous',
'http_response',
'pre_render',
'rendered',
'system_test_cache_tags_page',
......@@ -94,6 +95,7 @@ function testPageCacheTagsIndependentFromCacheabilityHeaders() {
sort($cache_entry->tags);
$expected_tags = array(
'config:user.role.anonymous',
'http_response',
'pre_render',
'rendered',
'system_test_cache_tags_page',
......
......@@ -35,8 +35,15 @@ services:
logger.channel.rest:
parent: logger.channel_base
arguments: ['rest']
# Event subscribers.
rest.resource_response.subscriber:
class: Drupal\rest\EventSubscriber\ResourceResponseSubscriber
tags:
- { name: event_subscriber }
arguments: ['@serializer', '@renderer', '@current_route_match']
rest.config_subscriber:
class: Drupal\rest\EventSubscriber\RestConfigSubscriber
arguments: ['@router.builder']
tags:
- { name: event_subscriber }
......@@ -3,6 +3,7 @@
namespace Drupal\rest\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\rest\RestResourceConfigInterface;
......@@ -255,4 +256,22 @@ protected function normalizeRestMethod($method) {
return strtoupper($method);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
\Drupal::service('router.builder')->setRebuildNeeded();
}
}
<?php
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Routing\RouteBuilderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber triggering a route rebuild when certain configuration changes.
*/
class RestConfigSubscriber implements EventSubscriberInterface {
/**
* The router builder.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routerBuilder;
/**
* Constructs the RestConfigSubscriber.
*
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The router builder service.
*/
public function __construct(RouteBuilderInterface $router_builder) {
$this->routerBuilder = $router_builder;
}
/**
* Informs the router builder a rebuild is needed when necessary.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'rest.settings' && $event->isChanged('bc_entity_resource_permissions')) {
$this->routerBuilder->setRebuildNeeded();
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}
......@@ -420,8 +420,7 @@ protected function enableService($resource_type, $method = 'GET', $format = NULL
* Rebuilds routing caches.
*/
protected function rebuildCache() {
// Rebuild routing cache, so that the REST API paths are available.
$this->container->get('router.builder')->rebuild();
$this->container->get('router.builder')->rebuildIfNeeded();
}
/**
......
......@@ -122,9 +122,9 @@ protected function getExpectedCacheContexts() {
protected function getExpectedCacheTags() {
// Because the 'user.permissions' cache context is missing, the cache tag
// for the anonymous user role is never added automatically.
return array_filter(parent::getExpectedCacheTags(), function ($tag) {
return array_values(array_filter(parent::getExpectedCacheTags(), function ($tag) {
return $tag !== 'config:user.role.anonymous';
});
}));
}
/**
......
......@@ -146,6 +146,15 @@ protected function provisionEntityResource() {
$this->provisionResource('entity.' . static::$entityTypeId, [static::$format], $auth);
}
/**
* Deprovisions the tested entity resource.
*/
protected function deprovisionEntityResource() {
$this->resourceConfigStorage->load('entity.' . static::$entityTypeId)
->delete();
$this->refreshTestStateAfterRestConfigChange();
}
/**
* {@inheritdoc}
*/
......@@ -195,9 +204,6 @@ public function setUp() {
}
$this->entity->save();
}
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
}
/**
......@@ -291,6 +297,7 @@ protected function getExpectedCacheTags() {
if (!static::$auth) {
$expected_cache_tags[] = 'config:user.role.anonymous';
}
$expected_cache_tags[] = 'http_response';
return Cache::mergeTags($expected_cache_tags, $this->entity->getCacheTags());
}
......@@ -458,8 +465,7 @@ public function testGet() {
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
$this->refreshTestStateAfterRestConfigChange();
// DX: 403 when unauthorized.
......@@ -475,6 +481,20 @@ public function testGet() {
$this->assertResourceResponse(200, FALSE, $response);
$this->deprovisionEntityResource();
// DX: upon deprovisioning, immediate 404 if no route, 406 otherwise.
$response = $this->request('GET', $url, $request_options);
if (!$has_canonical_url) {
$this->assertSame(404, $response->getStatusCode());
}
else {
$this->assert406Response($response);
}
$this->provisionEntityResource();
$url->setOption('query', ['_format' => 'non_existing_format']);
......@@ -673,9 +693,8 @@ public function testPost() {
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
$this->refreshTestStateAfterRestConfigChange();
$request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
// DX: 403 when unauthorized.
......@@ -876,9 +895,8 @@ public function testPatch() {
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
$this->refreshTestStateAfterRestConfigChange();
$request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
// DX: 403 when unauthorized.
......@@ -975,8 +993,7 @@ public function testDelete() {
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
$this->refreshTestStateAfterRestConfigChange();
$this->entity = $this->createEntity();
$url = $this->getUrl()->setOption('query', $url->getOption('query'));
......
......@@ -119,6 +119,7 @@ public function setUp() {
// Ensure there's a clean slate: delete all REST resource config entities.
$this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple());
$this->refreshTestStateAfterRestConfigChange();
}
/**
......@@ -141,8 +142,25 @@ protected function provisionResource($resource_type, $formats = [], $authenticat
'authentication' => $authentication,
]
])->save();
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
$this->refreshTestStateAfterRestConfigChange();
}
/**
* Refreshes the state of the tester to be in sync with the testee.
*
* Should be called after every change made to:
* - RestResourceConfig entities
* - the 'rest.settings' simple configuration
*/
protected function refreshTestStateAfterRestConfigChange() {
// Ensure that the cache tags invalidator has its internal values reset.
// Otherwise the http_response cache tag invalidation won't work.
$this->refreshVariables();
// Tests using this base class may trigger route rebuilds due to changes to
// RestResourceConfig entities or 'rest.settings'. Ensure the test generates
// routes using an up-to-date router.
\Drupal::service('router.builder')->rebuildIfNeeded();
}
/**
......
......@@ -70,6 +70,7 @@ function testSearchText() {
$this->assertCacheTag('node:1');
$this->assertCacheTag('user:2');
$this->assertCacheTag('rendered');
$this->assertCacheTag('http_response');
$this->assertCacheTag('node_list');
// Updating a node should invalidate the search plugin's index cache tag.
......@@ -83,6 +84,7 @@ function testSearchText() {
$this->assertCacheTag('node:1');
$this->assertCacheTag('user:2');
$this->assertCacheTag('rendered');
$this->assertCacheTag('http_response');
$this->assertCacheTag('node_list');
// Deleting a node should invalidate the search plugin's index cache tag.
......@@ -172,6 +174,7 @@ public function testSearchTagsBubbling() {
'config:search.page.node_search',
'search_index',
'search_index:node_search',
'http_response',
'rendered',
'node_list',
];
......@@ -192,8 +195,7 @@ public function testSearchTagsBubbling() {
'node_view',
'config:filter.format.plain_text',
]);
$cache_tags = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertEqual(explode(' ', $cache_tags), $expected_cache_tags);
$this->assertCacheTags($expected_cache_tags);
// Only get the new node in the search results, should result in node:1,
// node:2 and user:3 as cache tags even though only node:1 is shown. This is
......@@ -208,8 +210,7 @@ public function testSearchTagsBubbling() {
'user:3',
'node_view',
]);
$cache_tags = $this->drupalGetHeader('X-Drupal-Cache-Tags');
$this->assertEqual(explode(' ', $cache_tags), $expected_cache_tags);
$this->assertCacheTags($expected_cache_tags);
}
}
......@@ -122,10 +122,14 @@ protected function debugCacheTags(array $actual_tags, array $expected_tags) {
*/
protected function assertCacheTags(array $expected_tags, $include_default_tags = TRUE) {
// The anonymous role cache tag is only added if the user is anonymous.
if ($include_default_tags && \Drupal::currentUser()->isAnonymous()) {
$expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']);
if ($include_default_tags) {
if (\Drupal::currentUser()->isAnonymous()) {
$expected_tags = Cache::mergeTags($expected_tags, ['config:user.role.anonymous']);
}
$expected_tags[] = 'http_response';
}
$actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
$expected_tags = array_unique($expected_tags);
sort($expected_tags);
sort($actual_tags);
$this->assertIdentical($actual_tags, $expected_tags);
......
......@@ -343,7 +343,7 @@ public function testReferencedEntity() {
// 'user.permissions' is a required cache context, and responses that vary
// by this cache context when requested by anonymous users automatically
// also get this cache tag, to ensure correct invalidation.
$page_cache_tags = Cache::mergeTags(['rendered'], ['config:user.role.anonymous']);
$page_cache_tags = Cache::mergeTags(['http_response', 'rendered'], ['config:user.role.anonymous']);
// If the block module is used, the Block page display variant is used,
// which adds the block config entity type's list cache tags.
$page_cache_tags = Cache::mergeTags($page_cache_tags, \Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list'] : []);
......@@ -641,7 +641,7 @@ public function testReferencedEntity() {
// Verify cache hits.
$referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
$referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']);
$referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['http_response', 'rendered']);
$nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing());
$nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags);
......
......@@ -45,7 +45,7 @@ public function testFinishResponseSubscriber() {
// Check expected headers from FinishResponseSubscriber.
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', $expected_cache_contexts));
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous rendered');
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous http_response rendered');
// Confirm that the page wrapping is being added, so we're not getting a
// raw body returned.
$this->assertRaw('</html>', 'Page markup was found.');
......@@ -60,12 +60,12 @@ public function testFinishResponseSubscriber() {
$this->drupalGet('router_test/test18');
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url'])));
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered');
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
// 2. controller result: render array, per-role cacheable route access.
$this->drupalGet('router_test/test19');
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['x-drupal-cache-contexts'], implode(' ', Cache::mergeContexts($renderer_required_cache_contexts, ['url', 'user.roles'])));
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo rendered');
$this->assertEqual($headers['x-drupal-cache-tags'], 'config:user.role.anonymous foo http_response rendered');
// 3. controller result: Response object, globally cacheable route access.
$this->drupalGet('router_test/test1');
$headers = $this->drupalGetHeaders();
......@@ -80,12 +80,12 @@ public function testFinishResponseSubscriber() {
$this->drupalGet('router_test/test21');
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['x-drupal-cache-contexts'], '');
$this->assertEqual($headers['x-drupal-cache-tags'], '');
$this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
// 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'], '');
$this->assertEqual($headers['x-drupal-cache-tags'], 'http_response');
// Finally, verify that the X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags
// headers are not sent when their container parameter is set to FALSE.
......
......@@ -48,6 +48,7 @@ public function testRenderedTour() {
$expected_tags = [
'config:tour.tour.tour-test',
'config:user.role.anonymous',
'http_response',
'rendered',
];
$this->verifyPageCache($url, 'HIT', $expected_tags);
......@@ -68,6 +69,7 @@ public function testRenderedTour() {
// Verify a cache hit.
$expected_tags = [
'config:user.role.anonymous',
'http_response',
'rendered',
];
$this->verifyPageCache($url, 'HIT', $expected_tags);
......
......@@ -95,6 +95,7 @@ public function testGlossaryView() {
'node_list',
'user:0',
'user_list',
'http_response',
'rendered',
// FinishResponseSubscriber adds this cache tag to responses that have the
// 'user.permissions' cache context for anonymous users.
......
......@@ -43,7 +43,7 @@ public function testGoTo() {
$this->assertNotContains('</html>', $text);
// Response includes cache tags that we can assert.
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'rendered');
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache-Tags', 'http_response rendered');
// Test that we can read the JS settings.
$js_settings = $this->getDrupalSettings();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment