diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php index 2624726..b95522d 100644 --- a/core/lib/Drupal/Core/Menu/ContextualLinkManager.php +++ b/core/lib/Drupal/Core/Menu/ContextualLinkManager.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Menu; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Core\Access\AccessManager; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -54,6 +55,13 @@ class ContextualLinkManager extends DefaultPluginManager { protected $controllerResolver; /** + * The access manager. + * + * @var \Drupal\Core\Access\AccessManager + */ + protected $accessManager; + + /** * Constructs a new ContextualLinkManager instance. * * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver @@ -64,14 +72,17 @@ class ContextualLinkManager extends DefaultPluginManager { * The cache backend. * @param \Drupal\Core\Language\LanguageManager $language_manager * The language manager. + * @param \Drupal\Core\Access\AccessManager $access_manager + * The access manager. */ - public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager) { + public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager) { $this->discovery = new YamlDiscovery('contextual_links', $module_handler->getModuleDirectories()); $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); $this->factory = new ContainerFactory($this); $this->controllerResolver = $controller_resolver; - $this->alterInfo($module_handler, 'contextual_links'); + $this->accessManager = $access_manager; + $this->alterInfo($module_handler, 'contextual_links_plugins'); $this->setCacheBackend($cache_backend, $language_manager, 'contextual_links_plugins'); } @@ -140,14 +151,25 @@ public function getContextualLinksArrayByGroup($group_name, array $route_paramet foreach ($this->getContextualLinkPluginsByGroup($group_name) as $plugin_id => $plugin_definition) { /** @var $plugin \Drupal\Core\Menu\ContextualLinkInterface */ $plugin = $this->createInstance($plugin_id); + $route_name = $plugin->getRouteName(); + + // Check access. + if (!$this->accessManager->checkNamedRoute($route_name, $route_parameters)) { + continue; + } + $links[$plugin_id] = array( - 'route_name' => $plugin->getRouteName(), + 'route_name' => $route_name, 'route_parameters' => $route_parameters, 'title' => $plugin->getTitle(), 'weight' => $plugin->getWeight(), 'localized_options' => $plugin->getOptions(), + 'access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters), ); } + + $this->moduleHandler->alter('contextual_links', $links, $group, $route_parameters); + return $links; } diff --git a/core/modules/block/custom_block/custom_block.module b/core/modules/block/custom_block/custom_block.module index 61420ca..56bc7c6 100644 --- a/core/modules/block/custom_block/custom_block.module +++ b/core/modules/block/custom_block/custom_block.module @@ -117,12 +117,6 @@ function custom_block_menu() { 'weight' => 0, 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['block/%custom_block/delete'] = array( - 'title' => 'Delete', - 'weight' => 1, - 'type' => MENU_LOCAL_TASK, - 'route_name' => 'custom_block.delete', - ); return $items; } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 55daae4..b6890f0 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -979,6 +979,62 @@ function hook_menu_contextual_links_alter(&$links, $router_item, $root_path) { } /** + * Alter contextual links before they are rendered. + * + * This hook is invoked by + * \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup(). + * The system-determined contextual links are passed in by reference. Additional + * links may be added or existing links can be altered. + * + * Each contextual link must at least contain: + * - title: The localized title of the link. + * - route_name: The route name of the link. + * - route_parameters: The route parameters of the link. + * - localized_options: An array of options to pass to url(). + * + * @param array $links + * An associative array containing contextual links for the given $group, + * as described above. The array keys are used to build CSS class names for + * contextual links and must therefore be unique for each set of contextual + * links. + * @param string $group + * The contextual links group name being requested. + * @param array $route_parameters. + * The route parameters passed to each route_name of the contextual links. + * So for example it is an array like that: + * @code + * node => $node->id(). + * @endcode + * + * @see \Drupal\Core\Menu\ContextualLinkManager + */ +function hook_contextual_links_alter(array &$links, $group, array $route_parameters) { + if ($group == 'menu') { + // Dynamically use the menu name for the title of the menu edit contextual + // link. + $menu = \Drupal::entityManager()->getStorageController('menu')->load($route_parameters['menu']); + $links['menu_edit']['title'] = 'Edit menu: ' . $menu->label(); + } +} + +/** + * Alters the plugin definition of contextual links. + * + * @param array $contextual_links + * An array of contextual_links plugin definitions, keyed by plugin ID. + * Each entry contains the following keys: + * - title: The displayed title of the link + * - route_name: The route_name of the contextual link to be displayed + * - group: The group under which the contextual links should be added to. + * Possible values are for example 'node' or 'menu'. + * + * @see \Drupal\Core\Menu\ContextualLinkManager + */ +function hook_contextual_links_plugins_alter(array &$contextual_links) { + $contextual_links['menu_edit']['title'] = 'Edit the menu'; +} + +/** * Perform alterations before a page is rendered. * * Use this hook when you want to remove or alter elements at the page diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index f7c97fc..6ca7e89 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -1,6 +1,6 @@ - + ./tests/* diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php index f1ae065..15db4d5 100644 --- a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php @@ -52,6 +52,20 @@ class ContextualLinkManagerTest extends UnitTestCase { */ protected $cacheBackend; + /** + * The mocked module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; + + /** + * The mocked access manager. + * + * @var \Drupal\Core\Access\AccessManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $accessManager; + public static function getInfo() { return array( 'name' => 'Contextual links manager.', @@ -71,6 +85,9 @@ protected function setUp() { $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface'); $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface'); $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->accessManager = $this->getMockBuilder('Drupal\Core\Access\AccessManager') + ->disableOriginalConstructor() + ->getMock(); $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'controllerResolver'); $property->setAccessible(TRUE); @@ -84,6 +101,11 @@ protected function setUp() { $property->setAccessible(TRUE); $property->setValue($this->contextualLinkManager, $this->factory); + + $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'accessManager'); + $property->setAccessible(TRUE); + $property->setValue($this->contextualLinkManager, $this->accessManager); + $language_manager = $this->getMockBuilder('Drupal\Core\Language\LanguageManager') ->disableOriginalConstructor() ->getMock(); @@ -91,6 +113,12 @@ protected function setUp() { ->method('getLanguage') ->will($this->returnValue(new Language(array('id' => 'en')))); + $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface'); + + $method = new \ReflectionMethod('Drupal\Core\Menu\ContextualLinkManager', 'alterInfo'); + $method->setAccessible(TRUE); + $method->invoke($this->contextualLinkManager, $this->moduleHandler, 'contextual_links_plugins'); + $this->contextualLinkManager->setCacheBackend($this->cacheBackend, $language_manager, 'contextual_links_plugins'); } @@ -195,7 +223,6 @@ public function testProcessDefinitionWithoutGroup() { $this->contextualLinkManager->processDefinition($definition, 'test_plugin'); } - /** * Tests the getContextualLinksArrayByGroup method. * @@ -236,6 +263,11 @@ public function testGetContextualLinksArrayByGroup() { ->method('getDefinitions') ->will($this->returnValue($definitions)); + $this->accessManager->expects($this->any()) + ->method('checkNamedRoute') + ->will($this->returnValue(TRUE)); + + // Setup mocking of the plugin factory. $map = array(); foreach ($definitions as $plugin_id => $definition) { $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface'); @@ -266,4 +298,96 @@ public function testGetContextualLinksArrayByGroup() { } } + /** + * Tests the access checking of the getContextualLinksArrayByGroup method. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinksArrayByGroup() + */ + public function testGetContextualLinksArrayByGroupAccessCheck() { + $definitions = array( + 'test_plugin1' => array( + 'id' => 'test_plugin1', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 1', + 'weight' => 0, + 'group' => 'group1', + 'route_name' => 'test_route', + 'options' => array(), + ), + 'test_plugin2' => array( + 'id' => 'test_plugin2', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 2', + 'weight' => 2, + 'group' => 'group1', + 'route_name' => 'test_route2', + 'options' => array('key' => 'value'), + ), + ); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $this->accessManager->expects($this->any()) + ->method('checkNamedRoute') + ->will($this->returnValueMap(array( + array('test_route', array('key' => 'value'), NULL, TRUE), + array('test_route2', array('key' => 'value'), NULL, FALSE), + ))); + + // Setup mocking of the plugin factory. + $map = array(); + foreach ($definitions as $plugin_id => $definition) { + $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface'); + $plugin->expects($this->any()) + ->method('getRouteName') + ->will($this->returnValue($definition['route_name'])); + $plugin->expects($this->any()) + ->method('getTitle') + ->will($this->returnValue($definition['title'])); + $plugin->expects($this->any()) + ->method('getWeight') + ->will($this->returnValue($definition['weight'])); + $plugin->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($definition['options'])); + $map[] = array($plugin_id, array(), $plugin); + } + $this->factory->expects($this->any()) + ->method('createInstance') + ->will($this->returnValueMap($map)); + + $result = $this->contextualLinkManager->getContextualLinksArrayByGroup('group1', array('key' => 'value')); + + // Ensure that access checking was respected. + $this->assertTrue(isset($result['test_plugin1'])); + $this->assertFalse(isset($result['test_plugin2'])); + } + + /** + * Tests the plugins alter hook. + */ + public function testPluginDefinitionAlter() { + $definitions['test_plugin'] = array( + 'id' => 'test_plugin', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin', + 'weight' => 2, + 'group' => 'group1', + 'route_name' => 'test_route', + 'options' => array('key' => 'value'), + ); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $this->moduleHandler->expects($this->once()) + ->method('alter') + ->with('contextual_links_plugins', $definitions); + + $this->contextualLinkManager->getDefinition('test_plugin'); + } + } diff --git a/core/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/Array.php b/core/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/Array.php index ebbf928..d715514 100644 --- a/core/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/Array.php +++ b/core/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/Array.php @@ -165,7 +165,7 @@ public function assertEquals($expected, $actual, $delta = 0, $canonicalize = FAL $expString, $actString, FALSE, - 'Failed asserting that two arrays are equal.' + 'Failed asserting that two arrays are equal. ' . print_r(array_diff_assoc($expected, $actual), TRUE) ); } }