Exemplo n.º 1
1
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     // The block by itself doesn't really vary by user, but some of its
     // implementations are (collection module, I'm looking at you). For the sake
     // of semplicity, we add the user context here already.
     $contexts = parent::getCacheContexts();
     return Cache::mergeContexts($contexts, ['user']);
 }
Exemplo n.º 2
1
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     return Cache::mergeContexts(parent::getCacheContexts(), ['route.book_navigation']);
 }
 /**
  * Tests that the block is cached with the correct contexts and tags.
  */
 public function testBlock()
 {
     $block = $this->drupalPlaceBlock('block_content:' . $this->entity->uuid());
     $build = $this->container->get('entity.manager')->getViewBuilder('block')->view($block, 'block');
     // Render the block.
     // @todo The request stack manipulation won't be necessary once
     //   https://www.drupal.org/node/2367555 is fixed and the
     //   corresponding $request->isMethodSafe() checks are removed from
     //   Drupal\Core\Render\Renderer.
     $request_stack = $this->container->get('request_stack');
     $request_stack->push(new Request());
     $this->container->get('renderer')->renderRoot($build);
     $request_stack->pop();
     // Expected keys, contexts, and tags for the block.
     // @see \Drupal\block\BlockViewBuilder::viewMultiple()
     $expected_block_cache_keys = ['entity_view', 'block', $block->id()];
     $expected_block_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme'];
     $expected_block_cache_tags = Cache::mergeTags(['block_view', 'rendered'], $block->getCacheTags(), $block->getPlugin()->getCacheTags());
     // Expected contexts and tags for the BlockContent entity.
     // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults().
     $expected_entity_cache_contexts = ['theme'];
     $expected_entity_cache_tags = Cache::mergeTags(['block_content_view'], $this->entity->getCacheTags(), $this->getAdditionalCacheTagsForEntity($this->entity));
     // Verify that what was render cached matches the above expectations.
     $cid = $this->createCacheId($expected_block_cache_keys, $expected_block_cache_contexts);
     $redirected_cid = $this->createCacheId($expected_block_cache_keys, Cache::mergeContexts($expected_block_cache_contexts, $expected_entity_cache_contexts));
     $this->verifyRenderCache($cid, Cache::mergeTags($expected_block_cache_tags, $expected_entity_cache_tags), $cid !== $redirected_cid ? $redirected_cid : NULL);
 }
Exemplo n.º 4
0
 /**
  * {@inheritdoc}
  */
 public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL)
 {
     // @todo Remove when https://www.drupal.org/node/2453059 lands.
     $default_cache_contexts = ['languages', 'theme'];
     /** @var \Drupal\block\BlockInterface[] $entities */
     $build = array();
     foreach ($entities as $entity) {
         $entity_id = $entity->id();
         $plugin = $entity->getPlugin();
         $plugin_id = $plugin->getPluginId();
         $base_id = $plugin->getBaseId();
         $derivative_id = $plugin->getDerivativeId();
         $configuration = $plugin->getConfiguration();
         // Create the render array for the block as a whole.
         // @see template_preprocess_block().
         $build[$entity_id] = array('#theme' => 'block', '#attributes' => array(), '#contextual_links' => array('block' => array('route_parameters' => array('block' => $entity->id()))), '#weight' => $entity->getWeight(), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#cache' => ['contexts' => Cache::mergeContexts($default_cache_contexts, $plugin->getCacheContexts()), 'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags(), $plugin->getCacheTags()), 'max-age' => $plugin->getCacheMaxAge()], '#block' => $entity);
         $build[$entity_id]['#configuration']['label'] = String::checkPlain($configuration['label']);
         if ($plugin->isCacheable()) {
             $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
             // Generic cache keys, with the block plugin's custom keys appended.
             $default_cache_keys = array('entity_view', 'block', $entity->id());
             $build[$entity_id]['#cache']['keys'] = array_merge($default_cache_keys, $plugin->getCacheKeys());
         } else {
             $build[$entity_id] = $this->buildBlock($build[$entity_id]);
         }
         // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
         // alter hook wants to modify the block contents, it can append another
         // #pre_render hook.
         $this->moduleHandler()->alter(array('block_view', "block_view_{$base_id}"), $build[$entity_id], $plugin);
     }
     return $build;
 }
Exemplo n.º 5
0
 /**
  * {@inheritdoc}
  */
 public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL)
 {
     /** @var \Drupal\block\BlockInterface[] $entities */
     $build = array();
     foreach ($entities as $entity) {
         $entity_id = $entity->id();
         $plugin = $entity->getPlugin();
         $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
         $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
         // Create the render array for the block as a whole.
         // @see template_preprocess_block().
         $build[$entity_id] = array('#cache' => ['keys' => ['entity_view', 'block', $entity->id()], 'contexts' => Cache::mergeContexts($entity->getCacheContexts(), $plugin->getCacheContexts()), 'tags' => $cache_tags, 'max-age' => $plugin->getCacheMaxAge()], '#weight' => $entity->getWeight());
         // Allow altering of cacheability metadata or setting #create_placeholder.
         $this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
         if ($plugin instanceof MainContentBlockPluginInterface || $plugin instanceof TitleBlockPluginInterface) {
             // Immediately build a #pre_render-able block, since this block cannot
             // be built lazily.
             $build[$entity_id] += static::buildPreRenderableBlock($entity, $this->moduleHandler());
         } else {
             // Assign a #lazy_builder callback, which will generate a #pre_render-
             // able block lazily (when necessary).
             $build[$entity_id] += ['#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]]];
         }
     }
     return $build;
 }
Exemplo n.º 6
0
 /**
  * Confirms that our FinishResponseSubscriber logic works properly.
  */
 public function testFinishResponseSubscriber()
 {
     $renderer_required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
     $expected_cache_contexts = Cache::mergeContexts($renderer_required_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]);
     // Confirm that the router can get to a controller.
     $this->drupalGet('router_test/test1');
     $this->assertRaw('test1', 'The correct string was returned because the route was successful.');
     // Check expected headers from FinishResponseSubscriber.
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-ua-compatible'], 'IE=edge');
     $this->assertEqual($headers['content-language'], 'en');
     $this->assertEqual($headers['x-content-type-options'], 'nosniff');
     $this->assertEqual($headers['x-frame-options'], 'SAMEORIGIN');
     $this->drupalGet('router_test/test2');
     $this->assertRaw('test2', 'The correct string was returned because the route was successful.');
     // 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');
     // Confirm that the page wrapping is being added, so we're not getting a
     // raw body returned.
     $this->assertRaw('</html>', 'Page markup was found.');
     // In some instances, the subrequest handling may get confused and render
     // a page inception style.  This test verifies that is not happening.
     $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
     // Confirm that route-level access check's cacheability is applied to the
     // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags headers.
     // 1. controller result: render array, globally cacheable route access.
     $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');
     // 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');
     // 3. controller result: Response object, globally cacheable route access.
     $this->drupalGet('router_test/test1');
     $headers = $this->drupalGetHeaders();
     $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'], '');
 }
Exemplo n.º 7
0
 /**
  * Tests that #type=link bubbles outbound route/path processors' cacheability.
  */
 function testLinkCacheability()
 {
     $cases = [['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]], ['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => Cache::PERMANENT]], ['Route processor link', 'route:system.run_cron', [], ['contexts' => [], 'tags' => [], 'max-age' => 0]], ['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => 0]], ['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]], ['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]]];
     foreach ($cases as $case) {
         list($title, $uri, $options, $expected_cacheability) = $case;
         $expected_cacheability['contexts'] = Cache::mergeContexts($expected_cacheability['contexts'], ['languages:language_interface', 'theme']);
         $link = ['#type' => 'link', '#title' => $title, '#options' => $options, '#url' => Url::fromUri($uri)];
         \Drupal::service('renderer')->renderRoot($link);
         $this->pass($title);
         $this->assertEqual($expected_cacheability, $link['#cache']);
     }
 }
Exemplo n.º 8
0
 /**
  * {@inheritdoc}
  */
 public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL)
 {
     /** @var \Drupal\block\BlockInterface[] $entities */
     $build = array();
     foreach ($entities as $entity) {
         $entity_id = $entity->id();
         $plugin = $entity->getPlugin();
         $plugin_id = $plugin->getPluginId();
         $base_id = $plugin->getBaseId();
         $derivative_id = $plugin->getDerivativeId();
         $configuration = $plugin->getConfiguration();
         $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
         $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
         // Create the render array for the block as a whole.
         // @see template_preprocess_block().
         $build[$entity_id] = array('#theme' => 'block', '#attributes' => array(), '#contextual_links' => array('block' => array('route_parameters' => array('block' => $entity->id()))), '#weight' => $entity->getWeight(), '#configuration' => $configuration, '#plugin_id' => $plugin_id, '#base_plugin_id' => $base_id, '#derivative_plugin_id' => $derivative_id, '#id' => $entity->id(), '#cache' => ['keys' => ['entity_view', 'block', $entity->id()], 'contexts' => Cache::mergeContexts($entity->getCacheContexts(), $plugin->getCacheContexts()), 'tags' => $cache_tags, 'max-age' => $plugin->getCacheMaxAge()], '#pre_render' => [[$this, 'buildBlock']], '#block' => $entity);
         $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']);
         // Don't run in ::buildBlock() to ensure cache keys can be altered. If an
         // alter hook wants to modify the block contents, it can append another
         // #pre_render hook.
         $this->moduleHandler()->alter(array('block_view', "block_view_{$base_id}"), $build[$entity_id], $plugin);
     }
     return $build;
 }
Exemplo n.º 9
0
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
 }
 /**
  * Asserts that a block is built/rendered/cached with expected cacheability.
  *
  * @param string[] $expected_keys
  *   The expected cache keys.
  * @param string[] $expected_contexts
  *   The expected cache contexts.
  * @param string[] $expected_tags
  *   The expected cache tags.
  * @param int $expected_max_age
  *   The expected max-age.
  */
 protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age)
 {
     $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
     // Check that the expected cacheability metadata is present in:
     // - the built render array;
     $this->pass('Built render array');
     $build = $this->getBlockRenderArray();
     $this->assertIdentical($expected_keys, $build['#cache']['keys']);
     $this->assertIdentical($expected_contexts, $build['#cache']['contexts']);
     $this->assertIdentical($expected_tags, $build['#cache']['tags']);
     $this->assertIdentical($expected_max_age, $build['#cache']['max-age']);
     $this->assertFalse(isset($build['#create_placeholder']));
     // - the rendered render array;
     $this->pass('Rendered render array');
     $this->renderer->renderRoot($build);
     // - the render cache item.
     $this->pass('Render cache item');
     $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts);
     $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys());
     $cache_item = $this->container->get('cache.render')->get($cid);
     $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.');
     $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
     $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']);
     $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']);
     $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']);
     $this->container->get('cache.render')->delete($cid);
 }
Exemplo n.º 11
0
 /**
  * Tests that cache contexts are applied for both users.
  *
  * @param string[] $cache_contexts
  *   Expected cache contexts for both users.
  * @param string $message
  *   (optional) A verbose message to output.
  *
  * @return
  *   TRUE if the assertion succeeded, FALSE otherwise.
  */
 protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL)
 {
     // Default cache contexts that should exist on all test cases.
     $default_cache_contexts = ['languages:language_interface', 'theme'];
     $cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts);
     // Assert contexts for user1 which has only default permissions.
     $this->drupalLogin($this->adminUser);
     $this->drupalGet('test-page');
     $return = $this->assertCacheContexts($cache_contexts);
     $this->drupalLogout();
     // Assert contexts for user2 which has some additional permissions.
     $this->drupalLogin($this->adminUser2);
     $this->drupalGet('test-page');
     $return = $return && $this->assertCacheContexts($cache_contexts);
     if ($return) {
         $this->pass($message);
     } else {
         $this->fail($message);
     }
     return $return;
 }
Exemplo n.º 12
0
 /**
  * {@inheritdoc}
  */
 public function set(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
     //       https://www.drupal.org/node/2367555 lands.
     if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !($cid = $this->createCacheID($elements))) {
         return FALSE;
     }
     $data = $this->getCacheableRenderArray($elements);
     $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
     $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 \Drupal\Core\Render\RendererInterface::render()
     // @see ::get()
     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'])
         // -- B (specifies #cache['contexts'] = ['b'])
         //
         // At the time that we're evaluating whether A's rendering can be
         // retrieved from cache, we won't know the contexts required by its
         // children (the children might not even be built yet), so cacheGet()
         // will only be able to get what is cached for a $cid of 'foo'. But at
         // the time we're writing to that cache, we do know all the contexts that
         // were specified by all children, so what we need is a way to
         // persist that information between the cache write and the next cache
         // read. So, what we can do is store the following into 'foo':
         // [
         //   '#cache_redirect' => TRUE,
         //   '#cache' => [
         //     ...
         //     'contexts' => ['b'],
         //   ],
         // ]
         //
         // This efficiently lets cacheGet() redirect to a $cid that includes all
         // of the required contexts. The strategy is on-demand: in the case where
         // there aren't any additional contexts required by children that aren't
         // already included in the parent's pre-bubbled #cache information, no
         // cache redirection is needed.
         //
         // When implementing this redirection strategy, special care is needed to
         // resolve potential cache ping-pong problems. For example, consider the
         // following render structure:
         // - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
         // -- B (pre-bubbling, specifies #cache['contexts'] = ['b'])
         // --- C (pre-bubbling, specifies #cache['contexts'] = ['c'])
         // --- D (pre-bubbling, specifies #cache['contexts'] = ['d'])
         //
         // Additionally, suppose that:
         // - C only exists for a 'b' context value of 'b1'
         // - D only exists for a 'b' context value of 'b2'
         // This is an acceptable variation, since B specifies that its contents
         // vary on context 'b'.
         //
         // A naive implementation of cache redirection would result in the
         // following:
         // - When a request is processed where context 'b' = 'b1', what would be
         //   cached for a $pre_bubbling_cid of 'foo' is:
         //   [
         //     '#cache_redirect' => TRUE,
         //     '#cache' => [
         //       ...
         //       'contexts' => ['b', 'c'],
         //     ],
         //   ]
         // - When a request is processed where context 'b' = 'b2', we would
         //   retrieve the above from cache, but when following that redirection,
         //   get a cache miss, since we're processing a 'b' context value that
         //   has not yet been cached. Given the cache miss, we would continue
         //   with rendering the structure, perform the required context bubbling
         //   and then overwrite the above item with:
         //   [
         //     '#cache_redirect' => TRUE,
         //     '#cache' => [
         //       ...
         //       'contexts' => ['b', 'd'],
         //     ],
         //   ]
         // - Now, if a request comes in where context 'b' = 'b1' again, the above
         //   would redirect to a cache key that doesn't exist, since we have not
         //   yet cached an item that includes 'b'='b1' and something for 'd'. So
         //   we would process this request as a cache miss, at the end of which,
         //   we would overwrite the above item back to:
         //   [
         //     '#cache_redirect' => TRUE,
         //     '#cache' => [
         //       ...
         //       'contexts' => ['b', 'c'],
         //     ],
         //   ]
         // - The above would always result in accurate renderings, but would
         //   result in poor performance as we keep processing requests as cache
         //   misses even though the target of the redirection is cached, and
         //   it's only the redirection element itself that is creating the
         //   ping-pong problem.
         //
         // A way to resolve the ping-pong problem is to eventually reach a cache
         // state where the redirection element includes all of the contexts used
         // throughout all requests:
         // [
         //   '#cache_redirect' => TRUE,
         //   '#cache' => [
         //     ...
         //     'contexts' => ['b', 'c', 'd'],
         //   ],
         // ]
         //
         // We can't reach that state right away, since we don't know what the
         // result of future requests will be, but we can incrementally move
         // towards that state by progressively merging the 'contexts' value
         // across requests. That's the strategy employed below and tested in
         // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
         // The set of cache contexts for this element, including the bubbled ones,
         // for which we are handling a cache miss.
         $cache_contexts = $data['#cache']['contexts'];
         // Get the contexts by which this element should be varied according to
         // the current redirecting cache item, if any.
         $stored_cache_contexts = [];
         $stored_cache_tags = [];
         if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
             $stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts'];
             $stored_cache_tags = $stored_cache_redirect->data['#cache']['tags'];
         }
         // Calculate the union of the cache contexts for this request and the
         // stored cache contexts.
         $merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts);
         // Stored cache contexts incomplete: this request causes cache contexts to
         // be added to the redirecting cache item.
         if (array_diff($merged_cache_contexts, $stored_cache_contexts)) {
             $redirect_data = ['#cache_redirect' => TRUE, '#cache' => ['keys' => $elements['#cache']['keys'], 'contexts' => $merged_cache_contexts, 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags'])]];
             $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
         }
         // Current cache contexts incomplete: this request only uses a subset of
         // the cache contexts stored in the redirecting cache item. Vary by these
         // additional (conditional) cache contexts as well, otherwise the
         // redirecting cache item would be pointing to a cache item that can never
         // exist.
         if (array_diff($merged_cache_contexts, $cache_contexts)) {
             // Recalculate the cache ID.
             $recalculated_cid_pseudo_element = ['#cache' => ['keys' => $elements['#cache']['keys'], 'contexts' => $merged_cache_contexts]];
             $cid = $this->createCacheID($recalculated_cid_pseudo_element);
             // Ensure the about-to-be-cached data uses the merged cache contexts.
             $data['#cache']['contexts'] = $merged_cache_contexts;
         }
     }
     $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
 }
Exemplo n.º 13
0
 /**
  * Fills in the cache metadata of this view.
  *
  * Cache metadata is set per view and per display, and ends up being stored in
  * the view's configuration. This allows Views to determine very efficiently:
  * - the max-age
  * - the cache contexts
  * - the cache tags
  *
  * In other words: this allows us to do the (expensive) work of initializing
  * Views plugins and handlers to determine their effect on the cacheability of
  * a view at save time rather than at runtime.
  */
 protected function addCacheMetadata()
 {
     $executable = $this->getExecutable();
     $current_display = $executable->current_display;
     $displays = $this->get('display');
     foreach (array_keys($displays) as $display_id) {
         $display =& $this->getDisplay($display_id);
         $executable->setDisplay($display_id);
         $cache_metadata = $executable->getDisplay()->calculateCacheMetadata();
         $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
         $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
         $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
         // Always include at least the 'languages:' context as there will most
         // probably be translatable strings in the view output.
         $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]);
     }
     // Restore the previous active display.
     $executable->setDisplay($current_display);
 }
Exemplo n.º 14
0
 /**
  * {@inheritdoc}
  *
  * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
  */
 public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match)
 {
     list($page, $title) = $this->prepare($main_content, $request, $route_match);
     if (!isset($page['#type']) || $page['#type'] !== 'page') {
         throw new \LogicException('Must be #type page');
     }
     $page['#title'] = $title;
     // Now render the rendered page.html.twig template inside the html.html.twig
     // template, and use the bubbled #attached metadata from $page to ensure we
     // load all attached assets.
     $html = ['#type' => 'html', 'page' => $page];
     // The special page regions will appear directly in html.html.twig, not in
     // page.html.twig, hence add them here, just before rendering html.html.twig.
     $this->buildPageTopAndBottom($html);
     // Render, but don't replace placeholders yet, because that happens later in
     // the render pipeline. To not replace placeholders yet, we use
     // RendererInterface::render() instead of RendererInterface::renderRoot().
     // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
     $render_context = new RenderContext();
     $this->renderer->executeInRenderContext($render_context, function () use(&$html) {
         // RendererInterface::render() renders the $html render array and updates
         // it in place. We don't care about the return value (which is just
         // $html['#markup']), but about the resulting render array.
         // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
         $this->renderer->render($html);
     });
     // RendererInterface::render() always causes bubbleable metadata to be
     // stored in the render context, no need to check it conditionally.
     $bubbleable_metadata = $render_context->pop();
     $bubbleable_metadata->applyTo($html);
     $content = $this->renderCache->getCacheableRenderArray($html);
     // Also associate the required cache contexts.
     // (Because we use ::render() above and not ::renderRoot(), we manually must
     // ensure the HTML response varies by the required cache contexts.)
     $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']);
     // Also associate the "rendered" cache tag. This allows us to invalidate the
     // entire render cache, regardless of the cache bin.
     $content['#cache']['tags'][] = 'rendered';
     $response = new HtmlResponse($content, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
     return $response;
 }
Exemplo n.º 15
0
 /**
  * Tests random ordering with tags based caching.
  *
  * The random sorting should opt out of caching by defining a max age of 0.
  * At the same time, the row render caching still works.
  */
 public function testRandomOrderingWithRenderCaching()
 {
     $view_random = $this->getBasicRandomView();
     $display =& $view_random->storage->getDisplay('default');
     $display['display_options']['cache'] = ['type' => 'tag'];
     $view_random->storage->save();
     /** @var \Drupal\Core\Render\RendererInterface $renderer */
     $renderer = \Drupal::service('renderer');
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
     $original = $build = DisplayPluginBase::buildBasicRenderable($view_random->id(), 'default');
     $result = $renderer->renderPlain($build);
     $original['#cache'] += ['contexts' => []];
     $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
     $this->assertFalse($render_cache->get($original), 'Ensure there is no render cache entry.');
     $build = DisplayPluginBase::buildBasicRenderable($view_random->id(), 'default');
     $result2 = $renderer->renderPlain($build);
     // Ensure that the random ordering works and don't produce the same result.
     $this->assertNotEqual($result, $result2);
 }
Exemplo n.º 16
0
 /**
  * Asserts a view's result & render cache items' cache tags.
  *
  * This method starts with a pre bubbling basic render array.
  *
  * @param \Drupal\views\ViewExecutable $view
  *   The view.
  * @param string[] $expected_render_array_cache_tags
  *   The expected render cache tags.
  * @param bool $views_caching_is_enabled
  *   Defines whether views output / render caching is enabled.
  *
  * @return array
  *   The render array.
  */
 protected function assertViewsCacheTagsFromStaticRenderArray(ViewExecutable $view, array $expected_render_array_cache_tags, $views_caching_is_enabled)
 {
     $original = $build = DisplayPluginBase::buildBasicRenderable($view->id(), $view->current_display ?: 'default', $view->args);
     /** @var \Drupal\Core\Render\RendererInterface $renderer */
     $renderer = \Drupal::service('renderer');
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
     // Ensure the current request is a GET request so that render caching is
     // active for direct rendering of views, just like for actual requests.
     /** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
     $request_stack = \Drupal::service('request_stack');
     $request = new Request();
     $request->server->set('REQUEST_TIME', REQUEST_TIME);
     $request_stack->push($request);
     $renderer->renderRoot($build);
     // Render array cache tags.
     $this->pass('Checking render array cache tags.');
     sort($expected_render_array_cache_tags);
     $this->assertEqual($build['#cache']['tags'], $expected_render_array_cache_tags);
     $this->debugCacheTags($build['#cache']['tags'], $expected_render_array_cache_tags);
     $this->pass('Checking Views render cache item cache tags.');
     $original['#cache'] += ['contexts' => []];
     $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
     $render_cache_item = $render_cache->get($original);
     if ($views_caching_is_enabled) {
         $this->assertTrue(!empty($render_cache_item), 'Render cache item found.');
         if ($render_cache_item) {
             $this->assertEqual($render_cache_item['#cache']['tags'], $expected_render_array_cache_tags);
             $this->debugCacheTags($render_cache_item['#cache']['tags'], $expected_render_array_cache_tags);
         }
     } else {
         $this->assertFalse($render_cache_item, 'Render cache item not found.');
     }
     return $build;
 }
 /**
  * Tests cache tags presence and invalidation of the entity at its URI.
  *
  * Tests the following cache tags:
  * - "<entity type>_view"
  * - "<entity_type>:<entity ID>"
  */
 public function testEntityUri()
 {
     $entity_url = $this->entity->urlInfo();
     $entity_type = $this->entity->getEntityTypeId();
     // Selects the view mode that will be used.
     $view_mode = $this->selectViewMode($entity_type);
     // The default cache contexts for rendered entities.
     $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
     // Generate the standardized entity cache tags.
     $cache_tag = $this->entity->getCacheTags();
     $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags();
     $render_cache_tag = 'rendered';
     $this->pass("Test entity.", 'Debug');
     $this->verifyPageCache($entity_url, 'MISS');
     // Verify a cache hit, but also the presence of the correct cache tags.
     $this->verifyPageCache($entity_url, 'HIT');
     // Also verify the existence of an entity render cache entry, if this entity
     // type supports render caching.
     if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) {
         $cache_keys = ['entity_view', $entity_type, $this->entity->id(), $view_mode];
         $cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
         $redirected_cid = NULL;
         $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->entity);
         if (count($additional_cache_contexts)) {
             $redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts));
         }
         $expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag);
         $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity));
         $expected_cache_tags = Cache::mergeTags($expected_cache_tags, array($render_cache_tag));
         $this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid);
     }
     // Verify that after modifying the entity, there is a cache miss.
     $this->pass("Test modification of entity.", 'Debug');
     $this->entity->save();
     $this->verifyPageCache($entity_url, 'MISS');
     // Verify a cache hit.
     $this->verifyPageCache($entity_url, 'HIT');
     // Verify that after modifying the entity's display, there is a cache miss.
     $this->pass("Test modification of entity's '{$view_mode}' display.", 'Debug');
     $entity_display = entity_get_display($entity_type, $this->entity->bundle(), $view_mode);
     $entity_display->save();
     $this->verifyPageCache($entity_url, 'MISS');
     // Verify a cache hit.
     $this->verifyPageCache($entity_url, 'HIT');
     if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) {
         // Verify that after modifying the corresponding bundle entity, there is a
         // cache miss.
         $this->pass("Test modification of entity's bundle entity.", 'Debug');
         $bundle_entity = entity_load($bundle_entity_type_id, $this->entity->bundle());
         $bundle_entity->save();
         $this->verifyPageCache($entity_url, 'MISS');
         // Verify a cache hit.
         $this->verifyPageCache($entity_url, 'HIT');
     }
     if ($this->entity->getEntityType()->get('field_ui_base_route')) {
         // Verify that after modifying a configurable field on the entity, there
         // is a cache miss.
         $this->pass("Test modification of entity's configurable field.", 'Debug');
         $field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field';
         $field_storage = FieldStorageConfig::load($field_storage_name);
         $field_storage->save();
         $this->verifyPageCache($entity_url, 'MISS');
         // Verify a cache hit.
         $this->verifyPageCache($entity_url, 'HIT');
         // Verify that after modifying a configurable field on the entity, there
         // is a cache miss.
         $this->pass("Test modification of entity's configurable field.", 'Debug');
         $field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field';
         $field = FieldConfig::load($field_name);
         $field->save();
         $this->verifyPageCache($entity_url, 'MISS');
         // Verify a cache hit.
         $this->verifyPageCache($entity_url, 'HIT');
     }
     // Verify that after invalidating the entity's cache tag directly, there is
     // a cache miss.
     $this->pass("Test invalidation of entity's cache tag.", 'Debug');
     Cache::invalidateTags($this->entity->getCacheTagsToInvalidate());
     $this->verifyPageCache($entity_url, 'MISS');
     // Verify a cache hit.
     $this->verifyPageCache($entity_url, 'HIT');
     // Verify that after invalidating the generic entity type's view cache tag
     // directly, there is a cache miss.
     $this->pass("Test invalidation of entity's 'view' cache tag.", 'Debug');
     Cache::invalidateTags($view_cache_tag);
     $this->verifyPageCache($entity_url, 'MISS');
     // Verify a cache hit.
     $this->verifyPageCache($entity_url, 'HIT');
     // Verify that after deleting the entity, there is a cache miss.
     $this->pass('Test deletion of entity.', 'Debug');
     $this->entity->delete();
     $this->verifyPageCache($entity_url, 'MISS');
     $this->assertResponse(404);
 }
Exemplo n.º 18
0
 /**
  * Tests cache tags presence and invalidation of the entity when referenced.
  *
  * Tests the following cache tags:
  * - entity type view cache tag: "<entity type>_view"
  * - entity cache tag: "<entity type>:<entity ID>"
  * - entity type list cache tag: "<entity type>_list"
  * - referencing entity type view cache tag: "<referencing entity type>_view"
  * - referencing entity type cache tag: "<referencing entity type>:<referencing entity ID>"
  */
 public function testReferencedEntity()
 {
     $entity_type = $this->entity->getEntityTypeId();
     $referencing_entity_url = $this->referencingEntity->urlInfo('canonical');
     $non_referencing_entity_url = $this->nonReferencingEntity->urlInfo('canonical');
     $listing_url = Url::fromRoute('entity.entity_test.collection_referencing_entities', ['entity_reference_field_name' => $entity_type . '_reference', 'referenced_entity_type' => $entity_type, 'referenced_entity_id' => $this->entity->id()]);
     $empty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_empty', ['entity_type_id' => $entity_type]);
     $nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]);
     // The default cache contexts for rendered entities.
     $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
     $entity_cache_contexts = $default_cache_contexts;
     $page_cache_contexts = Cache::mergeContexts($default_cache_contexts, ['url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT]);
     // Cache tags present on every rendered page.
     // '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']);
     // 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'] : []);
     $page_cache_tags_referencing_entity = in_array('user.permissions', $this->getAccessCacheContextsForEntity($this->referencingEntity)) ? ['config:user.role.anonymous'] : [];
     $view_cache_tag = array();
     if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
         $view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags();
     }
     $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($entity_cache_contexts);
     $cache_context_tags = $context_metadata->getCacheTags();
     // Generate the cache tags for the (non) referencing entities.
     $referencing_entity_cache_tags = Cache::mergeTags($this->referencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
     // Includes the main entity's cache tags, since this entity references it.
     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->entity->getCacheTags());
     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity));
     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $view_cache_tag);
     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $cache_context_tags);
     $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, ['rendered']);
     $non_referencing_entity_cache_tags = Cache::mergeTags($this->nonReferencingEntity->getCacheTags(), \Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags());
     $non_referencing_entity_cache_tags = Cache::mergeTags($non_referencing_entity_cache_tags, ['rendered']);
     // Generate the cache tags for all two possible entity listing paths.
     // 1. list cache tag only (listing query has no match)
     // 2. list cache tag plus entity cache tag (listing query has a match)
     $empty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $page_cache_tags);
     $nonempty_entity_listing_cache_tags = Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->entity->getCacheTags());
     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $this->getAdditionalCacheTagsForEntityListing($this->entity));
     $nonempty_entity_listing_cache_tags = Cache::mergeTags($nonempty_entity_listing_cache_tags, $page_cache_tags);
     $this->pass("Test referencing entity.", 'Debug');
     $this->verifyPageCache($referencing_entity_url, 'MISS');
     // Verify a cache hit, but also the presence of the correct cache tags.
     $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags);
     $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity);
     $this->verifyPageCache($referencing_entity_url, 'HIT', $expected_tags);
     // Also verify the existence of an entity render cache entry.
     $cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full'];
     $cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
     $access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity);
     $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencingEntity);
     $redirected_cid = NULL;
     if (count($access_cache_contexts) || count($additional_cache_contexts)) {
         $cache_contexts = Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts);
         $cache_contexts = Cache::mergeContexts($cache_contexts, $access_cache_contexts);
         $redirected_cid = $this->createCacheId($cache_keys, $cache_contexts);
         $context_metadata = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts);
         $referencing_entity_cache_tags = Cache::mergeTags($referencing_entity_cache_tags, $context_metadata->getCacheTags());
     }
     $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid);
     $this->pass("Test non-referencing entity.", 'Debug');
     $this->verifyPageCache($non_referencing_entity_url, 'MISS');
     // Verify a cache hit, but also the presence of the correct cache tags.
     $this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags));
     // Also verify the existence of an entity render cache entry.
     $cache_keys = ['entity_view', 'entity_test', $this->nonReferencingEntity->id(), 'full'];
     $cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
     $this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
     $this->pass("Test listing of referencing entities.", 'Debug');
     // Prime the page cache for the listing of referencing entities.
     $this->verifyPageCache($listing_url, 'MISS');
     // Verify a cache hit, but also the presence of the correct cache tags.
     $expected_tags = Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags);
     $expected_tags = Cache::mergeTags($expected_tags, $page_cache_tags_referencing_entity);
     $this->verifyPageCache($listing_url, 'HIT', $expected_tags);
     $this->pass("Test empty listing.", 'Debug');
     // Prime the page cache for the empty listing.
     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
     // Verify a cache hit, but also the presence of the correct cache tags.
     $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
     // Verify the entity type's list cache contexts are present.
     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
     $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
     $this->pass("Test listing containing referenced entity.", 'Debug');
     // Prime the page cache for the listing containing the referenced entity.
     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags);
     // Verify a cache hit, but also the presence of the correct cache tags.
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
     // Verify the entity type's list cache contexts are present.
     $contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
     $this->assertEqual(Cache::mergeContexts($page_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
     // Verify that after modifying the referenced entity, there is a cache miss
     // for every route except the one for the non-referencing entity.
     $this->pass("Test modification of referenced entity.", 'Debug');
     $this->entity->save();
     $this->verifyPageCache($referencing_entity_url, 'MISS');
     $this->verifyPageCache($listing_url, 'MISS');
     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     // Verify cache hits.
     $this->verifyPageCache($referencing_entity_url, 'HIT');
     $this->verifyPageCache($listing_url, 'HIT');
     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     // Verify that after modifying the referencing entity, there is a cache miss
     // for every route except the ones for the non-referencing entity and the
     // empty entity listing.
     $this->pass("Test modification of referencing entity.", 'Debug');
     $this->referencingEntity->save();
     $this->verifyPageCache($referencing_entity_url, 'MISS');
     $this->verifyPageCache($listing_url, 'MISS');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
     // Verify cache hits.
     $this->verifyPageCache($referencing_entity_url, 'HIT');
     $this->verifyPageCache($listing_url, 'HIT');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     // Verify that after modifying the non-referencing entity, there is a cache
     // miss only for the non-referencing entity route.
     $this->pass("Test modification of non-referencing entity.", 'Debug');
     $this->nonReferencingEntity->save();
     $this->verifyPageCache($referencing_entity_url, 'HIT');
     $this->verifyPageCache($listing_url, 'HIT');
     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     $this->verifyPageCache($non_referencing_entity_url, 'MISS');
     // Verify cache hits.
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
         // Verify that after modifying the entity's display, there is a cache miss
         // for both the referencing entity, and the listing of referencing
         // entities, but not for any other routes.
         $referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId());
         $this->pass("Test modification of referenced entity's '{$referenced_entity_view_mode}' display.", 'Debug');
         $entity_display = entity_get_display($entity_type, $this->entity->bundle(), $referenced_entity_view_mode);
         $entity_display->save();
         $this->verifyPageCache($referencing_entity_url, 'MISS');
         $this->verifyPageCache($listing_url, 'MISS');
         $this->verifyPageCache($non_referencing_entity_url, 'HIT');
         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         // Verify cache hits.
         $this->verifyPageCache($referencing_entity_url, 'HIT');
         $this->verifyPageCache($listing_url, 'HIT');
     }
     if ($bundle_entity_type_id = $this->entity->getEntityType()->getBundleEntityType()) {
         // Verify that after modifying the corresponding bundle entity, there is a
         // cache miss for both the referencing entity, and the listing of
         // referencing entities, but not for any other routes.
         $this->pass("Test modification of referenced entity's bundle entity.", 'Debug');
         $bundle_entity = entity_load($bundle_entity_type_id, $this->entity->bundle());
         $bundle_entity->save();
         $this->verifyPageCache($referencing_entity_url, 'MISS');
         $this->verifyPageCache($listing_url, 'MISS');
         $this->verifyPageCache($non_referencing_entity_url, 'HIT');
         // Special case: entity types may choose to use their bundle entity type
         // cache tags, to avoid having excessively granular invalidation.
         $is_special_case = $bundle_entity->getCacheTags() == $this->entity->getCacheTags() && $bundle_entity->getEntityType()->getListCacheTags() == $this->entity->getEntityType()->getListCacheTags();
         if ($is_special_case) {
             $this->verifyPageCache($empty_entity_listing_url, 'MISS');
             $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
         } else {
             $this->verifyPageCache($empty_entity_listing_url, 'HIT');
             $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         }
         // Verify cache hits.
         $this->verifyPageCache($referencing_entity_url, 'HIT');
         $this->verifyPageCache($listing_url, 'HIT');
         if ($is_special_case) {
             $this->verifyPageCache($empty_entity_listing_url, 'HIT');
             $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         }
     }
     if ($this->entity->getEntityType()->get('field_ui_base_route')) {
         // Verify that after modifying a configurable field on the entity, there
         // is a cache miss.
         $this->pass("Test modification of referenced entity's configurable field.", 'Debug');
         $field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field';
         $field_storage = FieldStorageConfig::load($field_storage_name);
         $field_storage->save();
         $this->verifyPageCache($referencing_entity_url, 'MISS');
         $this->verifyPageCache($listing_url, 'MISS');
         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         $this->verifyPageCache($non_referencing_entity_url, 'HIT');
         // Verify cache hits.
         $this->verifyPageCache($referencing_entity_url, 'HIT');
         $this->verifyPageCache($listing_url, 'HIT');
         // Verify that after modifying a configurable field on the entity, there
         // is a cache miss.
         $this->pass("Test modification of referenced entity's configurable field.", 'Debug');
         $field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field';
         $field = FieldConfig::load($field_name);
         $field->save();
         $this->verifyPageCache($referencing_entity_url, 'MISS');
         $this->verifyPageCache($listing_url, 'MISS');
         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         $this->verifyPageCache($non_referencing_entity_url, 'HIT');
         // Verify cache hits.
         $this->verifyPageCache($referencing_entity_url, 'HIT');
         $this->verifyPageCache($listing_url, 'HIT');
     }
     // Verify that after invalidating the entity's cache tag directly, there is
     // a cache miss for every route except the ones for the non-referencing
     // entity and the empty entity listing.
     $this->pass("Test invalidation of referenced entity's cache tag.", 'Debug');
     Cache::invalidateTags($this->entity->getCacheTagsToInvalidate());
     $this->verifyPageCache($referencing_entity_url, 'MISS');
     $this->verifyPageCache($listing_url, 'MISS');
     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
     // Verify cache hits.
     $this->verifyPageCache($referencing_entity_url, 'HIT');
     $this->verifyPageCache($listing_url, 'HIT');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     // Verify that after invalidating the entity's list cache tag directly,
     // there is a cache miss for both the empty entity listing and the non-empty
     // entity listing routes, but not for other routes.
     $this->pass("Test invalidation of referenced entity's list cache tag.", 'Debug');
     Cache::invalidateTags($this->entity->getEntityType()->getListCacheTags());
     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
     $this->verifyPageCache($referencing_entity_url, 'HIT');
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     $this->verifyPageCache($listing_url, 'HIT');
     // Verify cache hits.
     $this->verifyPageCache($empty_entity_listing_url, 'HIT');
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
     if (!empty($view_cache_tag)) {
         // Verify that after invalidating the generic entity type's view cache tag
         // directly, there is a cache miss for both the referencing entity, and the
         // listing of referencing entities, but not for other routes.
         $this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug');
         Cache::invalidateTags($view_cache_tag);
         $this->verifyPageCache($referencing_entity_url, 'MISS');
         $this->verifyPageCache($listing_url, 'MISS');
         $this->verifyPageCache($non_referencing_entity_url, 'HIT');
         $this->verifyPageCache($empty_entity_listing_url, 'HIT');
         $this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
         // Verify cache hits.
         $this->verifyPageCache($referencing_entity_url, 'HIT');
         $this->verifyPageCache($listing_url, 'HIT');
     }
     // Verify that after deleting the entity, there is a cache miss for every
     // route except for the non-referencing entity one.
     $this->pass('Test deletion of referenced entity.', 'Debug');
     $this->entity->delete();
     $this->verifyPageCache($referencing_entity_url, 'MISS');
     $this->verifyPageCache($listing_url, 'MISS');
     $this->verifyPageCache($empty_entity_listing_url, 'MISS');
     $this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
     $this->verifyPageCache($non_referencing_entity_url, 'HIT');
     // 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']);
     $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);
     $this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags));
     $this->verifyPageCache($listing_url, 'HIT', $page_cache_tags);
     $this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
     $this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
 }
 /**
  * @covers ::process
  * @covers ::processCallback
  */
 function testProcess()
 {
     $cache_contexts = Cache::mergeContexts(['baz', 'qux']);
     $cache_tags = Cache::mergeTags(['foo', 'bar']);
     $map = [['100', TRUE, LanguageInterface::TYPE_CONTENT, '€100.00'], ['100.7654', TRUE, LanguageInterface::TYPE_CONTENT, '€100.77'], ['1.99', TRUE, LanguageInterface::TYPE_CONTENT, '€1.99'], ['2.99', TRUE, LanguageInterface::TYPE_CONTENT, '€2.99']];
     $currency = $this->getMock(CurrencyInterface::class);
     $currency->expects($this->any())->method('formatAmount')->willReturnMap($map);
     $currency->expects($this->atLeastOnce())->method('getCacheContexts')->willReturn($cache_contexts);
     $currency->expects($this->atLeastOnce())->method('getCacheTags')->willReturn($cache_tags);
     $this->currencyStorage->expects($this->any())->method('load')->with('EUR')->willReturn($currency);
     $this->input->expects($this->any())->method('parseAmount')->will($this->returnArgument(0));
     $langcode = $this->randomMachineName(2);
     $tokens_valid = ['[currency-localize:EUR:100]' => '€100.00', '[currency-localize:EUR:100.7654]' => '€100.77', '[currency-localize:EUR:1.99]' => '€1.99', '[currency-localize:EUR:2.99]' => '€2.99'];
     $tokens_invalid = ['[currency-localize]', '[currency-localize:]', '[currency-localize::]', '[currency-localize:EUR]', '[currency-localize:123:456]', '[currency-localize:123]'];
     foreach ($tokens_valid as $token => $replacement) {
         $result = $this->sut->process($token, $langcode);
         $this->assertInstanceOf(FilterProcessResult::class, $result);
         $this->assertSame($replacement, $result->getProcessedText());
         $this->assertSame($cache_contexts, $result->getCacheContexts());
         $this->assertSame($cache_tags, $result->getCacheTags());
     }
     foreach ($tokens_invalid as $token) {
         $result = $this->sut->process($token, $langcode);
         $this->assertInstanceOf(FilterProcessResult::class, $result);
         $this->assertSame($token, $result->getProcessedText());
         $this->assertEmpty($result->getCacheContexts());
         $this->assertEmpty($result->getCacheTags());
     }
 }
Exemplo n.º 20
0
 /**
  * {@inheritdoc}
  */
 public function build()
 {
     $build = [];
     // Default the max page age to permanent.
     $max_page_age = Cache::PERMANENT;
     $page = $this->executable->getPage();
     // Set default page cache keys that include the page and display.
     $page_cache_keys = ['page_manager_page', $page->id(), $this->configuration['uuid']];
     $page_cache_contexts = [];
     $contexts = $this->getContexts();
     foreach ($this->getRegionAssignments() as $region => $blocks) {
         if (!$blocks) {
             continue;
         }
         $region_name = Html::getClass("block-region-{$region}");
         $build['regions'][$region]['#prefix'] = '<div class="' . $region_name . '">';
         $build['regions'][$region]['#suffix'] = '</div>';
         /** @var $blocks \Drupal\Core\Block\BlockPluginInterface[] */
         $weight = 0;
         foreach ($blocks as $block_id => $block) {
             if ($block instanceof ContextAwarePluginInterface) {
                 $this->contextHandler()->applyContextMapping($block, $contexts);
             }
             if (!$block->access($this->account)) {
                 continue;
             }
             $max_age = $block->getCacheMaxAge();
             $block_build = ['#theme' => 'block', '#attributes' => [], '#weight' => $weight++, '#configuration' => $block->getConfiguration(), '#plugin_id' => $block->getPluginId(), '#base_plugin_id' => $block->getBaseId(), '#derivative_plugin_id' => $block->getDerivativeId(), '#block_plugin' => $block, '#pre_render' => [[$this, 'buildBlock']], '#cache' => ['keys' => ['page_manager_page', $page->id(), 'block', $block_id], 'tags' => Cache::mergeTags($page->getCacheTags(), $block->getCacheTags()), 'contexts' => $block->getCacheContexts(), 'max-age' => $max_age]];
             // Build the cache key and a list of all contexts for the whole page.
             $page_cache_keys[] = $block_id;
             $page_cache_contexts = Cache::mergeContexts($page_cache_contexts, $block_build['#cache']['contexts']);
             if (!empty($block_build['#configuration']['label'])) {
                 $block_build['#configuration']['label'] = SafeMarkup::checkPlain($block_build['#configuration']['label']);
             }
             // Update the page max age, set it to the lowest max age of all blocks.
             $max_page_age = Cache::mergeMaxAges($max_age, $max_page_age);
             $build['regions'][$region][$block_id] = $block_build;
         }
     }
     $build['#title'] = $this->renderPageTitle($this->configuration['page_title']);
     if ($max_page_age !== 0) {
         // If all blocks of this page can be cached, then the max page age is not
         // 0. In this case, we additionally cache the whole page, so we need
         // to fetch fewer caches. Also explicitly provide the cache contexts,
         // additional contexts might still bubble up from the block content, but
         // if not, then we save a cache redirection.
         // We don't have to set those values in case we can't cache all blocks,
         // as they will bubble up from the blocks.
         $build['regions']['#cache'] = ['keys' => $page_cache_keys, 'contexts' => $page_cache_contexts, 'max-age' => $max_page_age];
     }
     return $build;
 }
Exemplo n.º 21
0
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     $contexts = [];
     if ($this->options['expose_sort_order']) {
         // The sort order query arg is just important in case there is a exposed
         // sort order.
         $has_exposed_sort_handler = FALSE;
         /** @var \Drupal\views\Plugin\views\sort\SortPluginBase $sort_handler */
         foreach ($this->displayHandler->getHandlers('sort') as $sort_handler) {
             if ($sort_handler->isExposed()) {
                 $has_exposed_sort_handler = TRUE;
             }
         }
         if ($has_exposed_sort_handler) {
             $contexts[] = 'url.query_args:sort_order';
         }
     }
     // Merge in cache contexts for all exposed filters to prevent display of
     // cached forms.
     foreach ($this->displayHandler->getHandlers('filter') as $filter_hander) {
         if ($filter_hander->isExposed()) {
             $contexts = Cache::mergeContexts($contexts, $filter_hander->getCacheContexts());
         }
     }
     return $contexts;
 }
Exemplo n.º 22
0
 /**
  * See the docs for ::render().
  */
 protected function doRender(&$elements, $is_root_call = FALSE)
 {
     if (empty($elements)) {
         return '';
     }
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
         if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
             $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
         }
         $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
     }
     // Early-return nothing if user does not have access.
     if (isset($elements['#access'])) {
         // If #access is an AccessResultInterface object, we must apply it's
         // cacheability metadata to the render array.
         if ($elements['#access'] instanceof AccessResultInterface) {
             $this->addCacheableDependency($elements, $elements['#access']);
             if (!$elements['#access']->isAllowed()) {
                 return '';
             }
         } elseif ($elements['#access'] === FALSE) {
             return '';
         }
     }
     // Do not print elements twice.
     if (!empty($elements['#printed'])) {
         return '';
     }
     $context = $this->getCurrentRenderContext();
     if (!isset($context)) {
         throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
     }
     $context->push(new BubbleableMetadata());
     // Set the bubbleable rendering metadata that has configurable defaults, if:
     // - this is the root call, to ensure that the final render array definitely
     //   has these configurable defaults, even when no subtree is render cached.
     // - this is a render cacheable subtree, to ensure that the cached data has
     //   the configurable defaults (which may affect the ID and invalidation).
     if ($is_root_call || isset($elements['#cache']['keys'])) {
         $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
         if (isset($elements['#cache']['contexts'])) {
             $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
         } else {
             $elements['#cache']['contexts'] = $required_cache_contexts;
         }
     }
     // Try to fetch the prerendered element from cache, replace any placeholders
     // and return the final markup.
     if (isset($elements['#cache']['keys'])) {
         $cached_element = $this->renderCache->get($elements);
         if ($cached_element !== FALSE) {
             $elements = $cached_element;
             // Only when we're in a root (non-recursive) Renderer::render() call,
             // placeholders must be processed, to prevent breaking the render cache
             // in case of nested elements with #cache set.
             if ($is_root_call) {
                 $this->replacePlaceholders($elements);
             }
             // Mark the element markup as safe if is it a string.
             if (is_string($elements['#markup'])) {
                 $elements['#markup'] = SafeString::create($elements['#markup']);
             }
             // The render cache item contains all the bubbleable rendering metadata
             // for the subtree.
             $context->update($elements);
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $context->bubble();
             return $elements['#markup'];
         }
     }
     // Two-tier caching: track pre-bubbling elements' #cache for later
     // comparison.
     // @see \Drupal\Core\Render\RenderCacheInterface::get()
     // @see \Drupal\Core\Render\RenderCacheInterface::set()
     $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.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
         $elements += $this->elementInfo->getInfo($elements['#type']);
     }
     // First validate the usage of #lazy_builder; both of the next if-statements
     // use it if available.
     if (isset($elements['#lazy_builder'])) {
         // @todo Convert to assertions once https://www.drupal.org/node/2408013
         //   lands.
         if (!is_array($elements['#lazy_builder'])) {
             throw new \DomainException('The #lazy_builder property must have an array as a value.');
         }
         if (count($elements['#lazy_builder']) !== 2) {
             throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
         }
         if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) {
             return is_null($v) || is_scalar($v);
         }))) {
             throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
         }
         $children = Element::children($elements);
         if ($children) {
             throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children)));
         }
         $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed'];
         $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
         if (count($unsupported_keys)) {
             throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
         }
     }
     // If instructed to create a placeholder, and a #lazy_builder callback is
     // present (without such a callback, it would be impossible to replace the
     // placeholder), replace the current element with a placeholder.
     if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) {
         if (!isset($elements['#lazy_builder'])) {
             throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
         }
         $elements = $this->createPlaceholder($elements);
     }
     // Build the element if it is still empty.
     if (isset($elements['#lazy_builder'])) {
         $callable = $elements['#lazy_builder'][0];
         $args = $elements['#lazy_builder'][1];
         if (is_string($callable) && strpos($callable, '::') === FALSE) {
             $callable = $this->controllerResolver->getControllerFromDefinition($callable);
         }
         $new_elements = call_user_func_array($callable, $args);
         // Retain the original cacheability metadata, plus cache keys.
         CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements);
         if (isset($elements['#cache']['keys'])) {
             $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
         }
         $elements = $new_elements;
         $elements['#lazy_builder_built'] = TRUE;
     }
     // Make any final changes to the element before it is rendered. This means
     // that the $element or the children can be altered or corrected before the
     // element is rendered into the final text.
     if (isset($elements['#pre_render'])) {
         foreach ($elements['#pre_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements = call_user_func($callable, $elements);
         }
     }
     // Defaults for bubbleable rendering metadata.
     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
     $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     // Allow #pre_render to abort rendering.
     if (!empty($elements['#printed'])) {
         // The #printed element contains all the bubbleable rendering metadata for
         // the subtree.
         $context->update($elements);
         // #printed, so rendering is finished, all necessary info collected!
         $context->bubble();
         return '';
     }
     // Add any JavaScript state information associated with the element.
     if (!empty($elements['#states'])) {
         drupal_process_states($elements);
     }
     // Get the children of the element, sorted by weight.
     $children = Element::children($elements, TRUE);
     // Initialize this element's #children, unless a #pre_render callback
     // already preset #children.
     if (!isset($elements['#children'])) {
         $elements['#children'] = '';
     }
     if (!empty($elements['#markup'])) {
         // @todo Decide how to support non-HTML in the render API in
         //   https://www.drupal.org/node/2501313.
         $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']);
     }
     // Assume that if #theme is set it represents an implemented hook.
     $theme_is_implemented = isset($elements['#theme']);
     // Check the elements for insecure HTML and pass through sanitization.
     if (isset($elements)) {
         $markup_keys = array('#description', '#field_prefix', '#field_suffix');
         foreach ($markup_keys as $key) {
             if (!empty($elements[$key]) && is_scalar($elements[$key])) {
                 $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
             }
         }
     }
     // Call the element's #theme function if it is set. Then any children of the
     // element have to be rendered there. If the internal #render_children
     // property is set, do not call the #theme function to prevent infinite
     // recursion.
     if ($theme_is_implemented && !isset($elements['#render_children'])) {
         $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
         // If ThemeManagerInterface::render() returns FALSE this means that the
         // hook in #theme was not found in the registry and so we need to update
         // our flag accordingly. This is common for theme suggestions.
         $theme_is_implemented = $elements['#children'] !== FALSE;
     }
     // If #theme is not implemented or #render_children is set and the element
     // has an empty #children attribute, render the children now. This is the
     // same process as Renderer::render() but is inlined for speed.
     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
         foreach ($children as $key) {
             $elements['#children'] .= $this->doRender($elements[$key]);
         }
         $elements['#children'] = SafeString::create($elements['#children']);
     }
     // If #theme is not implemented and the element has raw #markup as a
     // fallback, prepend the content in #markup to #children. In this case
     // #children will contain whatever is provided by #pre_render prepended to
     // what is rendered recursively above. If #theme is implemented then it is
     // the responsibility of that theme implementation to render #markup if
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
         $elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']);
     }
     // Let the theme functions in #theme_wrappers add markup around the rendered
     // children.
     // #states and #attached have to be processed before #theme_wrappers,
     // because the #type 'page' render array from drupal_prepare_page() would
     // render the $page and wrap it into the html.html.twig template without the
     // attached assets otherwise.
     // If the internal #render_children property is set, do not call the
     // #theme_wrappers function(s) to prevent infinite recursion.
     if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
         foreach ($elements['#theme_wrappers'] as $key => $value) {
             // If the value of a #theme_wrappers item is an array then the theme
             // hook is found in the key of the item and the value contains attribute
             // overrides. Attribute overrides replace key/value pairs in $elements
             // for only this ThemeManagerInterface::render() call. This allows
             // #theme hooks and #theme_wrappers hooks to share variable names
             // without conflict or ambiguity.
             $wrapper_elements = $elements;
             if (is_string($key)) {
                 $wrapper_hook = $key;
                 foreach ($value as $attribute => $override) {
                     $wrapper_elements[$attribute] = $override;
                 }
             } else {
                 $wrapper_hook = $value;
             }
             $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
         }
     }
     // Filter the outputted content and make any last changes before the content
     // is sent to the browser. The changes are made on $content which allows the
     // outputted text to be filtered.
     if (isset($elements['#post_render'])) {
         foreach ($elements['#post_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
         }
     }
     // We store the resulting output in $elements['#markup'], to be consistent
     // with how render cached output gets stored. This ensures that placeholder
     // replacement logic gets the same data to work with, no matter if #cache is
     // disabled, #cache is enabled, there is a cache hit or miss.
     $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
     $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
     $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
     // We've rendered this element (and its subtree!), now update the context.
     $context->update($elements);
     // 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->renderCache->set($elements, $pre_bubbling_elements);
     }
     // Only when we're in a root (non-recursive) Renderer::render() call,
     // placeholders must be processed, to prevent breaking the render cache in
     // case of nested elements with #cache set.
     //
     // By running them here, we ensure that:
     // - they run when #cache is disabled,
     // - they run when #cache is enabled and there is a cache miss.
     // Only the case of a cache hit when #cache is enabled, is not handled here,
     // that is handled earlier in Renderer::render().
     if ($is_root_call) {
         $this->replacePlaceholders($elements);
         // @todo remove as part of https://www.drupal.org/node/2511330.
         if ($context->count() !== 1) {
             throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
         }
     }
     // Rendering is finished, all necessary info collected!
     $context->bubble();
     $elements['#printed'] = TRUE;
     return SafeString::create($elements['#markup']);
 }
 /**
  * Ensures that some cache contexts are present in the current response.
  *
  * @param string[] $expected_contexts
  *   The expected cache contexts.
  * @param string $message
  *   (optional) A verbose message to output.
  * @param bool $include_default_contexts
  *   (optional) Whether the default contexts should automatically be included.
  *
  * @return
  *   TRUE if the assertion succeeded, FALSE otherwise.
  */
 protected function assertCacheContexts(array $expected_contexts, $message = NULL, $include_default_contexts = TRUE)
 {
     if ($include_default_contexts) {
         $default_contexts = ['languages:language_interface', 'theme'];
         // Add the user.permission context to the list of default contexts except
         // when user is already there.
         if (!in_array('user', $expected_contexts)) {
             $default_contexts[] = 'user.permissions';
         }
         $expected_contexts = Cache::mergeContexts($expected_contexts, $default_contexts);
     }
     $actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts');
     sort($expected_contexts);
     sort($actual_contexts);
     $return = $this->assertIdentical($actual_contexts, $expected_contexts, $message);
     if (!$return) {
         debug('Unwanted cache contexts in response: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
         debug('Missing cache contexts in response: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
     }
     return $return;
 }
 /**
  * Tests the cache tags on the front page.
  *
  * @param bool $do_assert_views_caches
  *   Whether to check Views' result & output caches.
  */
 protected function doTestFrontPageViewCacheTags($do_assert_views_caches)
 {
     $view = Views::getView('frontpage');
     $view->setDisplay('page_1');
     $cache_contexts = ['user.node_grants:view', 'languages:' . LanguageInterface::TYPE_INTERFACE, 'user.permissions', 'theme', 'url.query_args', 'url.site'];
     $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
     // Test before there are any nodes.
     $empty_node_listing_cache_tags = ['config:views.view.frontpage', 'node_list'];
     $render_cache_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags);
     $render_cache_tags = Cache::mergeTags($render_cache_tags, ['config:system.site']);
     $this->assertViewsCacheTags($view, $empty_node_listing_cache_tags, $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']);
     $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1'), $cache_contexts, $expected_tags);
     // Create some nodes on the frontpage view. Add more than 10 nodes in order
     // to enable paging.
     $this->drupalCreateContentType(['type' => 'article']);
     for ($i = 0; $i < 15; $i++) {
         $node = Node::create(['body' => [['value' => $this->randomMachineName(32), 'format' => filter_default_format()]], 'type' => 'article', 'created' => $i, 'title' => $this->randomMachineName(8), 'nid' => $i + 1]);
         $node->enforceIsNew(TRUE);
         $node->save();
     }
     $cache_contexts = Cache::mergeContexts($cache_contexts, ['timezone']);
     $this->pass('First page');
     // First page.
     $first_page_result_cache_tags = ['config:views.view.frontpage', 'node_list', 'node:6', 'node:7', 'node:8', 'node:9', 'node:10', 'node:11', 'node:12', 'node:13', 'node:14', 'node:15'];
     $cache_context_tags = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($cache_contexts)->getCacheTags();
     $first_page_output_cache_tags = Cache::mergeTags($first_page_result_cache_tags, $cache_context_tags);
     $first_page_output_cache_tags = Cache::mergeTags($first_page_output_cache_tags, ['config:filter.format.plain_text', 'node_view', 'user_view', 'user:0']);
     $view->setDisplay('page_1');
     $view->setCurrentPage(0);
     $this->assertViewsCacheTags($view, $first_page_result_cache_tags, $do_assert_views_caches, $first_page_output_cache_tags);
     $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1'), $cache_contexts, Cache::mergeTags($first_page_output_cache_tags, ['rendered', 'config:user.role.anonymous']));
     // Second page.
     $this->pass('Second page');
     $this->assertPageCacheContextsAndTags(Url::fromRoute('view.frontpage.page_1', [], ['query' => ['page' => 1]]), $cache_contexts, ['node:1', 'node:2', 'node:3', 'node:4', 'node:5', 'config:filter.format.plain_text', 'config:views.view.frontpage', 'node_list', 'node_view', 'user_view', 'user:0', 'rendered', 'config:user.role.anonymous']);
     // Let's update a node title on the first page and ensure that the page
     // cache entry invalidates.
     $node = Node::load(10);
     $title = $node->getTitle() . 'a';
     $node->setTitle($title);
     $node->save();
     $this->drupalGet(Url::fromRoute('view.frontpage.page_1'));
     $this->assertText($title);
 }
 /**
  * Tests the basic translation workflow.
  */
 protected function doTestBasicTranslation()
 {
     // Create a new test entity with original values in the default language.
     $default_langcode = $this->langcodes[0];
     $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
     // Create the entity with the editor as owner, so that afterwards a new
     // translation is created by the translator and the translation author is
     // tested.
     $this->drupalLogin($this->editor);
     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
     $this->drupalLogin($this->translator);
     $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
     $this->assertTrue($entity, 'Entity found in the database.');
     $this->drupalGet($entity->urlInfo());
     $this->assertResponse(200, 'Entity URL is valid.');
     // Ensure that the content language cache context is not yet added to the
     // page.
     $this->assertCacheContexts($this->defaultCacheContexts);
     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
     $this->assertNoText('Source language', 'Source language column correctly hidden.');
     $translation = $this->getTranslation($entity, $default_langcode);
     foreach ($values[$default_langcode] as $property => $value) {
         $stored_value = $this->getValue($translation, $property, $default_langcode);
         $value = is_array($value) ? $value[0]['value'] : $value;
         $message = format_string('@property correctly stored in the default language.', array('@property' => $property));
         $this->assertEqual($stored_value, $value, $message);
     }
     // Add a content translation.
     $langcode = 'it';
     $language = ConfigurableLanguage::load($langcode);
     $values[$langcode] = $this->getNewEntityValues($langcode);
     $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode], array('language' => $language));
     $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
     // Assert that HTML is escaped in "all languages" in UI after SafeMarkup
     // change.
     if ($this->testHTMLEscapeForAllLanguages) {
         $this->assertNoRaw('&lt;span class=&quot;translation-entity-all-languages&quot;&gt;(all languages)&lt;/span&gt;');
         $this->assertRaw('<span class="translation-entity-all-languages">(all languages)</span>');
     }
     // Ensure that the content language cache context is not yet added to the
     // page.
     $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
     $this->drupalGet($entity->urlInfo());
     $this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts));
     // Reset the cache of the entity, so that the new translation gets the
     // updated values.
     $metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
     $metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
     $author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
     if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
         $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(), SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', array('@langcode' => $langcode)));
         $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(), SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.', array('@target' => $langcode, '@source' => $default_langcode)));
     } else {
         $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), 'Author of the entity remained untouched after translation for non translatable owner field.');
     }
     $created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
     if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
         $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(), SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.', array('@target' => $langcode, '@source' => $default_langcode)));
     } else {
         $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.');
     }
     if ($this->testLanguageSelector) {
         $this->assertNoFieldByXPath('//select[@id="edit-langcode-0-value"]', NULL, 'Language selector correctly disabled on translations.');
     }
     $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
     $this->assertNoText('Source language', 'Source language column correctly hidden.');
     // Switch the source language.
     $langcode = 'fr';
     $language = ConfigurableLanguage::load($langcode);
     $source_langcode = 'it';
     $edit = array('source_langcode[source]' => $source_langcode);
     $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode], array('language' => $language));
     // This does not save anything, it merely reloads the form and fills in the
     // fields with the values from the different source language.
     $this->drupalPostForm($add_url, $edit, t('Change'));
     $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.');
     // Add another translation and mark the other ones as outdated.
     $values[$langcode] = $this->getNewEntityValues($langcode);
     $edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE);
     $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [$entity->getEntityTypeId() => $entity->id(), 'source' => $source_langcode, 'target' => $langcode], array('language' => $language));
     $this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
     $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
     $this->drupalGet($entity->urlInfo('drupal:content-translation-overview'));
     $this->assertText('Source language', 'Source language column correctly shown.');
     // Check that the entered values have been correctly stored.
     foreach ($values as $langcode => $property_values) {
         $translation = $this->getTranslation($entity, $langcode);
         foreach ($property_values as $property => $value) {
             $stored_value = $this->getValue($translation, $property, $langcode);
             $value = is_array($value) ? $value[0]['value'] : $value;
             $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode));
             $this->assertEqual($stored_value, $value, $message);
         }
     }
 }
Exemplo n.º 26
0
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     $contexts = [];
     // By definition arguments depends on the URL.
     // @todo Once contexts are properly injected into block views we could pull
     //   the information from there.
     $contexts[] = 'url';
     // Asks all subplugins (argument defaults, argument validator and styles).
     if (($plugin = $this->getPlugin('argument_default')) && $plugin instanceof CacheableDependencyInterface) {
         $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
     }
     if (($plugin = $this->getPlugin('argument_validator')) && $plugin instanceof CacheableDependencyInterface) {
         $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
     }
     if (($plugin = $this->getPlugin('style')) && $plugin instanceof CacheableDependencyInterface) {
         $contexts = Cache::mergeContexts($contexts, $plugin->getCacheContexts());
     }
     return $contexts;
 }
Exemplo n.º 27
0
 /**
  * Gets the render cache for a given view.
  *
  * @param \Drupal\views\ViewExecutable $view
  *   The view.
  *
  * @return array|FALSE
  *   The render cache result or FALSE if not existent.
  */
 protected function getRenderCache(ViewExecutable $view)
 {
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
     $view->element = ['#cache' => []];
     $build = $view->buildRenderable();
     $build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
     return $render_cache->get($build);
 }
Exemplo n.º 28
0
 /**
  * {@inheritdoc}
  */
 public function getCacheContexts()
 {
     return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
 }
Exemplo n.º 29
0
 /**
  * Tests REST export with views render caching enabled.
  */
 public function testRestRenderCaching()
 {
     $this->drupalLogin($this->adminUser);
     /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
     $render_cache = \Drupal::service('render_cache');
     // Enable render caching for the views.
     /** @var \Drupal\views\ViewEntityInterface $storage */
     $storage = View::load('test_serializer_display_entity');
     $options =& $storage->getDisplay('default');
     $options['display_options']['cache'] = ['type' => 'tag'];
     $storage->save();
     $original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1');
     // Ensure that there is no corresponding render cache item yet.
     $original['#cache'] += ['contexts' => []];
     $original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
     $cache_tags = ['config:views.view.test_serializer_display_entity', 'entity_test:1', 'entity_test:10', 'entity_test:2', 'entity_test:3', 'entity_test:4', 'entity_test:5', 'entity_test:6', 'entity_test:7', 'entity_test:8', 'entity_test:9', 'entity_test_list'];
     $cache_contexts = ['entity_test_view_grants', 'languages:language_interface', 'theme', 'request_format'];
     $this->assertFalse($render_cache->get($original));
     // Request the page, once in XML and once in JSON to ensure that the caching
     // varies by it.
     $result1 = $this->drupalGetJSON('test/serialize/entity');
     $this->addRequestWithFormat('json');
     $this->assertHeader('content-type', 'application/json');
     $this->assertCacheContexts($cache_contexts);
     $this->assertCacheTags($cache_tags);
     $this->assertTrue($render_cache->get($original));
     $result_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
     $this->addRequestWithFormat('xml');
     $this->assertHeader('content-type', 'text/xml; charset=UTF-8');
     $this->assertCacheContexts($cache_contexts);
     $this->assertCacheTags($cache_tags);
     $this->assertTrue($render_cache->get($original));
     // Ensure that the XML output is different from the JSON one.
     $this->assertNotEqual($result1, $result_xml);
     // Ensure that the cached page works.
     $result2 = $this->drupalGetJSON('test/serialize/entity');
     $this->addRequestWithFormat('json');
     $this->assertHeader('content-type', 'application/json');
     $this->assertEqual($result2, $result1);
     $this->assertCacheContexts($cache_contexts);
     $this->assertCacheTags($cache_tags);
     $this->assertTrue($render_cache->get($original));
     // Create a new entity and ensure that the cache tags are taken over.
     EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save();
     $result3 = $this->drupalGetJSON('test/serialize/entity');
     $this->addRequestWithFormat('json');
     $this->assertHeader('content-type', 'application/json');
     $this->assertNotEqual($result3, $result2);
     // Add the new entity cache tag and remove the first one, because we just
     // show 10 items in total.
     $cache_tags[] = 'entity_test:11';
     unset($cache_tags[array_search('entity_test:1', $cache_tags)]);
     $this->assertCacheContexts($cache_contexts);
     $this->assertCacheTags($cache_tags);
     $this->assertTrue($render_cache->get($original));
 }
Exemplo n.º 30
0
 /**
  * See the docs for ::render().
  */
 protected function doRender(&$elements, $is_root_call = FALSE)
 {
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
         if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
             $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
         }
         $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
     }
     // Early-return nothing if user does not have access.
     if (empty($elements) || isset($elements['#access']) && !$elements['#access']) {
         return '';
     }
     // Do not print elements twice.
     if (!empty($elements['#printed'])) {
         return '';
     }
     if (!isset(static::$stack)) {
         static::$stack = new \SplStack();
     }
     static::$stack->push(new BubbleableMetadata());
     // Set the bubbleable rendering metadata that has configurable defaults, if:
     // - this is the root call, to ensure that the final render array definitely
     //   has these configurable defaults, even when no subtree is render cached.
     // - this is a render cacheable subtree, to ensure that the cached data has
     //   the configurable defaults (which may affect the ID and invalidation).
     if ($is_root_call || isset($elements['#cache']['keys'])) {
         $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
         if (isset($elements['#cache']['contexts'])) {
             $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
         } else {
             $elements['#cache']['contexts'] = $required_cache_contexts;
         }
     }
     // Try to fetch the prerendered element from cache, run any
     // #post_render_cache callbacks and return the final markup.
     if (isset($elements['#cache']['keys'])) {
         $cached_element = $this->renderCache->get($elements);
         if ($cached_element !== FALSE) {
             $elements = $cached_element;
             // Only when we're not in a root (non-recursive) drupal_render() call,
             // #post_render_cache callbacks must be executed, to prevent breaking
             // the render cache in case of nested elements with #cache set.
             if ($is_root_call) {
                 $this->processPostRenderCache($elements);
             }
             // Mark the element markup as safe. If we have cached children, we need
             // to mark them as safe too. The parent markup contains the child
             // markup, so if the parent markup is safe, then the markup of the
             // individual children must be safe as well.
             $elements['#markup'] = SafeMarkup::set($elements['#markup']);
             if (!empty($elements['#cache_properties'])) {
                 foreach (Element::children($cached_element) as $key) {
                     SafeMarkup::set($cached_element[$key]['#markup']);
                 }
             }
             // The render cache item contains all the bubbleable rendering metadata
             // for the subtree.
             $this->updateStack($elements);
             // Render cache hit, so rendering is finished, all necessary info
             // collected!
             $this->bubbleStack();
             return $elements['#markup'];
         }
     }
     // Two-tier caching: track pre-bubbling elements' #cache for later
     // comparison.
     // @see \Drupal\Core\Render\RenderCacheInterface::get()
     // @see \Drupal\Core\Render\RenderCacheInterface::set()
     $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.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
         $elements += $this->elementInfo->getInfo($elements['#type']);
     }
     // Make any final changes to the element before it is rendered. This means
     // that the $element or the children can be altered or corrected before the
     // element is rendered into the final text.
     if (isset($elements['#pre_render'])) {
         foreach ($elements['#pre_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements = call_user_func($callable, $elements);
         }
     }
     // Defaults for bubbleable rendering metadata.
     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
     $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
     $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
     // Allow #pre_render to abort rendering.
     if (!empty($elements['#printed'])) {
         // The #printed element contains all the bubbleable rendering metadata for
         // the subtree.
         $this->updateStack($elements);
         // #printed, so rendering is finished, all necessary info collected!
         $this->bubbleStack();
         return '';
     }
     // Add any JavaScript state information associated with the element.
     if (!empty($elements['#states'])) {
         drupal_process_states($elements);
     }
     // Get the children of the element, sorted by weight.
     $children = Element::children($elements, TRUE);
     // Initialize this element's #children, unless a #pre_render callback
     // already preset #children.
     if (!isset($elements['#children'])) {
         $elements['#children'] = '';
     }
     // @todo Simplify after https://www.drupal.org/node/2273925.
     if (isset($elements['#markup'])) {
         $elements['#markup'] = SafeMarkup::set($elements['#markup']);
     }
     // Assume that if #theme is set it represents an implemented hook.
     $theme_is_implemented = isset($elements['#theme']);
     // Check the elements for insecure HTML and pass through sanitization.
     if (isset($elements)) {
         $markup_keys = array('#description', '#field_prefix', '#field_suffix');
         foreach ($markup_keys as $key) {
             if (!empty($elements[$key]) && is_scalar($elements[$key])) {
                 $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
             }
         }
     }
     // Call the element's #theme function if it is set. Then any children of the
     // element have to be rendered there. If the internal #render_children
     // property is set, do not call the #theme function to prevent infinite
     // recursion.
     if ($theme_is_implemented && !isset($elements['#render_children'])) {
         $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
         // If ThemeManagerInterface::render() returns FALSE this means that the
         // hook in #theme was not found in the registry and so we need to update
         // our flag accordingly. This is common for theme suggestions.
         $theme_is_implemented = $elements['#children'] !== FALSE;
     }
     // If #theme is not implemented or #render_children is set and the element
     // has an empty #children attribute, render the children now. This is the
     // same process as Renderer::render() but is inlined for speed.
     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
         foreach ($children as $key) {
             $elements['#children'] .= $this->doRender($elements[$key]);
         }
         $elements['#children'] = SafeMarkup::set($elements['#children']);
     }
     // If #theme is not implemented and the element has raw #markup as a
     // fallback, prepend the content in #markup to #children. In this case
     // #children will contain whatever is provided by #pre_render prepended to
     // what is rendered recursively above. If #theme is implemented then it is
     // the responsibility of that theme implementation to render #markup if
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
         $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
     }
     // Let the theme functions in #theme_wrappers add markup around the rendered
     // children.
     // #states and #attached have to be processed before #theme_wrappers,
     // because the #type 'page' render array from drupal_prepare_page() would
     // render the $page and wrap it into the html.html.twig template without the
     // attached assets otherwise.
     // If the internal #render_children property is set, do not call the
     // #theme_wrappers function(s) to prevent infinite recursion.
     if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
         foreach ($elements['#theme_wrappers'] as $key => $value) {
             // If the value of a #theme_wrappers item is an array then the theme
             // hook is found in the key of the item and the value contains attribute
             // overrides. Attribute overrides replace key/value pairs in $elements
             // for only this ThemeManagerInterface::render() call. This allows
             // #theme hooks and #theme_wrappers hooks to share variable names
             // without conflict or ambiguity.
             $wrapper_elements = $elements;
             if (is_string($key)) {
                 $wrapper_hook = $key;
                 foreach ($value as $attribute => $override) {
                     $wrapper_elements[$attribute] = $override;
                 }
             } else {
                 $wrapper_hook = $value;
             }
             $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
         }
     }
     // Filter the outputted content and make any last changes before the content
     // is sent to the browser. The changes are made on $content which allows the
     // outputted text to be filtered.
     if (isset($elements['#post_render'])) {
         foreach ($elements['#post_render'] as $callable) {
             if (is_string($callable) && strpos($callable, '::') === FALSE) {
                 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
             }
             $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
         }
     }
     // We store the resulting output in $elements['#markup'], to be consistent
     // with how render cached output gets stored. This ensures that
     // #post_render_cache callbacks get the same data to work with, no matter if
     // #cache is disabled, #cache is enabled, there is a cache hit or miss.
     $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
     $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
     $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
     // We've rendered this element (and its subtree!), now update the stack.
     $this->updateStack($elements);
     // 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->renderCache->set($elements, $pre_bubbling_elements);
     }
     // Only when we're in a root (non-recursive) drupal_render() call,
     // #post_render_cache callbacks must be executed, to prevent breaking the
     // render cache in case of nested elements with #cache set.
     //
     // By running them here, we ensure that:
     // - they run when #cache is disabled,
     // - they run when #cache is enabled and there is a cache miss.
     // Only the case of a cache hit when #cache is enabled, is not handled here,
     // that is handled earlier in Renderer::render().
     if ($is_root_call) {
         // We've already called ::updateStack() earlier, which updated both the
         // element and current stack frame. However,
         // Renderer::processPostRenderCache() can both change the element
         // further and create and render new child elements, so provide a fresh
         // stack frame to collect those additions, merge them back to the element,
         // and then update the current frame to match the modified element state.
         do {
             static::$stack->push(new BubbleableMetadata());
             $this->processPostRenderCache($elements);
             $post_render_additions = static::$stack->pop();
             $elements['#post_render_cache'] = NULL;
             BubbleableMetadata::createFromRenderArray($elements)->merge($post_render_additions)->applyTo($elements);
         } while (!empty($elements['#post_render_cache']));
         if (static::$stack->count() !== 1) {
             throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
         }
     }
     // Rendering is finished, all necessary info collected!
     $this->bubbleStack();
     $elements['#printed'] = TRUE;
     $elements['#markup'] = SafeMarkup::set($elements['#markup']);
     return $elements['#markup'];
 }