Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Menu / LocalTaskManager.php
1 <?php
2
3 namespace Drupal\Core\Menu;
4
5 use Drupal\Component\Plugin\Exception\PluginException;
6 use Drupal\Core\Access\AccessManagerInterface;
7 use Drupal\Core\Cache\Cache;
8 use Drupal\Core\Cache\CacheableMetadata;
9 use Drupal\Core\Cache\CacheBackendInterface;
10 use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
11 use Drupal\Core\Controller\ControllerResolverInterface;
12 use Drupal\Core\Extension\ModuleHandlerInterface;
13 use Drupal\Core\Language\LanguageManagerInterface;
14 use Drupal\Core\Plugin\DefaultPluginManager;
15 use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
16 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
17 use Drupal\Core\Plugin\Factory\ContainerFactory;
18 use Drupal\Core\Routing\RouteMatchInterface;
19 use Drupal\Core\Routing\RouteProviderInterface;
20 use Drupal\Core\Session\AccountInterface;
21 use Drupal\Core\Url;
22 use Symfony\Component\HttpFoundation\RequestStack;
23 use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
24
25 /**
26  * Provides the default local task manager using YML as primary definition.
27  */
28 class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerInterface {
29
30   /**
31    * {@inheritdoc}
32    */
33   protected $defaults = [
34     // (required) The name of the route this task links to.
35     'route_name' => '',
36     // Parameters for route variables when generating a link.
37     'route_parameters' => [],
38     // The static title for the local task.
39     'title' => '',
40     // The route name where the root tab appears.
41     'base_route' => '',
42     // The plugin ID of the parent tab (or NULL for the top-level tab).
43     'parent_id' => NULL,
44     // The weight of the tab.
45     'weight' => NULL,
46     // The default link options.
47     'options' => [],
48     // Default class for local task implementations.
49     'class' => 'Drupal\Core\Menu\LocalTaskDefault',
50     // The plugin id. Set by the plugin system based on the top-level YAML key.
51     'id' => '',
52   ];
53
54   /**
55    * An argument resolver object.
56    *
57    * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
58    */
59   protected $argumentResolver;
60
61   /**
62    * A controller resolver object.
63    *
64    * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface
65    *
66    * @deprecated
67    *   Using the 'controller_resolver' service as the first argument is
68    *   deprecated, use the 'http_kernel.controller.argument_resolver' instead.
69    *   If your subclass requires the 'controller_resolver' service add it as an
70    *   additional argument.
71    *
72    * @see https://www.drupal.org/node/2959408
73    */
74   protected $controllerResolver;
75
76   /**
77    * The request stack.
78    *
79    * @var \Symfony\Component\HttpFoundation\RequestStack
80    */
81   protected $requestStack;
82
83   /**
84    * The current route match.
85    *
86    * @var \Drupal\Core\Routing\RouteMatchInterface
87    */
88   protected $routeMatch;
89
90   /**
91    * The plugin instances.
92    *
93    * @var array
94    */
95   protected $instances = [];
96
97   /**
98    * The local task render arrays for the current route.
99    *
100    * @var array
101    */
102   protected $taskData;
103
104   /**
105    * The route provider to load routes by name.
106    *
107    * @var \Drupal\Core\Routing\RouteProviderInterface
108    */
109   protected $routeProvider;
110
111   /**
112    * The access manager.
113    *
114    * @var \Drupal\Core\Access\AccessManagerInterface
115    */
116   protected $accessManager;
117
118   /**
119    * The current user.
120    *
121    * @var \Drupal\Core\Session\AccountInterface
122    */
123   protected $account;
124
125   /**
126    * Constructs a \Drupal\Core\Menu\LocalTaskManager object.
127    *
128    * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
129    *   An object to use in resolving route arguments.
130    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
131    *   The request object to use for building titles and paths for plugin instances.
132    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
133    *   The current route match.
134    * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
135    *   The route provider to load routes by name.
136    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
137    *   The module handler.
138    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
139    *   The cache backend.
140    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
141    *   The language manager.
142    * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
143    *   The access manager.
144    * @param \Drupal\Core\Session\AccountInterface $account
145    *   The current user.
146    */
147   public function __construct(ArgumentResolverInterface $argument_resolver, RequestStack $request_stack, RouteMatchInterface $route_match, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account) {
148     $this->factory = new ContainerFactory($this, '\Drupal\Core\Menu\LocalTaskInterface');
149     $this->argumentResolver = $argument_resolver;
150     if ($argument_resolver instanceof ControllerResolverInterface) {
151       @trigger_error("Using the 'controller_resolver' service as the first argument is deprecated, use the 'http_kernel.controller.argument_resolver' instead. If your subclass requires the 'controller_resolver' service add it as an additional argument. See https://www.drupal.org/node/2959408.", E_USER_DEPRECATED);
152       $this->controllerResolver = $argument_resolver;
153     }
154     $this->requestStack = $request_stack;
155     $this->routeMatch = $route_match;
156     $this->routeProvider = $route_provider;
157     $this->accessManager = $access_manager;
158     $this->account = $account;
159     $this->moduleHandler = $module_handler;
160     $this->alterInfo('local_tasks');
161     $this->setCacheBackend($cache, 'local_task_plugins:' . $language_manager->getCurrentLanguage()->getId(), ['local_task']);
162   }
163
164   /**
165    * {@inheritdoc}
166    */
167   protected function getDiscovery() {
168     if (!isset($this->discovery)) {
169       $yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
170       $yaml_discovery->addTranslatableProperty('title', 'title_context');
171       $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
172     }
173     return $this->discovery;
174   }
175
176   /**
177    * {@inheritdoc}
178    */
179   public function processDefinition(&$definition, $plugin_id) {
180     parent::processDefinition($definition, $plugin_id);
181     // If there is no route name, this is a broken definition.
182     if (empty($definition['route_name'])) {
183       throw new PluginException(sprintf('Plugin (%s) definition must include "route_name"', $plugin_id));
184     }
185   }
186
187   /**
188    * {@inheritdoc}
189    */
190   public function getTitle(LocalTaskInterface $local_task) {
191     $controller = [$local_task, 'getTitle'];
192     $request = $this->requestStack->getCurrentRequest();
193     $arguments = $this->argumentResolver->getArguments($request, $controller);
194     return call_user_func_array($controller, $arguments);
195   }
196
197   /**
198    * {@inheritdoc}
199    */
200   public function getDefinitions() {
201     $definitions = parent::getDefinitions();
202
203     $count = 0;
204     foreach ($definitions as &$definition) {
205       if (isset($definition['weight'])) {
206         // Add some micro weight.
207         $definition['weight'] += ($count++) * 1e-6;
208       }
209     }
210
211     return $definitions;
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function getLocalTasksForRoute($route_name) {
218     if (!isset($this->instances[$route_name])) {
219       $this->instances[$route_name] = [];
220       if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
221         $base_routes = $cache->data['base_routes'];
222         $parents = $cache->data['parents'];
223         $children = $cache->data['children'];
224       }
225       else {
226         $definitions = $this->getDefinitions();
227         // We build the hierarchy by finding all tabs that should
228         // appear on the current route.
229         $base_routes = [];
230         $parents = [];
231         $children = [];
232         foreach ($definitions as $plugin_id => $task_info) {
233           // Fill in the base_route from the parent to insure consistency.
234           if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) {
235             $task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
236             // Populate the definitions we use in the next loop. Using a
237             // reference like &$task_info causes bugs.
238             $definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
239           }
240           if ($route_name == $task_info['route_name']) {
241             if (!empty($task_info['base_route'])) {
242               $base_routes[$task_info['base_route']] = $task_info['base_route'];
243             }
244             // Tabs that link to the current route are viable parents
245             // and their parent and children should be visible also.
246             // @todo - this only works for 2 levels of tabs.
247             // instead need to iterate up.
248             $parents[$plugin_id] = TRUE;
249             if (!empty($task_info['parent_id'])) {
250               $parents[$task_info['parent_id']] = TRUE;
251             }
252           }
253         }
254         if ($base_routes) {
255           // Find all the plugins with the same root and that are at the top
256           // level or that have a visible parent.
257           foreach ($definitions as $plugin_id => $task_info) {
258             if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) {
259               // Concat '> ' with root ID for the parent of top-level tabs.
260               $parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id'];
261               $children[$parent][$plugin_id] = $task_info;
262             }
263           }
264         }
265         $data = [
266           'base_routes' => $base_routes,
267           'parents' => $parents,
268           'children' => $children,
269         ];
270         $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags);
271       }
272       // Create a plugin instance for each element of the hierarchy.
273       foreach ($base_routes as $base_route) {
274         // Convert the tree keyed by plugin IDs into a simple one with
275         // integer depth.  Create instances for each plugin along the way.
276         $level = 0;
277         // We used this above as the top-level parent array key.
278         $next_parent = '> ' . $base_route;
279         do {
280           $parent = $next_parent;
281           $next_parent = FALSE;
282           foreach ($children[$parent] as $plugin_id => $task_info) {
283             $plugin = $this->createInstance($plugin_id);
284             $this->instances[$route_name][$level][$plugin_id] = $plugin;
285             // Normally, the link generator compares the href of every link with
286             // the current path and sets the active class accordingly. But the
287             // parents of the current local task may be on a different route in
288             // which case we have to set the class manually by flagging it
289             // active.
290             if (!empty($parents[$plugin_id]) && $route_name != $task_info['route_name']) {
291               $plugin->setActive();
292             }
293             if (isset($children[$plugin_id])) {
294               // This tab has visible children.
295               $next_parent = $plugin_id;
296             }
297           }
298           $level++;
299         } while ($next_parent);
300       }
301
302     }
303     return $this->instances[$route_name];
304   }
305
306   /**
307    * {@inheritdoc}
308    */
309   public function getTasksBuild($current_route_name, RefinableCacheableDependencyInterface &$cacheability) {
310     $tree = $this->getLocalTasksForRoute($current_route_name);
311     $build = [];
312
313     // Collect all route names.
314     $route_names = [];
315     foreach ($tree as $instances) {
316       foreach ($instances as $child) {
317         $route_names[] = $child->getRouteName();
318       }
319     }
320     // Pre-fetch all routes involved in the tree. This reduces the number
321     // of SQL queries that would otherwise be triggered by the access manager.
322     if ($route_names) {
323       $this->routeProvider->getRoutesByNames($route_names);
324     }
325
326     foreach ($tree as $level => $instances) {
327       /** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */
328       foreach ($instances as $plugin_id => $child) {
329         $route_name = $child->getRouteName();
330         $route_parameters = $child->getRouteParameters($this->routeMatch);
331
332         // Given that the active flag depends on the route we have to add the
333         // route cache context.
334         $cacheability->addCacheContexts(['route']);
335         $active = $this->isRouteActive($current_route_name, $route_name, $route_parameters);
336
337         // The plugin may have been set active in getLocalTasksForRoute() if
338         // one of its child tabs is the active tab.
339         $active = $active || $child->getActive();
340         // @todo It might make sense to use link render elements instead.
341
342         $link = [
343           'title' => $this->getTitle($child),
344           'url' => Url::fromRoute($route_name, $route_parameters),
345           'localized_options' => $child->getOptions($this->routeMatch),
346         ];
347         $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE);
348         $build[$level][$plugin_id] = [
349           '#theme' => 'menu_local_task',
350           '#link' => $link,
351           '#active' => $active,
352           '#weight' => $child->getWeight(),
353           '#access' => $access,
354         ];
355         $cacheability->addCacheableDependency($access)->addCacheableDependency($child);
356       }
357     }
358
359     return $build;
360   }
361
362   /**
363    * {@inheritdoc}
364    */
365   public function getLocalTasks($route_name, $level = 0) {
366     if (!isset($this->taskData[$route_name])) {
367       $cacheability = new CacheableMetadata();
368       $cacheability->addCacheContexts(['route']);
369       // Look for route-based tabs.
370       $this->taskData[$route_name] = [
371         'tabs' => [],
372         'cacheability' => $cacheability,
373       ];
374
375       if (!$this->requestStack->getCurrentRequest()->attributes->has('exception')) {
376         // Safe to build tasks only when no exceptions raised.
377         $data = [];
378         $local_tasks = $this->getTasksBuild($route_name, $cacheability);
379         foreach ($local_tasks as $tab_level => $items) {
380           $data[$tab_level] = empty($data[$tab_level]) ? $items : array_merge($data[$tab_level], $items);
381         }
382         $this->taskData[$route_name]['tabs'] = $data;
383         // Allow modules to alter local tasks.
384         $this->moduleHandler->alter('menu_local_tasks', $this->taskData[$route_name], $route_name, $cacheability);
385         $this->taskData[$route_name]['cacheability'] = $cacheability;
386       }
387     }
388
389     if (isset($this->taskData[$route_name]['tabs'][$level])) {
390       return [
391         'tabs' => $this->taskData[$route_name]['tabs'][$level],
392         'route_name' => $route_name,
393         'cacheability' => $this->taskData[$route_name]['cacheability'],
394       ];
395     }
396
397     return [
398       'tabs' => [],
399       'route_name' => $route_name,
400       'cacheability' => $this->taskData[$route_name]['cacheability'],
401     ];
402   }
403
404   /**
405    * Determines whether the route of a certain local task is currently active.
406    *
407    * @param string $current_route_name
408    *   The route name of the current main request.
409    * @param string $route_name
410    *   The route name of the local task to determine the active status.
411    * @param array $route_parameters
412    *
413    * @return bool
414    *   Returns TRUE if the passed route_name and route_parameters is considered
415    *   as the same as the one from the request, otherwise FALSE.
416    */
417   protected function isRouteActive($current_route_name, $route_name, $route_parameters) {
418     // Flag the list element as active if this tab's route and parameters match
419     // the current request's route and route variables.
420     $active = $current_route_name == $route_name;
421     if ($active) {
422       // The request is injected, so we need to verify that we have the expected
423       // _raw_variables attribute.
424       $raw_variables_bag = $this->routeMatch->getRawParameters();
425       // If we don't have _raw_variables, we assume the attributes are still the
426       // original values.
427       $raw_variables = $raw_variables_bag ? $raw_variables_bag->all() : $this->routeMatch->getParameters()->all();
428       $active = array_intersect_assoc($route_parameters, $raw_variables) == $route_parameters;
429     }
430     return $active;
431   }
432
433 }