diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 096551b364c21c9615ae6c5889d46f57df9804f1..2554392c46ccf4e8664da9fe9c33366d36d3637c 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -192,7 +192,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. - $pre_bubbling_cid = NULL; if (isset($elements['#cache']['keys'])) { $cached_element = $this->cacheGet($elements); if ($cached_element !== FALSE) { @@ -212,16 +211,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $this->bubbleStack(); return $elements['#markup']; } - else { - // Two-tier caching: set pre-bubbling cache ID, if this element is - // cacheable.. - // @see ::cacheGet() - // @see ::cacheSet() - if ($this->requestStack->getCurrentRequest()->isMethodSafe() && $cid = $this->createCacheID($elements)) { - $pre_bubbling_cid = $cid; - } - } } + // Two-tier caching: track pre-bubbling elements' #cache for later + // comparison. + // @see ::cacheGet() + // @see ::cacheSet() + $pre_bubbling_elements = []; + $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. @@ -381,10 +377,13 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); - // Cache the processed element if #cache is set, and the metadata necessary - // to generate a cache ID is present. - if (isset($elements['#cache']) && isset($elements['#cache']['keys'])) { - $this->cacheSet($elements, $pre_bubbling_cid); + // Cache the processed element if both $pre_bubbling_elements and $elements + // have the metadata necessary to generate a cache ID. + if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { + if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { + throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); + } + $this->cacheSet($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) drupal_render() call, @@ -551,15 +550,20 @@ protected function cacheGet(array $elements) { * * @param array $elements * A renderable array. - * @param string|null $pre_bubbling_cid - * The pre-bubbling cache ID. + * @param array $pre_bubbling_elements + * A renderable array corresponding to the state (in particular, the + * cacheability metadata) of $elements prior to the beginning of its + * rendering process, and therefore before any bubbling of child + * information has taken place. Only the #cache property is used by this + * function, so the caller may omit all other properties and children from + * this array. * * @return bool|null * Returns FALSE if no cache item could be created, NULL otherwise. * * @see ::getFromCache() */ - protected function cacheSet(array &$elements, $pre_bubbling_cid) { + protected function cacheSet(array &$elements, array $pre_bubbling_elements) { // Form submissions rely on the form being built during the POST request, // and render caching of forms prevents this from happening. // @todo remove the isMethodSafe() check when @@ -574,11 +578,14 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) { $expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age']; $cache = $this->cacheFactory->get($bin); + // Calculate the pre-bubbling CID. + $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); + // Two-tier caching: detect different CID post-bubbling, create redirect, // update redirect if different set of cache contexts. // @see ::doRender() // @see ::cacheGet() - if (isset($pre_bubbling_cid) && $pre_bubbling_cid !== $cid) { + if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { // The cache redirection strategy we're implementing here is pretty // simple in concept. Suppose we have the following render structure: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php index 7f8641773aaf9070b40d9b7a357de166055194e9..5389b021f8a44aa3ef79baf49002dc03003a6b81 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php @@ -502,6 +502,25 @@ public function providerTestBubblingWithPrerender() { return $data; } + /** + * Tests that an element's cache keys cannot be changed during its rendering. + * + * @expectedException \LogicException + * @expectedExceptionMessage Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata. + */ + public function testOverWriteCacheKeys() { + $this->setUpRequest(); + $this->setupMemoryCache(); + + // Ensure a logic exception + $data = [ + '#cache' => [ + 'keys' => ['llama', 'bar'], + ], + '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender'], + ]; + $this->renderer->render($data); + } } @@ -583,4 +602,16 @@ public static function bubblingPostRenderCache(array $element, array $context) { return $element; } + /** + * #pre_render callback for testOverWriteCacheKeys(). + */ + public static function bubblingCacheOverwritePrerender($elements) { + // Overwrite the #cache entry with new data. + $elements['#cache'] = [ + 'keys' => ['llama', 'foo'], + ]; + $elements['#markup'] = 'Setting cache keys just now!'; + return $elements; + } + }