diff --git a/core/core.services.yml b/core/core.services.yml index 65882af..d61c198 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -173,7 +173,7 @@ services: arguments: ['@container.namespaces'] plugin.manager.menu.local_action: class: Drupal\Core\Menu\LocalActionManager - arguments: ['@container.namespaces', '@controller_resolver', '@request', '@module_handler', '@cache.cache', '@language_manager'] + arguments: ['@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager', '@access_manager'] plugin.manager.menu.local_task: class: Drupal\Core\Menu\LocalTaskManager arguments: ['@container.namespaces', '@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager'] diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 0b59cfe..99976e7 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -1736,12 +1736,19 @@ function theme_menu_local_action($variables) { $link += array( 'href' => '', 'localized_options' => array(), + 'route_parameters' => array(), ); $link['localized_options']['attributes']['class'][] = 'button'; $link['localized_options']['attributes']['class'][] = 'button-action'; $output = '
  • '; - $output .= l($link['title'], $link['href'], $link['localized_options']); + // @todo Remove the is_plugin check once + if (!empty($link['route_name']) && !empty($link['is_plugin'])) { + $output .= Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link['localized_options']); + } + else { + $output .= l($link['title'], $link['href'], $link['localized_options']); + } $output .= "
  • "; return $output; @@ -2307,21 +2314,9 @@ function menu_secondary_local_tasks() { */ function menu_get_local_actions() { $links = menu_local_tasks(); - $router_item = menu_get_item(); + $route_name = Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME); $manager = Drupal::service('plugin.manager.menu.local_action'); - $local_actions = $manager->getActionsForRoute($router_item['route_name']); - foreach ($local_actions as $plugin) { - $route_path = $manager->getPath($plugin); - $action_router_item = menu_get_item($route_path); - $links['actions'][$route_path] = array( - '#theme' => 'menu_local_action', - '#link' => array( - 'title' => $manager->getTitle($plugin), - 'href' => $route_path, - ), - '#access' => $action_router_item['access'], - ); - } + $links['actions'] += $manager->getActionsForRoute($route_name); return $links['actions']; } diff --git a/core/lib/Drupal/Core/Annotation/Menu/LocalAction.php b/core/lib/Drupal/Core/Annotation/Menu/LocalAction.php deleted file mode 100644 index 3c4cdb3..0000000 --- a/core/lib/Drupal/Core/Annotation/Menu/LocalAction.php +++ /dev/null @@ -1,49 +0,0 @@ -generator = $generator; + $this->routeProvider = $route_provider; // This is available for subclasses that need to translate a dynamic title. - $this->t = $string_translation; + $this->stringTranslation = $string_translation; } /** @@ -62,11 +63,11 @@ public function __construct(TranslatorInterface $string_translation, UrlGenerat */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { return new static( - $container->get('string_translation'), - $container->get('url_generator'), $configuration, $plugin_id, - $plugin_definition + $plugin_definition, + $container->get('string_translation'), + $container->get('router.route_provider') ); } @@ -82,23 +83,56 @@ public function getRouteName() { */ public function getTitle() { // Subclasses may pull in the request or specific attributes as parameters. - return $this->pluginDefinition['title']; + return $this->t($this->pluginDefinition['title']); } /** * {@inheritdoc} */ - public function getPath() { - // Subclasses may set a request into the generator or use any desired method - // to generate the path. - // @todo - use the new method from https://drupal.org/node/2031353 - $path = $this->generator->generate($this->getRouteName()); - // In order to get the Drupal path the base URL has to be stripped off. - $base_url = $this->generator->getContext()->getBaseUrl(); - if (!empty($base_url) && strpos($path, $base_url) === 0) { - $path = substr($path, strlen($base_url)); + public function getRouteParameters(Request $request) { + $parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : array(); + $route = $this->routeProvider->getRouteByName($this->getRouteName()); + $variables = $route->compile()->getVariables(); + + // Noramlly the \Drupal\Core\ParamConverter\ParamConverterManager has + // processed the Request attributes, and in that case the _raw_variables + // attribute holds the original path strings keyed to the corresponding + // slugs in the path patterns. For example, if the route's path pattern is + // /filter/tips/{filter_format} and the path is /filter/tips/plain_text then + // $raw_variables->get('filter_format') == 'plain_text'. + + $raw_variables = $request->attributes->get('_raw_variables'); + + foreach ($variables as $name) { + if (isset($parameters[$name])) { + continue; + } + + if ($raw_variables && $raw_variables->has($name)) { + $parameters[$name] = $raw_variables->get($name); + } + elseif ($request->attributes->has($name)) { + $parameters[$name] = $request->attributes->get($name); + } } - return trim($path, '/'); + // The UrlGenerator will throw an exception if expected parameters are + // missing. This method should be overridden if that is possible. + return $parameters; } + /** + * {@inheritdoc} + */ + public function getOptions(Request $request) { + return (array) $this->pluginDefinition['options']; + } + + /** + * Translates a string to the current language or to a given language. + * + * See the t() documentation for details. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->stringTranslation->translate($string, $args, $options); + } } diff --git a/core/lib/Drupal/Core/Menu/LocalActionInterface.php b/core/lib/Drupal/Core/Menu/LocalActionInterface.php index 65287b6..4151160 100644 --- a/core/lib/Drupal/Core/Menu/LocalActionInterface.php +++ b/core/lib/Drupal/Core/Menu/LocalActionInterface.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Menu; +use Symfony\Component\HttpFoundation\Request; + /** * Defines an interface for menu local actions. */ @@ -21,6 +23,37 @@ public function getRouteName(); /** + * Returns the route parameters needed to render a link for the local task. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return array + * An array of parameter names and values. + * + * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate() + */ + public function getRouteParameters(Request $request); + + /** + * Returns the weight of the local task. +@@ -51,14 +55,17 @@ public function getPath(); + public function getWeight(); + + /** + * Returns options for rendering a link to the local task. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return array + * An associative array of options. + * + * @see \Drupal\Core\Utility\LinkGeneratorInterface::generate() + */ + public function getOptions(Request $request); + + /** * Returns the localized title to be shown for this action. * * Subclasses may add optional arguments like NodeInterface $node = NULL that @@ -33,17 +66,6 @@ public function getRouteName(); */ public function getTitle(); - /** - * Return an internal Drupal path to use when linking to the action. - * - * Subclasses may add arguments for request attributes which will then be - * automatically supplied by the controller resolver. - * - * @return string - * The path to use when linking to the action. - * - * @see \Drupal\Core\Menu\LocalActionManager::getPath() - */ - public function getPath(); + } diff --git a/core/lib/Drupal/Core/Menu/LocalActionManager.php b/core/lib/Drupal/Core/Menu/LocalActionManager.php index c99dd83..8abf522 100644 --- a/core/lib/Drupal/Core/Menu/LocalActionManager.php +++ b/core/lib/Drupal/Core/Menu/LocalActionManager.php @@ -7,11 +7,17 @@ namespace Drupal\Core\Menu; +use Drupal\Core\Access\AccessManager; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManager; use Drupal\Core\Menu\LocalActionInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Component\Plugin\Discovery\ProcessDecorator; +use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; +use Drupal\Core\Plugin\Discovery\YamlDiscovery; +use Drupal\Core\Plugin\Factory\ContainerFactory; +use Drupal\Core\Routing\RouteProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; @@ -26,6 +32,28 @@ class LocalActionManager extends DefaultPluginManager { /** + * Provides some default values for all local action plugin.s + * + * @var array + */ + protected $defaults = array( + // The ID. + 'id' => '', + // The static title for the local action. + 'title' => '', + // (Required) the route name used to generate a link. + 'route_name' => '', + // Default route parameters for generating links. + 'route_parameters' => array(), + // Associative array of link options. + 'options' => array(), + // The route names where this local action appears. + 'appears_on' => array(), + // Default class for local action implementations. + 'class' => 'Drupal\Core\Menu\LocalActionBase', + ); + + /** * A controller resolver object. * * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface @@ -40,6 +68,20 @@ class LocalActionManager extends DefaultPluginManager { protected $request; /** + * The route provider to load routes by name. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * The access manager. + * + * @var \Drupal\Core\Access\AccessManager + */ + protected $accessManager; + +/** * The plugin instances. * * @var array @@ -49,28 +91,33 @@ class LocalActionManager extends DefaultPluginManager { /** * Constructs a LocalActionManager object. * - * @param \Traversable $namespaces - * An object that implements \Traversable which contains the root paths - * keyed by the corresponding namespace to look for plugin implementations. * @param \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface $controller_resolver * An object to use in introspecting route methods. * @param \Symfony\Component\HttpFoundation\Request $request * The request object to use for building titles and paths for plugin * instances. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * Cache backend instance to use. * @param \Drupal\Core\Language\LanguageManager $language_manager * The language manager. + * @param \Drupal\Core\Access\AccessManager $access_manager + * The access manager. */ - public function __construct(\Traversable $namespaces, ControllerResolverInterface $controller_resolver, Request $request, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager) { - parent::__construct('Plugin/Menu/LocalAction', $namespaces, array(), 'Drupal\Core\Annotation\Menu\LocalAction'); - + public function __construct(ControllerResolverInterface $controller_resolver, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager) { + // Skip calling the parent constructor, since that assumes annontation-based discovery + $this->discovery = new YamlDiscovery('local_actions', $module_handler->getModuleDirectories()); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $this->factory = new ContainerFactory($this); $this->controllerResolver = $controller_resolver; $this->request = $request; + $this->routeProvider = $route_provider; + $this->accessManager = $access_manager; $this->alterInfo($module_handler, 'menu_local_actions'); - $this->setCacheBackend($cache_backend, $language_manager, 'local_action_plugins'); + $this->setCacheBackend($cache_backend, $language_manager, 'local_action_plugins', array('local_action' => TRUE)); } /** @@ -92,44 +139,47 @@ public function getTitle(LocalActionInterface $local_action) { } /** - * Gets the Drupal path for a local action. - * - * @param \Drupal\Core\Menu\LocalActionInterface $local_action - * An object to get the path from. - * - * @return string - * The path. - * - * @throws \BadMethodCallException - * If the plugin does not implement the getPath() method. - */ - public function getPath(LocalActionInterface $local_action) { - $controller = array($local_action, 'getPath'); - $arguments = $this->controllerResolver->getArguments($this->request, $controller); - return call_user_func_array($controller, $arguments); - } - - /** * Finds all local actions that appear on a named route. * - * @param string $route_name - * The route for which to find local actions. + * @param string $route_appears + * The route name for which to find local actions. * - * @return \Drupal\Core\Menu\LocalActionInterface[] - * An array of LocalActionInterface objects that appear on the route path. + * @return array + * An array of link render arrays. */ - public function getActionsForRoute($route_name) { - if (!isset($this->instances[$route_name])) { - $this->instances[$route_name] = array(); + public function getActionsForRoute($route_appears) { + if (!isset($this->instances[$route_appears])) { + $route_names = array(); + $this->instances[$route_appears] = array(); // @todo - optimize this lookup by compiling or caching. foreach ($this->getDefinitions() as $plugin_id => $action_info) { - if (in_array($route_name, $action_info['appears_on'])) { + if (in_array($route_appears, $action_info['appears_on'])) { $plugin = $this->createInstance($plugin_id); - $this->instances[$route_name][$plugin_id] = $plugin; + $route_names[] = $plugin->getRouteName(); + $this->instances[$route_appears][$plugin_id] = $plugin; } } + // Pre-fetch all the action route objects. This reduces the number of SQL + // queries that would otherwise be triggered by the access manager. + $routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array(); + } + $links = array(); + foreach ($this->instances[$route_appears] as $plugin) { + $route_name = $plugin->getRouteName(); + $route_parameters = $plugin->getRouteParameters($this->request); + $links[$route_name] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'title' => $this->getTitle($plugin), + 'route_name' => $route_name, + 'route_parameters' => $route_parameters, + 'localized_options' => $plugin->getOptions($this->request), + 'is_plugin' => TRUE, + ), + '#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters), + ); } - return $this->instances[$route_name]; + return $links; } } diff --git a/core/lib/Drupal/Core/Menu/Plugin/Derivative/StaticLocalActionDeriver.php b/core/lib/Drupal/Core/Menu/Plugin/Derivative/StaticLocalActionDeriver.php deleted file mode 100644 index e9ef0ee..0000000 --- a/core/lib/Drupal/Core/Menu/Plugin/Derivative/StaticLocalActionDeriver.php +++ /dev/null @@ -1,124 +0,0 @@ -get('module_handler'), - $container->get('string_translation') - ); - } - - /** - * Constructs a StaticLocalActionDeriver object. - * - * @param string $base_plugin_id - * The base plugin ID. - * @param\Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - * @param \Drupal\Core\StringTranslation\TranslationInterface translation_manager - * The translation manager. - */ - public function __construct($base_plugin_id, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager) { - $this->basePluginId = $base_plugin_id; - $this->moduleHandler = $module_handler; - $this->translationManager = $translation_manager; - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinition($derivative_id, array $base_plugin_definition) { - if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) { - return $this->derivatives[$derivative_id]; - } - $this->getDerivativeDefinitions($base_plugin_definition); - return $this->derivatives[$derivative_id]; - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions(array $base_plugin_definition) { - $yaml_discovery = new YamlDiscovery('local_actions', $this->moduleHandler->getModuleDirectories()); - $required_keys = array('title' => 1, 'route_name' => 1, 'appears_on' => 1); - - foreach ($yaml_discovery->findAll() as $module => $local_actions) { - if (!empty($local_actions)) { - foreach ($local_actions as $name => $info) { - if ($missing_keys = array_diff_key($required_keys, array_intersect_key($info, $required_keys))) { - throw new PluginException(String::format('Static local action @name is missing @keys', array('@name' => $name, '@keys' => implode(', ', array_keys($missing_keys))))); - } - - $info += array('provider' => $module); - // Make sure 'appears_on' is an array. - $info['appears_on'] = (array) $info['appears_on']; - $info['title'] = $this->t($info['title']); - $this->derivatives[$name] = $info + $base_plugin_definition; - } - } - } - - return $this->derivatives; - } - - /** - * Translates a string to the current language or to a given language. - * - * See the t() documentation for details. - */ - protected function t($string, array $args = array(), array $options = array()) { - return $this->translationManager->translate($string, $args, $options); - } - -} diff --git a/core/lib/Drupal/Core/Menu/Plugin/Menu/LocalAction/StaticLocalAction.php b/core/lib/Drupal/Core/Menu/Plugin/Menu/LocalAction/StaticLocalAction.php deleted file mode 100644 index b0e0e9d..0000000 --- a/core/lib/Drupal/Core/Menu/Plugin/Menu/LocalAction/StaticLocalAction.php +++ /dev/null @@ -1,21 +0,0 @@ - $theme_info) { if ($theme_info->status) { - $actions['local_action_static:custom_block_add_action']['appears_on'][] = "block_admin_display.$theme"; + $actions['custom_block_add_action']['appears_on'][] = "block_admin_display.$theme"; } } } diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/Tests/CustomBlockTypeTest.php b/core/modules/block/custom_block/lib/Drupal/custom_block/Tests/CustomBlockTypeTest.php index 9b0dc0b..8aa3a14 100644 --- a/core/modules/block/custom_block/lib/Drupal/custom_block/Tests/CustomBlockTypeTest.php +++ b/core/modules/block/custom_block/lib/Drupal/custom_block/Tests/CustomBlockTypeTest.php @@ -156,7 +156,7 @@ public function testsCustomBlockAddTypes() { // Test that adding a block from the 'place blocks' form sends you to the // block configure form. - $this->drupalGet('admin/structure/block/list/' . $theme . '/add'); + $this->drupalGet('admin/structure/block/list/' . $theme); $this->clickLink(t('Add custom block')); $this->clickLink('foo'); $edit = array('info' => $this->randomName(8)); diff --git a/core/modules/config/tests/config_test/config_test.local_actions.yml b/core/modules/config/tests/config_test/config_test.local_actions.yml new file mode 100644 index 0000000..82eb1fc --- /dev/null +++ b/core/modules/config/tests/config_test/config_test.local_actions.yml @@ -0,0 +1,5 @@ +config_test_entity_add_local_action: + route_name: config_test_entity_add + title: 'Add test configuration' + appears_on: + - config_test_list_page diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module index a985921..d833917 100644 --- a/core/modules/config/tests/config_test/config_test.module +++ b/core/modules/config/tests/config_test/config_test.module @@ -30,11 +30,6 @@ function config_test_menu() { 'title' => 'Test configuration', 'route_name' => 'config_test_list_page', ); - $items['admin/structure/config_test/add'] = array( - 'title' => 'Add test configuration', - 'route_name' => 'config_test_entity_add', - 'type' => MENU_SIBLING_LOCAL_TASK, - ); $items['admin/structure/config_test/manage/%config_test'] = array( 'route_name' => 'config_test_entity_edit', ); diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/LocalAction/AddConfigTestEntityLocalAction.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/LocalAction/AddConfigTestEntityLocalAction.php deleted file mode 100644 index 9a55b79..0000000 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/Plugin/Menu/LocalAction/AddConfigTestEntityLocalAction.php +++ /dev/null @@ -1,24 +0,0 @@ - 'List', 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['admin/config/content/formats/add'] = array( - 'route_name' => 'filter_format_add', - 'type' => MENU_SIBLING_LOCAL_TASK, - ); $items['admin/config/content/formats/manage/%'] = array( 'title callback' => 'entity_page_label', 'title arguments' => array(5), diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/Menu/LocalAction/AddFilterFormatLocalAction.php b/core/modules/filter/lib/Drupal/filter/Plugin/Menu/LocalAction/AddFilterFormatLocalAction.php deleted file mode 100644 index f42bb88..0000000 --- a/core/modules/filter/lib/Drupal/filter/Plugin/Menu/LocalAction/AddFilterFormatLocalAction.php +++ /dev/null @@ -1,24 +0,0 @@ - 'List the current image styles on the site.', 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['admin/config/media/image-styles/add'] = array( - 'route_name' => 'image_style_add', - 'type' => MENU_SIBLING_LOCAL_TASK, - 'weight' => 1, - ); $items['admin/config/media/image-styles/manage/%image_style'] = array( 'title' => 'Edit style', 'description' => 'Configure an image style.', diff --git a/core/modules/image/lib/Drupal/image/Plugin/Menu/LocalAction/ImageStyleAddLocalAction.php b/core/modules/image/lib/Drupal/image/Plugin/Menu/LocalAction/ImageStyleAddLocalAction.php deleted file mode 100644 index 0af82bf..0000000 --- a/core/modules/image/lib/Drupal/image/Plugin/Menu/LocalAction/ImageStyleAddLocalAction.php +++ /dev/null @@ -1,24 +0,0 @@ - 'entity_page_label', 'title arguments' => array(4), ); - $items['admin/structure/menu/manage/%menu/add'] = array( - 'title' => 'Add link', - 'route_name' => 'menu_link_add', - 'type' => MENU_LOCAL_ACTION, - ); $items['admin/structure/menu/manage/%menu/edit'] = array( 'title' => 'Edit menu', 'type' => MENU_DEFAULT_LOCAL_TASK, diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Menu/LocalAction/AddShortcutSetLocalAction.php b/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Menu/LocalAction/AddShortcutSetLocalAction.php deleted file mode 100644 index 65abdf0..0000000 --- a/core/modules/shortcut/lib/Drupal/shortcut/Plugin/Menu/LocalAction/AddShortcutSetLocalAction.php +++ /dev/null @@ -1,24 +0,0 @@ - 'Add and modify shortcut sets.', 'route_name' => 'shortcut_set_admin', ); - $items['admin/config/user-interface/shortcut/add-set'] = array( - 'title' => 'Add shortcut set', - 'route_name' => 'shortcut_set_add', - 'type' => MENU_SIBLING_LOCAL_TASK, - ); $items['admin/config/user-interface/shortcut/manage/%shortcut_set'] = array( 'title' => 'Edit shortcuts', 'route_name' => 'shortcut_set_customize', diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/LocalAction/MenuTestLocalAction.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/LocalAction/MenuTestLocalAction.php deleted file mode 100644 index d09a8fb..0000000 --- a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/LocalAction/MenuTestLocalAction.php +++ /dev/null @@ -1,24 +0,0 @@ - 'views_ui.list', ); - $items['admin/structure/views/add'] = array( - 'route_name' => 'views_ui.add', - 'type' => MENU_SIBLING_LOCAL_TASK, - ); - // @todo - remove all items of type MENU_VISIBLE_IN_BREADCRUMB // when there is a route-aware breadcrumb builder. $items['admin/structure/views/settings'] = array(