Version 1
[yaffs-website] / web / core / lib / Drupal / Core / Menu / DefaultMenuLinkTreeManipulators.php
1 <?php
2
3 namespace Drupal\Core\Menu;
4
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;
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  */
20 class 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 that
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->condition('nid', $nids, 'IN');
140
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
144       // entries per user.
145       $access_result = AccessResult::allowed()->cachePerPermissions();
146       if ($this->account->hasPermission('bypass node access')) {
147         $query->accessCheck(FALSE);
148       }
149       else {
150         $access_result->addCacheContexts(['user.node_grants:view']);
151         $query->condition('status', NodeInterface::PUBLISHED);
152       }
153
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;
158         }
159       }
160     }
161
162     return $tree;
163   }
164
165   /**
166    * Collects the node links in the menu tree.
167    *
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.
172    *
173    * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
174    *   The manipulated menu link tree.
175    */
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();
183       }
184       if ($element->hasChildren) {
185         $this->collectNodeLinks($element->subtree, $node_links);
186       }
187     }
188   }
189
190   /**
191    * Checks access for one menu link instance.
192    *
193    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
194    *   The menu link instance.
195    *
196    * @return \Drupal\Core\Access\AccessResultInterface
197    *   The access result.
198    */
199   protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
200     $access_result = NULL;
201     if ($this->account->hasPermission('link to any page')) {
202       $access_result = AccessResult::allowed();
203     }
204     else {
205       $url = $instance->getUrlObject();
206
207       // When no route name is specified, this must be an external link.
208       if (!$url->isRouted()) {
209         $access_result = AccessResult::allowed();
210       }
211       else {
212         $access_result = $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->account, TRUE);
213       }
214     }
215     return $access_result->cachePerPermissions();
216   }
217
218   /**
219    * Generates a unique index and sorts by it.
220    *
221    * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
222    *   The menu link tree to manipulate.
223    *
224    * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
225    *   The manipulated menu link tree.
226    */
227   public function generateIndexAndSort(array $tree) {
228     $new_tree = [];
229     foreach ($tree as $key => $v) {
230       if ($tree[$key]->subtree) {
231         $tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree);
232       }
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];
239     }
240     ksort($new_tree);
241     return $new_tree;
242   }
243
244   /**
245    * Flattens the tree to a single level.
246    *
247    * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
248    *   The menu link tree to manipulate.
249    *
250    * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
251    *   The manipulated menu link tree.
252    */
253   public function flatten(array $tree) {
254     foreach ($tree as $key => $element) {
255       if ($tree[$key]->subtree) {
256         $tree += $this->flatten($tree[$key]->subtree);
257       }
258       $tree[$key]->subtree = [];
259     }
260     return $tree;
261   }
262
263 }