1<?php 2 3namespace Drupal\Core\Menu; 4 5use Drupal\Core\Access\AccessManagerInterface; 6use Drupal\Core\Access\AccessResult; 7use Drupal\Core\Entity\EntityTypeManagerInterface; 8use Drupal\Core\Session\AccountInterface; 9use Drupal\node\NodeInterface; 10 11/** 12 * Provides a couple of menu link tree manipulators. 13 * 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) 19 */ 20class DefaultMenuLinkTreeManipulators { 21 22 /** 23 * The access manager. 24 * 25 * @var \Drupal\Core\Access\AccessManagerInterface 26 */ 27 protected $accessManager; 28 29 /** 30 * The current user. 31 * 32 * @var \Drupal\Core\Session\AccountInterface 33 */ 34 protected $account; 35 36 /** 37 * The entity type manager. 38 * 39 * @var \Drupal\Core\Entity\EntityTypeManagerInterface 40 */ 41 protected $entityTypeManager; 42 43 /** 44 * Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object. 45 * 46 * @param \Drupal\Core\Access\AccessManagerInterface $access_manager 47 * The access manager. 48 * @param \Drupal\Core\Session\AccountInterface $account 49 * The current user. 50 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager 51 * The entity type manager. 52 */ 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; 57 } 58 59 /** 60 * Performs access checks of a menu tree. 61 * 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. 66 * 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.) 71 * 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 the 79 * subtree is unconditionally inaccessible. 80 * 81 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree 82 * The menu link tree to manipulate. 83 * 84 * @return \Drupal\Core\Menu\MenuLinkTreeElement[] 85 * The manipulated menu link tree. 86 */ 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); 93 } 94 if ($tree[$key]->access->isAllowed()) { 95 if ($tree[$key]->subtree) { 96 $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree); 97 } 98 } 99 else { 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 = []; 115 } 116 } 117 return $tree; 118 } 119 120 /** 121 * Performs access checking for nodes in an optimized way. 122 * 123 * This manipulator should be added before the generic ::checkAccess() one, 124 * because it provides a performance optimization for ::checkAccess(). 125 * 126 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree 127 * The menu link tree to manipulate. 128 * 129 * @return \Drupal\Core\Menu\MenuLinkTreeElement[] 130 * The manipulated menu link tree. 131 */ 132 public function checkNodeAccess(array $tree) { 133 $node_links = []; 134 $this->collectNodeLinks($tree, $node_links); 135 if ($node_links) { 136 $nids = array_keys($node_links); 137 138 $query = $this->entityTypeManager->getStorage('node')->getQuery(); 139 $query->accessCheck(TRUE); 140 $query->condition('nid', $nids, 'IN'); 141 142 // Allows admins to view all nodes, by both disabling node_access 143 // query rewrite as well as not checking for the node status. The 144 // 'view own unpublished nodes' permission is ignored to not require cache 145 // entries per user. 146 $access_result = AccessResult::allowed()->cachePerPermissions(); 147 if ($this->account->hasPermission('bypass node access')) { 148 $query->accessCheck(FALSE); 149 } 150 else { 151 $access_result->addCacheContexts(['user.node_grants:view']); 152 $query->condition('status', NodeInterface::PUBLISHED); 153 } 154 155 $nids = $query->execute(); 156 foreach ($nids as $nid) { 157 foreach ($node_links[$nid] as $key => $link) { 158 $node_links[$nid][$key]->access = $access_result; 159 } 160 } 161 } 162 163 return $tree; 164 } 165 166 /** 167 * Collects the node links in the menu tree. 168 * 169 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree 170 * The menu link tree to manipulate. 171 * @param array $node_links 172 * Stores references to menu link elements to effectively set access. 173 */ 174 protected function collectNodeLinks(array &$tree, array &$node_links) { 175 foreach ($tree as $key => &$element) { 176 if ($element->link->getRouteName() == 'entity.node.canonical') { 177 $nid = $element->link->getRouteParameters()['node']; 178 $node_links[$nid][$key] = $element; 179 // Deny access by default. checkNodeAccess() will re-add it. 180 $element->access = AccessResult::neutral(); 181 } 182 if ($element->hasChildren) { 183 $this->collectNodeLinks($element->subtree, $node_links); 184 } 185 } 186 } 187 188 /** 189 * Checks access for one menu link instance. 190 * 191 * @param \Drupal\Core\Menu\MenuLinkInterface $instance 192 * The menu link instance. 193 * 194 * @return \Drupal\Core\Access\AccessResultInterface 195 * The access result. 196 */ 197 protected function menuLinkCheckAccess(MenuLinkInterface $instance) { 198 $access_result = NULL; 199 if ($this->account->hasPermission('link to any page')) { 200 $access_result = AccessResult::allowed(); 201 } 202 else { 203 $url = $instance->getUrlObject(); 204 205 // When no route name is specified, this must be an external link. 206 if (!$url->isRouted()) { 207 $access_result = AccessResult::allowed(); 208 } 209 else { 210 $access_result = $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->account, TRUE); 211 } 212 } 213 return $access_result->cachePerPermissions(); 214 } 215 216 /** 217 * Generates a unique index and sorts by it. 218 * 219 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree 220 * The menu link tree to manipulate. 221 * 222 * @return \Drupal\Core\Menu\MenuLinkTreeElement[] 223 * The manipulated menu link tree. 224 */ 225 public function generateIndexAndSort(array $tree) { 226 $new_tree = []; 227 foreach ($tree as $key => $v) { 228 if ($tree[$key]->subtree) { 229 $tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree); 230 } 231 $instance = $tree[$key]->link; 232 // The weights are made a uniform 5 digits by adding 50000 as an offset. 233 // After $this->menuLinkCheckAccess(), $instance->getTitle() has the 234 // localized or translated title. Adding the plugin id to the end of the 235 // index insures that it is unique. 236 $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; 237 } 238 ksort($new_tree); 239 return $new_tree; 240 } 241 242 /** 243 * Flattens the tree to a single level. 244 * 245 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree 246 * The menu link tree to manipulate. 247 * 248 * @return \Drupal\Core\Menu\MenuLinkTreeElement[] 249 * The manipulated menu link tree. 250 */ 251 public function flatten(array $tree) { 252 foreach ($tree as $key => $element) { 253 if ($tree[$key]->subtree) { 254 $tree += $this->flatten($tree[$key]->subtree); 255 } 256 $tree[$key]->subtree = []; 257 } 258 return $tree; 259 } 260 261} 262