Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
71.43% |
5 / 7 |
CRAP | |
91.38% |
53 / 58 |
DefaultMenuLinkTreeManipulators | |
0.00% |
0 / 1 |
|
71.43% |
5 / 7 |
24.37 | |
91.38% |
53 / 58 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
checkAccess | |
100.00% |
1 / 1 |
5 | |
100.00% |
9 / 9 |
|||
checkNodeAccess | |
0.00% |
0 / 1 |
5.01 | |
93.75% |
15 / 16 |
|||
collectNodeLinks | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
menuLinkCheckAccess | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
generateIndexAndSort | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
flatten | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators. | |
*/ | |
namespace Drupal\Core\Menu; | |
use Drupal\Core\Access\AccessManagerInterface; | |
use Drupal\Core\Access\AccessResult; | |
use Drupal\Core\Entity\Query\QueryFactory; | |
use Drupal\Core\Session\AccountInterface; | |
/** | |
* Provides a couple of menu link tree manipulators. | |
* | |
* This class provides menu link tree manipulators to: | |
* - perform render cached menu-optimized access checking | |
* - optimized node access checking | |
* - generate a unique index for the elements in a tree and sorting by it | |
* - flatten a tree (i.e. a 1-dimensional tree) | |
*/ | |
class DefaultMenuLinkTreeManipulators { | |
/** | |
* The access manager. | |
* | |
* @var \Drupal\Core\Access\AccessManagerInterface | |
*/ | |
protected $accessManager; | |
/** | |
* The current user. | |
* | |
* @var \Drupal\Core\Session\AccountInterface | |
*/ | |
protected $account; | |
/** | |
* The entity query factory. | |
* | |
* @var \Drupal\Core\Entity\Query\QueryFactory | |
*/ | |
protected $queryFactory; | |
/** | |
* Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object. | |
* | |
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager | |
* The access manager. | |
* @param \Drupal\Core\Session\AccountInterface $account | |
* The current user. | |
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory | |
* The entity query factory. | |
*/ | |
public function __construct(AccessManagerInterface $access_manager, AccountInterface $account, QueryFactory $query_factory) { | |
$this->accessManager = $access_manager; | |
$this->account = $account; | |
$this->queryFactory = $query_factory; | |
} | |
/** | |
* Performs access checks of a menu tree. | |
* | |
* Sets the 'access' property to AccessResultInterface objects on menu link | |
* tree elements. Descends into subtrees if the root of the subtree is | |
* accessible. Inaccessible subtrees are deleted, except the top-level | |
* inaccessible link, to be compatible with render caching. | |
* | |
* (This means that top-level inaccessible links are *not* removed; it is up | |
* to the code doing something with the tree to exclude inaccessible links, | |
* just like MenuLinkTree::build() does. This allows those things to specify | |
* the necessary cacheability metadata.) | |
* | |
* This is compatible with render caching, because of cache context bubbling: | |
* conditionally defined cache contexts (i.e. subtrees that are only | |
* accessible to some users) will bubble just like they do for render arrays. | |
* This is why inaccessible subtrees are deleted, except at the top-level | |
* inaccessible link: if we didn't keep the first (depth-wise) inaccessible | |
* link, we wouldn't be able to know which cache contexts would cause those | |
* subtrees to become accessible again, thus forcing us to conclude that that | |
* subtree is unconditionally inaccessible. | |
* | |
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree | |
* The menu link tree to manipulate. | |
* | |
* @return \Drupal\Core\Menu\MenuLinkTreeElement[] | |
* The manipulated menu link tree. | |
*/ | |
public function checkAccess(array $tree) { | |
foreach ($tree as $key => $element) { | |
// Other menu tree manipulators may already have calculated access, do not | |
// overwrite the existing value in that case. | |
if (!isset($element->access)) { | |
$tree[$key]->access = $this->menuLinkCheckAccess($element->link); | |
} | |
if ($tree[$key]->access->isAllowed()) { | |
if ($tree[$key]->subtree) { | |
$tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree); | |
} | |
} | |
else { | |
// Replace the link with an InaccessibleMenuLink object, so that if it | |
// is accidentally rendered, no sensitive information is divulged. | |
$tree[$key]->link = new InaccessibleMenuLink($tree[$key]->link); | |
// Always keep top-level inaccessible links: their cacheability metadata | |
// that indicates why they're not accessible by the current user must be | |
// bubbled. Otherwise, those subtrees will not be varied by any cache | |
// contexts at all, therefore forcing them to remain empty for all users | |
// unless some other part of the menu link tree accidentally varies by | |
// the same cache contexts. | |
// For deeper levels, we *can* remove the subtrees and therefore also | |
// not perform access checking on the subtree, thanks to bubbling/cache | |
// redirects. This therefore allows us to still do significantly less | |
// work in case of inaccessible subtrees, which is the entire reason why | |
// this deletes subtrees in the first place. | |
$tree[$key]->subtree = []; | |
} | |
} | |
return $tree; | |
} | |
/** | |
* Performs access checking for nodes in an optimized way. | |
* | |
* This manipulator should be added before the generic ::checkAccess() one, | |
* because it provides a performance optimization for ::checkAccess(). | |
* | |
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree | |
* The menu link tree to manipulate. | |
* | |
* @return \Drupal\Core\Menu\MenuLinkTreeElement[] | |
* The manipulated menu link tree. | |
*/ | |
public function checkNodeAccess(array $tree) { | |
$node_links = array(); | |
$this->collectNodeLinks($tree, $node_links); | |
if ($node_links) { | |
$nids = array_keys($node_links); | |
$query = $this->queryFactory->get('node'); | |
$query->condition('nid', $nids, 'IN'); | |
// Allows admins to view all nodes, by both disabling node_access | |
// query rewrite as well as not checking for the node status. The | |
// 'view own unpublished nodes' permission is ignored to not require cache | |
// entries per user. | |
$access_result = AccessResult::allowed()->cachePerPermissions(); | |
if ($this->account->hasPermission('bypass node access')) { | |
$query->accessCheck(FALSE); | |
} | |
else { | |
$access_result->addCacheContexts(['user.node_grants:view']); | |
$query->condition('status', NODE_PUBLISHED); | |
} | |
$nids = $query->execute(); | |
foreach ($nids as $nid) { | |
foreach ($node_links[$nid] as $key => $link) { | |
$node_links[$nid][$key]->access = $access_result; | |
} | |
} | |
} | |
return $tree; | |
} | |
/** | |
* Collects the node links in the menu tree. | |
* | |
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree | |
* The menu link tree to manipulate. | |
* @param array $node_links | |
* Stores references to menu link elements to effectively set access. | |
* | |
* @return \Drupal\Core\Menu\MenuLinkTreeElement[] | |
* The manipulated menu link tree. | |
*/ | |
protected function collectNodeLinks(array &$tree, array &$node_links) { | |
foreach ($tree as $key => &$element) { | |
if ($element->link->getRouteName() == 'entity.node.canonical') { | |
$nid = $element->link->getRouteParameters()['node']; | |
$node_links[$nid][$key] = $element; | |
// Deny access by default. checkNodeAccess() will re-add it. | |
$element->access = AccessResult::neutral(); | |
} | |
if ($element->hasChildren) { | |
$this->collectNodeLinks($element->subtree, $node_links); | |
} | |
} | |
} | |
/** | |
* Checks access for one menu link instance. | |
* | |
* @param \Drupal\Core\Menu\MenuLinkInterface $instance | |
* The menu link instance. | |
* | |
* @return \Drupal\Core\Access\AccessResultInterface | |
* The access result. | |
*/ | |
protected function menuLinkCheckAccess(MenuLinkInterface $instance) { | |
$access_result = NULL; | |
if ($this->account->hasPermission('link to any page')) { | |
$access_result = AccessResult::allowed(); | |
} | |
else { | |
$url = $instance->getUrlObject(); | |
// When no route name is specified, this must be an external link. | |
if (!$url->isRouted()) { | |
$access_result = AccessResult::allowed(); | |
} | |
else { | |
$access_result = $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $this->account, TRUE); | |
} | |
} | |
return $access_result->cachePerPermissions(); | |
} | |
/** | |
* Generates a unique index and sorts by it. | |
* | |
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree | |
* The menu link tree to manipulate. | |
* | |
* @return \Drupal\Core\Menu\MenuLinkTreeElement[] | |
* The manipulated menu link tree. | |
*/ | |
public function generateIndexAndSort(array $tree) { | |
$new_tree = array(); | |
foreach ($tree as $key => $v) { | |
if ($tree[$key]->subtree) { | |
$tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree); | |
} | |
$instance = $tree[$key]->link; | |
// The weights are made a uniform 5 digits by adding 50000 as an offset. | |
// After $this->menuLinkCheckAccess(), $instance->getTitle() has the | |
// localized or translated title. Adding the plugin id to the end of the | |
// index insures that it is unique. | |
$new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; | |
} | |
ksort($new_tree); | |
return $new_tree; | |
} | |
/** | |
* Flattens the tree to a single level. | |
* | |
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree | |
* The menu link tree to manipulate. | |
* | |
* @return \Drupal\Core\Menu\MenuLinkTreeElement[] | |
* The manipulated menu link tree. | |
*/ | |
public function flatten(array $tree) { | |
foreach ($tree as $key => $element) { | |
if ($tree[$key]->subtree) { | |
$tree += $this->flatten($tree[$key]->subtree); | |
} | |
$tree[$key]->subtree = array(); | |
} | |
return $tree; | |
} | |
} |