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