Commit 9ae3c9fb authored by catch's avatar catch

Issue #1805054 by Wim Leers, Gábor Hojtsy, effulgentsia, jibran, jessebeach,...

Issue #1805054 by Wim Leers, Gábor Hojtsy, effulgentsia, jibran, jessebeach, catch, dawehner, Fabianx, sun, larowlan: Cache localized, access filtered, URL resolved, and rendered menu trees
parent 132564c7
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Session\AccountInterface;
......@@ -15,11 +16,10 @@
* Provides a couple of menu link tree manipulators.
*
* This class provides menu link tree manipulators to:
* - perform access checking
* - perform render cached menu-optimized access checking
* - optimized node access checking
* - generate a unique index for the elements in a tree and sorting by it
* - flatten a tree (i.e. a 1-dimensional tree)
* - extract a subtree of the given tree according to the active trail
*/
class DefaultMenuLinkTreeManipulators {
......@@ -63,12 +63,24 @@ public function __construct(AccessManagerInterface $access_manager, AccountInter
/**
* Performs access checks of a menu tree.
*
* Removes menu links from the given menu tree whose links are inaccessible
* for the current user, sets the 'access' property to TRUE on tree elements
* that are accessible for the current user.
* Sets the 'access' property to AccessResultInterface objects on menu link
* tree elements. Descends into subtrees if the root of the subtree is
* accessible. Inaccessible subtrees are deleted, except the top-level
* inaccessible link, to be compatible with render caching.
*
* Makes the resulting menu tree impossible to render cache, unless render
* caching per user is acceptable.
* (This means that top-level inaccessible links are *not* removed; it is up
* to the code doing something with the tree to exclude inaccessible links,
* just like MenuLinkTree::build() does. This allows those things to specify
* the necessary cacheability metadata.)
*
* This is compatible with render caching, because of cache context bubbling:
* conditionally defined cache contexts (i.e. subtrees that are only
* accessible to some users) will bubble just like they do for render arrays.
* This is why inaccessible subtrees are deleted, except at the top-level
* inaccessible link: if we didn't keep the first (depth-wise) inaccessible
* link, we wouldn't be able to know which cache contexts would cause those
* subtrees to become accessible again, thus forcing us to conclude that that
* subtree is unconditionally inaccessible.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
......@@ -83,13 +95,27 @@ public function checkAccess(array $tree) {
if (!isset($element->access)) {
$tree[$key]->access = $this->menuLinkCheckAccess($element->link);
}
if ($tree[$key]->access) {
if ($tree[$key]->access->isAllowed()) {
if ($tree[$key]->subtree) {
$tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
}
}
else {
unset($tree[$key]);
// Replace the link with an InaccessibleMenuLink object, so that if it
// is accidentally rendered, no sensitive information is divulged.
$tree[$key]->link = new InaccessibleMenuLink($tree[$key]->link);
// Always keep top-level inaccessible links: their cacheability metadata
// that indicates why they're not accessible by the current user must be
// bubbled. Otherwise, those subtrees will not be varied by any cache
// contexts at all, therefore forcing them to remain empty for all users
// unless some other part of the menu link tree accidentally varies by
// the same cache contexts.
// For deeper levels, we *can* remove the subtrees and therefore also
// not perform access checking on the subtree, thanks to bubbling/cache
// redirects. This therefore allows us to still do significantly less
// work in case of inaccessible subtrees, which is the entire reason why
// this deletes subtrees in the first place.
$tree[$key]->subtree = [];
}
}
return $tree;
......@@ -120,17 +146,19 @@ public function checkNodeAccess(array $tree) {
// query rewrite as well as not checking for the node status. The
// 'view own unpublished nodes' permission is ignored to not require cache
// entries per user.
$access_result = AccessResult::allowed()->cachePerPermissions();
if ($this->account->hasPermission('bypass node access')) {
$query->accessCheck(FALSE);
}
else {
$access_result->addCacheContexts(['user.node_grants:view']);
$query->condition('status', NODE_PUBLISHED);
}
$nids = $query->execute();
foreach ($nids as $nid) {
foreach ($node_links[$nid] as $key => $link) {
$node_links[$nid][$key]->access = TRUE;
$node_links[$nid][$key]->access = $access_result;
}
}
}
......@@ -155,7 +183,7 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
$nid = $element->link->getRouteParameters()['node'];
$node_links[$nid][$key] = $element;
// Deny access by default. checkNodeAccess() will re-add it.
$element->access = FALSE;
$element->access = AccessResult::neutral();
}
if ($element->hasChildren) {
$this->collectNodeLinks($element->subtree, $node_links);
......@@ -169,24 +197,27 @@ protected function collectNodeLinks(array &$tree, array &$node_links) {
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The menu link instance.
*
* @return bool
* TRUE if the current user can access the link, FALSE otherwise.
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
$access_result = NULL;
if ($this->account->hasPermission('link to any page')) {
return TRUE;
}
// Use the definition here since that's a lot faster than creating a Url
// object that we don't need.
$definition = $instance->getPluginDefinition();
// 'url' should only be populated for external links.
if (!empty($definition['url']) && empty($definition['route_name'])) {
$access = TRUE;
$access_result = AccessResult::allowed();
}
else {
$access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
// Use the definition here since that's a lot faster than creating a Url
// object that we don't need.
$definition = $instance->getPluginDefinition();
// 'url' should only be populated for external links.
if (!empty($definition['url']) && empty($definition['route_name'])) {
$access_result = AccessResult::allowed();
}
else {
$access_result = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account, TRUE);
}
}
return $access;
return $access_result->cachePerPermissions();
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Menu\InaccessibleMenuLink.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\SafeMarkup;
/**
* A menu link plugin for wrapping another menu link, in sensitive situations.
*
* @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
*/
class InaccessibleMenuLink extends MenuLinkBase {
/**
* The wrapped menu link.
*
* @var \Drupal\Core\Menu\MenuLinkInterface
*/
protected $wrappedLink;
/**
* Constructs a new InaccessibleMenuLink.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $wrapped_link
* The menu link to wrap.
*/
public function __construct(MenuLinkInterface $wrapped_link) {
$this->wrappedLink = $wrapped_link;
$plugin_definition = [
'route_name' => '<front>',
'route_parameters' => [],
'url' => NULL,
] + $this->wrappedLink->getPluginDefinition();
parent::__construct([], $this->wrappedLink->getPluginId(), $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->t('Inaccessible');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->wrappedLink->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->wrappedLink->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->wrappedLink->getCacheMaxAge();
}
/**
* {@inheritdoc}
*/
public function updateLink(array $new_definition_values, $persist) {
throw new PluginException(SafeMarkup::format('Inaccessible menu link plugins do not support updating'));
}
}
......@@ -8,6 +8,8 @@
namespace Drupal\Core\Menu;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -152,17 +154,97 @@ public function transform(array $tree, array $manipulators) {
/**
* {@inheritdoc}
*/
public function build(array $tree, $level = 0) {
public function build(array $tree) {
$tree_access_cacheability = new CacheableMetadata();
$tree_link_cacheability = new CacheableMetadata();
$items = $this->buildItems($tree, $tree_access_cacheability, $tree_link_cacheability);
$build = [];
// Apply the tree-wide gathered access cacheability metadata and link
// cacheability metadata to the render array. This ensures that the
// rendered menu is varied by the cache contexts that the access results
// and (dynamic) links depended upon, and invalidated by the cache tags
// that may change the values of the access results and links.
$tree_cacheability = $tree_access_cacheability->merge($tree_link_cacheability);
$tree_cacheability->applyTo($build);
if ($items) {
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Get the menu name from the last link.
$item = end($items);
$link = $item['original_link'];
$menu_name = $link->getMenuName();
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
$build['#items'] = $items;
// Set cache tag.
$build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
}
return $build;
}
/**
* Builds the #items property for a menu tree's renderable array.
*
* Helper function for ::build().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* A data structure representing the tree, as returned from
* MenuLinkTreeInterface::load().
* @param \Drupal\Core\Cache\CacheableMetadata &$tree_access_cacheability
* Internal use only. The aggregated cacheability metadata for the access
* results across the entire tree. Used when rendering the root level.
* @param \Drupal\Core\Cache\CacheableMetadata &$tree_link_cacheability
* Internal use only. The aggregated cacheability metadata for the menu
* links across the entire tree. Used when rendering the root level.
*
* @return array
* The value to use for the #items property of a renderable menu.
*
* @throws \DomainException
*/
protected function buildItems(array $tree, CacheableMetadata &$tree_access_cacheability, CacheableMetadata &$tree_link_cacheability) {
$items = array();
foreach ($tree as $data) {
$class = ['menu-item'];
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $data->link;
// Generally we only deal with visible links, but just in case.
if (!$link->isEnabled()) {
continue;
}
if ($data->access !== NULL && !$data->access instanceof AccessResultInterface) {
throw new \DomainException('MenuLinkTreeElement::access must be either NULL or an AccessResultInterface object.');
}
// Gather the access cacheability of every item in the menu link tree,
// including inaccessible items. This allows us to render cache the menu
// tree, yet still automatically vary the rendered menu by the same cache
// contexts that the access results vary by.
// However, if $data->access is not an AccessResultInterface object, this
// will still render the menu link, because this method does not want to
// require access checking to be able to render a menu tree.
if ($data->access instanceof AccessResultInterface) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($data->access));
}
// Gather the cacheability of every item in the menu link tree. Some links
// may be dynamic: they may have a dynamic text (e.g. a "Hi, <user>" link
// text, which would vary by 'user' cache context), or a dynamic route
// name or route parameters.
$tree_link_cacheability = $tree_link_cacheability->merge(CacheableMetadata::createFromObject($data->link));
// Only render accessible links.
if ($data->access instanceof AccessResultInterface && !$data->access->isAllowed()) {
continue;
}
$class = ['menu-item'];
// Set a class for the <li>-tag. Only set 'expanded' class if the link
// also has visible children within the current tree.
if ($data->hasChildren && !empty($data->subtree)) {
......@@ -176,14 +258,15 @@ public function build(array $tree, $level = 0) {
$class[] = 'menu-item--active-trail';
}
// Allow menu-specific theme overrides.
// Note: links are rendered in the menu.html.twig template; and they
// automatically bubble their associated cacheability metadata.
$element = array();
$element['attributes'] = new Attribute();
$element['attributes']['class'] = $class;
$element['title'] = $link->getTitle();
$element['url'] = $link->getUrlObject();
$element['url']->setOption('set_active_class', TRUE);
$element['below'] = $data->subtree ? $this->build($data->subtree, $level + 1) : array();
$element['below'] = $data->subtree ? $this->buildItems($data->subtree, $tree_access_cacheability, $tree_link_cacheability) : array();
if (isset($data->options)) {
$element['url']->setOptions(NestedArray::mergeDeep($element['url']->getOptions(), $data->options));
}
......@@ -192,26 +275,7 @@ public function build(array $tree, $level = 0) {
$items[$link->getPluginId()] = $element;
}
if (!$items) {
return array();
}
elseif ($level == 0) {
$build = array();
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Get the menu name from the last link.
$menu_name = $link->getMenuName();
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme'] = 'menu__' . strtr($menu_name, '-', '_');
$build['#items'] = $items;
// Set cache tag.
$build['#cache']['tags'][] = 'config:system.menu.' . $menu_name;
return $build;
}
else {
return $items;
}
return $items;
}
/**
......
......@@ -70,10 +70,10 @@ class MenuLinkTreeElement {
/**
* Whether this link is accessible by the current user.
*
* If the value is NULL the access was not determined yet, if Boolean it was
* determined already.
* If the value is NULL the access was not determined yet, if an access result
* object, it was determined already.
*
* @var bool|NULL
* @var \Drupal\Core\Access\AccessResultInterface|NULL
*/
public $access;
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Menu;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\StringTranslation\StringTranslationTrait;
......@@ -53,7 +54,7 @@ public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityManager
/**
* {@inheritdoc}
*/
public function getParentSelectOptions($id = '', array $menus = NULL) {
public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL) {
if (!isset($menus)) {
$menus = $this->getMenuOptions();
}
......@@ -72,7 +73,7 @@ public function getParentSelectOptions($id = '', array $menus = NULL) {
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
);
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit);
$this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit, $cacheability);
}
return $options;
}
......@@ -81,7 +82,8 @@ public function getParentSelectOptions($id = '', array $menus = NULL) {
* {@inheritdoc}
*/
public function parentSelectElement($menu_parent, $id = '', array $menus = NULL) {
$options = $this->getParentSelectOptions($id, $menus);
$options_cacheability = new CacheableMetadata();
$options = $this->getParentSelectOptions($id, $menus, $options_cacheability);
// If no options were found, there is nothing to select.
if ($options) {
$element = array(
......@@ -98,6 +100,7 @@ public function parentSelectElement($menu_parent, $id = '', array $menus = NULL)
// Only provide the default value if it is valid among the options.
$element += array('#default_value' => $menu_parent);
}
$options_cacheability->applyTo($element);
return $element;
}
return array();
......@@ -137,13 +140,29 @@ protected function getParentDepthLimit($id) {
* An excluded menu link.
* @param int $depth_limit
* The maximum depth of menu links considered for the select options.
* @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
* The object to add cacheability metadata to, if not NULL.
*/
protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit) {
protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit, CacheableMetadata &$cacheability = NULL) {
foreach ($tree as $element) {
if ($element->depth > $depth_limit) {
// Don't iterate through any links on this level.
break;
}
// Collect the cacheability metadata of the access result, as well as the
// link.
if ($cacheability) {
$cacheability = $cacheability
->merge(CacheableMetadata::createFromObject($element->access))
->merge(CacheableMetadata::createFromObject($element->link));
}
// Only show accessible links.
if (!$element->access->isAllowed()) {
continue;
}
$link = $element->link;
if ($link->getPluginId() != $exclude) {
$title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE);
......@@ -152,7 +171,7 @@ protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent,
}
$options[$menu_name . ':' . $link->getPluginId()] = $title;
if (!empty($element->subtree)) {
$this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit);
$this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit, $cacheability);
}
}
}
......
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Menu;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Defines an interface for menu selector form elements and menu link options.
*/
......@@ -21,12 +23,15 @@ interface MenuParentFormSelectorInterface {
* @param array $menus
* Optional array of menu names as keys and titles as values to limit
* the select options. If NULL, all menus will be included.
* @param \Drupal\Core\Cache\CacheableMetadata|NULL &$cacheability
* Optional cacheability metadata object, which will be populated based on
* the accessibility of the links and the cacheability of the links.
*
* @return array
* Keyed array where the keys are contain a menu name and parent ID and
* the values are a menu name or link title indented by depth.
*/
public function getParentSelectOptions($id = '', array $menus = NULL);
public function getParentSelectOptions($id = '', array $menus = NULL, CacheableMetadata &$cacheability = NULL);
/**
* Gets a form element to choose a menu and parent.
......
......@@ -177,4 +177,20 @@ public static function setAttributes(array &$element, array $map) {
}
}
/**
* Indicates whether the given element is empty.
*
* An element that only has #cache set is considered empty, because it will
* render to the empty string.
*
* @param array $elements
* The element.
*
* @return bool
* Whether the given element is empty.
*/
public static function isEmpty(array $elements) {
return empty($elements) || (count($elements) === 1 && array_keys($elements) === ['#cache']);
}
}
......@@ -9,9 +9,11 @@
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\Element;
/**
* Provides a Block view builder.
......@@ -103,7 +105,7 @@ public function buildBlock($build) {
// Remove the block entity from the render array, to ensure that blocks
// can be rendered without the block config entity.
unset($build['#block']);
if (!empty($content)) {
if ($content !== NULL && !Element::isEmpty($content)) {
// Place the $content returned by the block plugin into a 'content' child
// element, as a way to allow the plugin to have complete control of its
// properties and rendering (e.g., its own #theme) without conflicting
......@@ -122,6 +124,8 @@ public function buildBlock($build) {
}
$build['content'] = $content;
}
// Either the block's content is completely empty, or it consists only of
// cacheability metadata.
else {
// Abort rendering: render as the empty string and ensure this block is
// render cached, so we can avoid the work of having to repeatedly
......@@ -131,6 +135,15 @@ public function buildBlock($build) {
'#markup' => '',
'#cache' => $build['#cache'],
);
// If $content is not empty, then it contains cacheability metadata, and
// we must merge it with the existing cacheability metadata. This allows
// blocks to be empty, yet still bubble cacheability metadata, to indicate
// why they are empty.
if (!empty($content)) {
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromRenderArray($content))
->applyTo($build);
}
}
return $build;
}
......
......@@ -193,7 +193,7 @@ public function getCacheContexts() {
/**
* {@inheritdoc}
*
* @todo Make cacheable as part of https://www.drupal.org/node/1805054.
* @todo Make cacheable in https://www.drupal.org/node/2483181
*/
public function getCacheMaxAge() {
return 0;
......
......@@ -8,6 +8,7 @@
* used for navigation.
*/
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Link;
......@@ -402,7 +403,8 @@ function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_stat
// To avoid an 'illegal option' error after saving the form we have to load
// all available menu parents. Otherwise, it is not possible to dynamically
// add options to the list using ajax.
$options = $menu_parent_selector->getParentSelectOptions('');
$options_cacheability = new CacheableMetadata();
$options = $menu_parent_selector->getParentSelectOptions('', NULL, $options_cacheability);
$form['menu']['menu_parent'] = array(
'#type' => 'select',
'#title' => t('Default parent item'),
......@@ -411,6 +413,7 @@ function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_stat
'#description' => t('Choose the menu item to be the default parent for a new link in the content authoring form.'),
'#attributes' => array('class' => array('menu-title-select')),
);
$options_cacheability->applyTo($form['menu']['menu_parent']);
$form['actions']['submit']['#validate'][] = 'menu_ui_form_node_type_form_validate';
$form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
......
......@@ -60,6 +60,8 @@ public function getParentOptions(Request $request) {
$available_menus[$menu] = $menu;
}
}
// @todo Update this to use the optional $cacheability parameter, so that
// a cacheable JSON response can be sent.
$options = $this->menuParentSelector->getParentSelectOptions('', $available_menus);
return new JsonResponse($options);
......
......@@ -8,6 +8,7 @@
namespace Drupal\menu_ui;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Form\FormStateInterface;
......@@ -337,7 +338,15 @@ protected function buildOverviewForm(array &$form, FormStateInterface $form_stat
*/
protected function buildOverviewTreeForm($tree, $delta) {
$form = &$this->overviewTreeForm;
$tree_access_cacheability = new CacheableMetadata();
foreach ($tree as $element) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
// Only render accessible links.
if (!$element->access->isAllowed()) {
continue;
}
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($link) {
......@@ -419,6 +428,11 @@ protected function buildOverviewTreeForm($tree, $delta) {
$this->buildOverviewTreeForm($element->subtree, $delta);
}
}
$tree_access_cacheability
->merge(CacheableMetadata::createFromRenderArray($form))
->applyTo($form);
return $form;
}
......
......@@ -54,6 +54,9 @@ public function testMenuBlock() {
'config:block_list',
'config:block.block.' . $block->id(),
'config:system.menu.llama',
// The cache contexts associated with the (in)accessible menu links are
// bubbled.
'config:user.role.anonymous',
);
$this->verifyPageCache($url, 'HIT', $expected_tags);
......
......@@ -53,6 +53,13 @@ protected function setUp() {
* Test creating, editing, deleting menu links via node form widget.
*/
function testMenuNodeFormWidget() {
// Verify that cacheability metadata is bubbled from the menu link tree
// access checking that is performed when determining the "default parent
// item" options in menu_ui_form_node_type_form_alter(). The "log out" link
// adds the "user.roles:authenticated" cache context.
$this->drupalGet('admin/structure/types/manage/page');
$this->assertCacheContext('user.roles:authenticated');
// Disable the default main menu, so that no menus are enabled.
$edit = array(
'menu_options[main]' => FALSE,
......
......@@ -555,6 +555,9 @@ function testUnpublishedNodeMenuItem() {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/structure/menu/manage/' . $item->getMenuName());
$this->assertNoText($item->getTitle(), "Menu link pointing to unpublished node is only visible to users with 'bypass node access' permission");
// The cache contexts associated with the (in)accessible menu links are
// bubbled. See DefaultMenuLinkTreeManipulators::menuLinkCheckAccess().
$this->assertCacheContext('user.permissions');
}
/**
......
......@@ -78,7 +78,9 @@ function testPageCacheTags() {
'theme',
'timezone',
'user.permissions',
'user.roles',
// The cache contexts associated with the (in)accessible menu links are
// bubbled.
'user.roles:authenticated',
];
// Full node page 1.
......
......@@ -8,6 +8,7 @@
namespace Drupal\system\Controller;
use Drupal\Component\Serialization\Json;