3 namespace Drupal\Core\Menu;
5 use Drupal\Core\Access\AccessManagerInterface;
6 use Drupal\Core\Access\AccessResult;
7 use Drupal\Core\Entity\EntityTypeManagerInterface;
8 use Drupal\Core\Session\AccountInterface;
9 use Drupal\node\NodeInterface;
12 * Provides a couple of menu link tree manipulators.
14 * This class provides menu link tree manipulators to:
15 * - perform render cached menu-optimized access checking
16 * - optimized node access checking
17 * - generate a unique index for the elements in a tree and sorting by it
18 * - flatten a tree (i.e. a 1-dimensional tree)
20 class DefaultMenuLinkTreeManipulators {
25 * @var \Drupal\Core\Access\AccessManagerInterface
27 protected $accessManager;
32 * @var \Drupal\Core\Session\AccountInterface
37 * The entity type manager.
39 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
41 protected $entityTypeManager;
44 * Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object.
46 * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
48 * @param \Drupal\Core\Session\AccountInterface $account
50 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
51 * The entity type manager.
53 public function __construct(AccessManagerInterface $access_manager, AccountInterface $account, EntityTypeManagerInterface $entity_type_manager) {
54 $this->accessManager = $access_manager;
55 $this->account = $account;
56 $this->entityTypeManager = $entity_type_manager;
60 * Performs access checks of a menu tree.
62 * Sets the 'access' property to AccessResultInterface objects on menu link
63 * tree elements. Descends into subtrees if the root of the subtree is
64 * accessible. Inaccessible subtrees are deleted, except the top-level
65 * inaccessible link, to be compatible with render caching.
67 * (This means that top-level inaccessible links are *not* removed; it is up
68 * to the code doing something with the tree to exclude inaccessible links,
69 * just like MenuLinkTree::build() does. This allows those things to specify
70 * the necessary cacheability metadata.)
72 * This is compatible with render caching, because of cache context bubbling:
73 * conditionally defined cache contexts (i.e. subtrees that are only
74 * accessible to some users) will bubble just like they do for render arrays.
75 * This is why inaccessible subtrees are deleted, except at the top-level
76 * inaccessible link: if we didn't keep the first (depth-wise) inaccessible
77 * link, we wouldn't be able to know which cache contexts would cause those
78 * subtrees to become accessible again, thus forcing us to conclude that that
79 * subtree is unconditionally inaccessible.
81 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
82 * The menu link tree to manipulate.
84 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
85 * The manipulated menu link tree.
87 public function checkAccess(array $tree) {
88 foreach ($tree as $key => $element) {
89 // Other menu tree manipulators may already have calculated access, do not
90 // overwrite the existing value in that case.
91 if (!isset($element->access)) {
92 $tree[$key]->access = $this->menuLinkCheckAccess($element->link);
94 if ($tree[$key]->access->isAllowed()) {
95 if ($tree[$key]->subtree) {
96 $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
100 // Replace the link with an InaccessibleMenuLink object, so that if it
101 // is accidentally rendered, no sensitive information is divulged.
102 $tree[$key]->link = new InaccessibleMenuLink($tree[$key]->link);
103 // Always keep top-level inaccessible links: their cacheability metadata
104 // that indicates why they're not accessible by the current user must be
105 // bubbled. Otherwise, those subtrees will not be varied by any cache
106 // contexts at all, therefore forcing them to remain empty for all users
107 // unless some other part of the menu link tree accidentally varies by
108 // the same cache contexts.
109 // For deeper levels, we *can* remove the subtrees and therefore also
110 // not perform access checking on the subtree, thanks to bubbling/cache
111 // redirects. This therefore allows us to still do significantly less
112 // work in case of inaccessible subtrees, which is the entire reason why
113 // this deletes subtrees in the first place.
114 $tree[$key]->subtree = [];
121 * Performs access checking for nodes in an optimized way.
123 * This manipulator should be added before the generic ::checkAccess() one,
124 * because it provides a performance optimization for ::checkAccess().
126 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
127 * The menu link tree to manipulate.
129 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
130 * The manipulated menu link tree.
132 public function checkNodeAccess(array $tree) {
134 $this->collectNodeLinks($tree, $node_links);
136 $nids = array_keys($node_links);
138 $query = $this->entityTypeManager->getStorage('node')->getQuery();
139 $query->condition('nid', $nids, 'IN');
141 // Allows admins to view all nodes, by both disabling node_access
142 // query rewrite as well as not checking for the node status. The
143 // 'view own unpublished nodes' permission is ignored to not require cache
145 $access_result = AccessResult::allowed()->cachePerPermissions();
146 if ($this->account->hasPermission('bypass node access')) {
147 $query->accessCheck(FALSE);
150 $access_result->addCacheContexts(['user.node_grants:view']);
151 $query->condition('status', NodeInterface::PUBLISHED);
154 $nids = $query->execute();
155 foreach ($nids as $nid) {
156 foreach ($node_links[$nid] as $key => $link) {
157 $node_links[$nid][$key]->access = $access_result;
166 * Collects the node links in the menu tree.
168 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
169 * The menu link tree to manipulate.
170 * @param array $node_links
171 * Stores references to menu link elements to effectively set access.
173 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
174 * The manipulated menu link tree.
176 protected function collectNodeLinks(array &$tree, array &$node_links) {
177 foreach ($tree as $key => &$element) {
178 if ($element->link->getRouteName() == 'entity.node.canonical') {
179 $nid = $element->link->getRouteParameters()['node'];
180 $node_links[$nid][$key] = $element;
181 // Deny access by default. checkNodeAccess() will re-add it.
182 $element->access = AccessResult::neutral();
184 if ($element->hasChildren) {
185 $this->collectNodeLinks($element->subtree, $node_links);
191 * Checks access for one menu link instance.
193 * @param \Drupal\Core\Menu\MenuLinkInterface $instance
194 * The menu link instance.
196 * @return \Drupal\Core\Access\AccessResultInterface
199 protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
200 $access_result = NULL;
201 if ($this->account->hasPermission('link to any page')) {
202 $access_result = AccessResult::allowed();
205 $url = $instance->getUrlObject();
207 // When no route name is specified, this must be an external link.
208 if (!$url->isRouted()) {
209 $access_result = AccessResult::allowed();
212 $access_result = $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->account, TRUE);
215 return $access_result->cachePerPermissions();
219 * Generates a unique index and sorts by it.
221 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
222 * The menu link tree to manipulate.
224 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
225 * The manipulated menu link tree.
227 public function generateIndexAndSort(array $tree) {
229 foreach ($tree as $key => $v) {
230 if ($tree[$key]->subtree) {
231 $tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree);
233 $instance = $tree[$key]->link;
234 // The weights are made a uniform 5 digits by adding 50000 as an offset.
235 // After $this->menuLinkCheckAccess(), $instance->getTitle() has the
236 // localized or translated title. Adding the plugin id to the end of the
237 // index insures that it is unique.
238 $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key];
245 * Flattens the tree to a single level.
247 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
248 * The menu link tree to manipulate.
250 * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
251 * The manipulated menu link tree.
253 public function flatten(array $tree) {
254 foreach ($tree as $key => $element) {
255 if ($tree[$key]->subtree) {
256 $tree += $this->flatten($tree[$key]->subtree);
258 $tree[$key]->subtree = [];