Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Menu / MenuLinkManager.php
1 <?php
2
3 namespace Drupal\Core\Menu;
4
5 use Drupal\Component\Plugin\Exception\PluginException;
6 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
7 use Drupal\Component\Utility\NestedArray;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
10 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
11 use Drupal\Core\Plugin\Factory\ContainerFactory;
12
13 /**
14  * Manages discovery, instantiation, and tree building of menu link plugins.
15  *
16  * This manager finds plugins that are rendered as menu links.
17  */
18 class MenuLinkManager implements MenuLinkManagerInterface {
19
20   /**
21    * Provides some default values for the definition of all menu link plugins.
22    *
23    * @todo Decide how to keep these field definitions in sync.
24    *   https://www.drupal.org/node/2302085
25    *
26    * @var array
27    */
28   protected $defaults = [
29     // (required) The name of the menu for this link.
30     'menu_name' => 'tools',
31     // (required) The name of the route this links to, unless it's external.
32     'route_name' => '',
33     // Parameters for route variables when generating a link.
34     'route_parameters' => [],
35     // The external URL if this link has one (required if route_name is empty).
36     'url' => '',
37     // The static title for the menu link. If this came from a YAML definition
38     // or other safe source this may be a TranslatableMarkup object.
39     'title' => '',
40     // The description. If this came from a YAML definition or other safe source
41     // this may be be a TranslatableMarkup object.
42     'description' => '',
43     // The plugin ID of the parent link (or NULL for a top-level link).
44     'parent' => '',
45     // The weight of the link.
46     'weight' => 0,
47     // The default link options.
48     'options' => [],
49     'expanded' => 0,
50     'enabled' => 1,
51     // The name of the module providing this link.
52     'provider' => '',
53     'metadata' => [],
54     // Default class for local task implementations.
55     'class' => 'Drupal\Core\Menu\MenuLinkDefault',
56     'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
57     // The plugin ID. Set by the plugin system based on the top-level YAML key.
58     'id' => '',
59   ];
60
61   /**
62    * The object that discovers plugins managed by this manager.
63    *
64    * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
65    */
66   protected $discovery;
67
68   /**
69    * The object that instantiates plugins managed by this manager.
70    *
71    * @var \Drupal\Component\Plugin\Factory\FactoryInterface
72    */
73   protected $factory;
74
75   /**
76    * The menu link tree storage.
77    *
78    * @var \Drupal\Core\Menu\MenuTreeStorageInterface
79    */
80   protected $treeStorage;
81
82   /**
83    * Service providing overrides for static links.
84    *
85    * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
86    */
87   protected $overrides;
88
89   /**
90    * The module handler.
91    *
92    * @var \Drupal\Core\Extension\ModuleHandlerInterface
93    */
94   protected $moduleHandler;
95
96
97   /**
98    * Constructs a \Drupal\Core\Menu\MenuLinkManager object.
99    *
100    * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
101    *   The menu link tree storage.
102    * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
103    *   The service providing overrides for static links.
104    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
105    *   The module handler.
106    */
107   public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
108     $this->treeStorage = $tree_storage;
109     $this->overrides = $overrides;
110     $this->moduleHandler = $module_handler;
111   }
112
113   /**
114    * Performs extra processing on plugin definitions.
115    *
116    * By default we add defaults for the type to the definition. If a type has
117    * additional processing logic, the logic can be added by replacing or
118    * extending this method.
119    *
120    * @param array $definition
121    *   The definition to be processed and modified by reference.
122    * @param $plugin_id
123    *   The ID of the plugin this definition is being used for.
124    */
125   protected function processDefinition(array &$definition, $plugin_id) {
126     $definition = NestedArray::mergeDeep($this->defaults, $definition);
127     // Typecast so NULL, no parent, will be an empty string since the parent ID
128     // should be a string.
129     $definition['parent'] = (string) $definition['parent'];
130     $definition['id'] = $plugin_id;
131   }
132
133   /**
134    * Gets the plugin discovery.
135    *
136    * @return \Drupal\Component\Plugin\Discovery\DiscoveryInterface
137    */
138   protected function getDiscovery() {
139     if (!isset($this->discovery)) {
140       $yaml_discovery = new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories());
141       $yaml_discovery->addTranslatableProperty('title', 'title_context');
142       $yaml_discovery->addTranslatableProperty('description', 'description_context');
143       $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
144     }
145     return $this->discovery;
146   }
147
148   /**
149    * Gets the plugin factory.
150    *
151    * @return \Drupal\Component\Plugin\Factory\FactoryInterface
152    */
153   protected function getFactory() {
154     if (!isset($this->factory)) {
155       $this->factory = new ContainerFactory($this);
156     }
157     return $this->factory;
158   }
159
160   /**
161    * {@inheritdoc}
162    */
163   public function getDefinitions() {
164     // Since this function is called rarely, instantiate the discovery here.
165     $definitions = $this->getDiscovery()->getDefinitions();
166
167     $this->moduleHandler->alter('menu_links_discovered', $definitions);
168
169     foreach ($definitions as $plugin_id => &$definition) {
170       $definition['id'] = $plugin_id;
171       $this->processDefinition($definition, $plugin_id);
172     }
173
174     // If this plugin was provided by a module that does not exist, remove the
175     // plugin definition.
176     // @todo Address what to do with an invalid plugin.
177     //   https://www.drupal.org/node/2302623
178     foreach ($definitions as $plugin_id => $plugin_definition) {
179       if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
180         unset($definitions[$plugin_id]);
181       }
182     }
183     return $definitions;
184   }
185
186   /**
187    * {@inheritdoc}
188    */
189   public function rebuild() {
190     $definitions = $this->getDefinitions();
191     // Apply overrides from config.
192     $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
193     foreach ($overrides as $id => $changes) {
194       if (!empty($definitions[$id])) {
195         $definitions[$id] = $changes + $definitions[$id];
196       }
197     }
198     $this->treeStorage->rebuild($definitions);
199   }
200
201   /**
202    * {@inheritdoc}
203    */
204   public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
205     $definition = $this->treeStorage->load($plugin_id);
206     if (empty($definition) && $exception_on_invalid) {
207       throw new PluginNotFoundException($plugin_id);
208     }
209     return $definition;
210   }
211
212   /**
213    * {@inheritdoc}
214    */
215   public function hasDefinition($plugin_id) {
216     return (bool) $this->getDefinition($plugin_id, FALSE);
217   }
218
219   /**
220    * Returns a pre-configured menu link plugin instance.
221    *
222    * @param string $plugin_id
223    *   The ID of the plugin being instantiated.
224    * @param array $configuration
225    *   An array of configuration relevant to the plugin instance.
226    *
227    * @return \Drupal\Core\Menu\MenuLinkInterface
228    *   A menu link instance.
229    *
230    * @throws \Drupal\Component\Plugin\Exception\PluginException
231    *   If the instance cannot be created, such as if the ID is invalid.
232    */
233   public function createInstance($plugin_id, array $configuration = []) {
234     return $this->getFactory()->createInstance($plugin_id, $configuration);
235   }
236
237   /**
238    * {@inheritdoc}
239    */
240   public function getInstance(array $options) {
241     if (isset($options['id'])) {
242       return $this->createInstance($options['id']);
243     }
244   }
245
246   /**
247    * {@inheritdoc}
248    */
249   public function deleteLinksInMenu($menu_name) {
250     foreach ($this->treeStorage->loadByProperties(['menu_name' => $menu_name]) as $plugin_id => $definition) {
251       $instance = $this->createInstance($plugin_id);
252       if ($instance->isDeletable()) {
253         $this->deleteInstance($instance, TRUE);
254       }
255       elseif ($instance->isResettable()) {
256         $new_instance = $this->resetInstance($instance);
257         $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
258       }
259     }
260   }
261
262   /**
263    * Deletes a specific instance.
264    *
265    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
266    *   The plugin instance to be deleted.
267    * @param bool $persist
268    *   If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
269    *
270    * @throws \Drupal\Component\Plugin\Exception\PluginException
271    *   If the plugin instance does not support deletion.
272    */
273   protected function deleteInstance(MenuLinkInterface $instance, $persist) {
274     $id = $instance->getPluginId();
275     if ($instance->isDeletable()) {
276       if ($persist) {
277         $instance->deleteLink();
278       }
279     }
280     else {
281       throw new PluginException("Menu link plugin with ID '$id' does not support deletion");
282     }
283     $this->treeStorage->delete($id);
284   }
285
286   /**
287    * {@inheritdoc}
288    */
289   public function removeDefinition($id, $persist = TRUE) {
290     $definition = $this->treeStorage->load($id);
291     // It's possible the definition has already been deleted, or doesn't exist.
292     if ($definition) {
293       $instance = $this->createInstance($id);
294       $this->deleteInstance($instance, $persist);
295     }
296   }
297
298   /**
299    * {@inheritdoc}
300    */
301   public function menuNameInUse($menu_name) {
302     $this->treeStorage->menuNameInUse($menu_name);
303   }
304
305   /**
306    * {@inheritdoc}
307    */
308   public function countMenuLinks($menu_name = NULL) {
309     return $this->treeStorage->countMenuLinks($menu_name);
310   }
311
312   /**
313    * {@inheritdoc}
314    */
315   public function getParentIds($id) {
316     if ($this->getDefinition($id, FALSE)) {
317       return $this->treeStorage->getRootPathIds($id);
318     }
319     return NULL;
320   }
321
322   /**
323    * {@inheritdoc}
324    */
325   public function getChildIds($id) {
326     if ($this->getDefinition($id, FALSE)) {
327       return $this->treeStorage->getAllChildIds($id);
328     }
329     return NULL;
330   }
331
332   /**
333    * {@inheritdoc}
334    */
335   public function loadLinksByRoute($route_name, array $route_parameters = [], $menu_name = NULL) {
336     $instances = [];
337     $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
338     foreach ($loaded as $plugin_id => $definition) {
339       $instances[$plugin_id] = $this->createInstance($plugin_id);
340     }
341     return $instances;
342   }
343
344   /**
345    * {@inheritdoc}
346    */
347   public function addDefinition($id, array $definition) {
348     if ($this->treeStorage->load($id)) {
349       throw new PluginException("The menu link ID $id already exists as a plugin definition");
350     }
351     elseif ($id === '') {
352       throw new PluginException("The menu link ID cannot be empty");
353     }
354     // Add defaults, so there is no requirement to specify everything.
355     $this->processDefinition($definition, $id);
356     // Store the new link in the tree.
357     $this->treeStorage->save($definition);
358     return $this->createInstance($id);
359   }
360
361   /**
362    * {@inheritdoc}
363    */
364   public function updateDefinition($id, array $new_definition_values, $persist = TRUE) {
365     $instance = $this->createInstance($id);
366     if ($instance) {
367       $new_definition_values['id'] = $id;
368       $changed_definition = $instance->updateLink($new_definition_values, $persist);
369       $this->treeStorage->save($changed_definition);
370     }
371     return $instance;
372   }
373
374   /**
375    * {@inheritdoc}
376    */
377   public function resetLink($id) {
378     $instance = $this->createInstance($id);
379     $new_instance = $this->resetInstance($instance);
380     return $new_instance;
381   }
382
383   /**
384    * Resets the menu link to its default settings.
385    *
386    * @param \Drupal\Core\Menu\MenuLinkInterface $instance
387    *   The menu link which should be reset.
388    *
389    * @return \Drupal\Core\Menu\MenuLinkInterface
390    *   The reset menu link.
391    *
392    * @throws \Drupal\Component\Plugin\Exception\PluginException
393    *   Thrown when the menu link is not resettable.
394    */
395   protected function resetInstance(MenuLinkInterface $instance) {
396     $id = $instance->getPluginId();
397
398     if (!$instance->isResettable()) {
399       throw new PluginException("Menu link $id is not resettable");
400     }
401     // Get the original data from disk, reset the override and re-save the menu
402     // tree for this link.
403     $definition = $this->getDefinitions()[$id];
404     $this->overrides->deleteOverride($id);
405     $this->treeStorage->save($definition);
406     return $this->createInstance($id);
407   }
408
409   /**
410    * {@inheritdoc}
411    */
412   public function resetDefinitions() {
413     $this->treeStorage->resetDefinitions();
414   }
415
416 }