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