DefaultMenuLinkTreeManipulators.php 9.3 KB

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