9291fda0bf1621c4dfff2814e494b38cfdaa9326
[yaffs-website] / web / core / modules / menu_ui / src / MenuForm.php
1 <?php
2
3 namespace Drupal\menu_ui;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Cache\CacheableMetadata;
7 use Drupal\Core\Entity\EntityForm;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Link;
11 use Drupal\Core\Menu\MenuLinkManagerInterface;
12 use Drupal\Core\Menu\MenuLinkTreeElement;
13 use Drupal\Core\Menu\MenuLinkTreeInterface;
14 use Drupal\Core\Menu\MenuTreeParameters;
15 use Drupal\Core\Render\Element;
16 use Drupal\Core\Url;
17 use Drupal\Core\Utility\LinkGeneratorInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
19
20 /**
21  * Base form for menu edit forms.
22  *
23  * @internal
24  */
25 class MenuForm extends EntityForm {
26
27   /**
28    * The menu link manager.
29    *
30    * @var \Drupal\Core\Menu\MenuLinkManagerInterface
31    */
32   protected $menuLinkManager;
33
34   /**
35    * The menu tree service.
36    *
37    * @var \Drupal\Core\Menu\MenuLinkTreeInterface
38    */
39   protected $menuTree;
40
41   /**
42    * The link generator.
43    *
44    * @var \Drupal\Core\Utility\LinkGeneratorInterface
45    */
46   protected $linkGenerator;
47
48   /**
49    * The overview tree form.
50    *
51    * @var array
52    */
53   protected $overviewTreeForm = ['#tree' => TRUE];
54
55   /**
56    * Constructs a MenuForm object.
57    *
58    * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
59    *   The menu link manager.
60    * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
61    *   The menu tree service.
62    * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
63    *   The link generator.
64    */
65   public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator) {
66     $this->menuLinkManager = $menu_link_manager;
67     $this->menuTree = $menu_tree;
68     $this->linkGenerator = $link_generator;
69   }
70
71   /**
72    * {@inheritdoc}
73    */
74   public static function create(ContainerInterface $container) {
75     return new static(
76       $container->get('plugin.manager.menu.link'),
77       $container->get('menu.link_tree'),
78       $container->get('link_generator')
79     );
80   }
81
82   /**
83    * {@inheritdoc}
84    */
85   public function form(array $form, FormStateInterface $form_state) {
86     $menu = $this->entity;
87
88     if ($this->operation == 'edit') {
89       $form['#title'] = $this->t('Edit menu %label', ['%label' => $menu->label()]);
90     }
91
92     $form['label'] = [
93       '#type' => 'textfield',
94       '#title' => $this->t('Title'),
95       '#default_value' => $menu->label(),
96       '#required' => TRUE,
97     ];
98     $form['id'] = [
99       '#type' => 'machine_name',
100       '#title' => $this->t('Menu name'),
101       '#default_value' => $menu->id(),
102       '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
103       '#description' => $this->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
104       '#machine_name' => [
105         'exists' => [$this, 'menuNameExists'],
106         'source' => ['label'],
107         'replace_pattern' => '[^a-z0-9-]+',
108         'replace' => '-',
109       ],
110       // A menu's machine name cannot be changed.
111       '#disabled' => !$menu->isNew() || $menu->isLocked(),
112     ];
113     $form['description'] = [
114       '#type' => 'textfield',
115       '#title' => t('Administrative summary'),
116       '#maxlength' => 512,
117       '#default_value' => $menu->getDescription(),
118     ];
119
120     $form['langcode'] = [
121       '#type' => 'language_select',
122       '#title' => t('Menu language'),
123       '#languages' => LanguageInterface::STATE_ALL,
124       '#default_value' => $menu->language()->getId(),
125     ];
126
127     // Add menu links administration form for existing menus.
128     if (!$menu->isNew() || $menu->isLocked()) {
129       // Form API supports constructing and validating self-contained sections
130       // within forms, but does not allow handling the form section's submission
131       // equally separated yet. Therefore, we use a $form_state key to point to
132       // the parents of the form section.
133       // @see self::submitOverviewForm()
134       $form_state->set('menu_overview_form_parents', ['links']);
135       $form['links'] = [];
136       $form['links'] = $this->buildOverviewForm($form['links'], $form_state);
137     }
138
139     return parent::form($form, $form_state);
140   }
141
142   /**
143    * Returns whether a menu name already exists.
144    *
145    * @param string $value
146    *   The name of the menu.
147    *
148    * @return bool
149    *   Returns TRUE if the menu already exists, FALSE otherwise.
150    */
151   public function menuNameExists($value) {
152     // Check first to see if a menu with this ID exists.
153     if ($this->entityTypeManager->getStorage('menu')->getQuery()->condition('id', $value)->range(0, 1)->count()->execute()) {
154       return TRUE;
155     }
156
157     // Check for a link assigned to this menu.
158     return $this->menuLinkManager->menuNameInUse($value);
159   }
160
161   /**
162    * {@inheritdoc}
163    */
164   public function save(array $form, FormStateInterface $form_state) {
165     $menu = $this->entity;
166     $status = $menu->save();
167     $edit_link = $this->entity->link($this->t('Edit'));
168     if ($status == SAVED_UPDATED) {
169       $this->messenger()->addStatus($this->t('Menu %label has been updated.', ['%label' => $menu->label()]));
170       $this->logger('menu')->notice('Menu %label has been updated.', ['%label' => $menu->label(), 'link' => $edit_link]);
171     }
172     else {
173       $this->messenger()->addStatus($this->t('Menu %label has been added.', ['%label' => $menu->label()]));
174       $this->logger('menu')->notice('Menu %label has been added.', ['%label' => $menu->label(), 'link' => $edit_link]);
175     }
176
177     $form_state->setRedirectUrl($this->entity->urlInfo('edit-form'));
178   }
179
180   /**
181    * {@inheritdoc}
182    */
183   public function submitForm(array &$form, FormStateInterface $form_state) {
184     parent::submitForm($form, $form_state);
185
186     if (!$this->entity->isNew() || $this->entity->isLocked()) {
187       $this->submitOverviewForm($form, $form_state);
188     }
189   }
190
191   /**
192    * Form constructor to edit an entire menu tree at once.
193    *
194    * Shows for one menu the menu links accessible to the current user and
195    * relevant operations.
196    *
197    * This form constructor can be integrated as a section into another form. It
198    * relies on the following keys in $form_state:
199    * - menu: A menu entity.
200    * - menu_overview_form_parents: An array containing the parent keys to this
201    *   form.
202    * Forms integrating this section should call menu_overview_form_submit() from
203    * their form submit handler.
204    */
205   protected function buildOverviewForm(array &$form, FormStateInterface $form_state) {
206     // Ensure that menu_overview_form_submit() knows the parents of this form
207     // section.
208     if (!$form_state->has('menu_overview_form_parents')) {
209       $form_state->set('menu_overview_form_parents', []);
210     }
211
212     $form['#attached']['library'][] = 'menu_ui/drupal.menu_ui.adminforms';
213
214     $tree = $this->menuTree->load($this->entity->id(), new MenuTreeParameters());
215
216     // We indicate that a menu administrator is running the menu access check.
217     $this->getRequest()->attributes->set('_menu_admin', TRUE);
218     $manipulators = [
219       ['callable' => 'menu.default_tree_manipulators:checkAccess'],
220       ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
221     ];
222     $tree = $this->menuTree->transform($tree, $manipulators);
223     $this->getRequest()->attributes->set('_menu_admin', FALSE);
224
225     // Determine the delta; the number of weights to be made available.
226     $count = function (array $tree) {
227       $sum = function ($carry, MenuLinkTreeElement $item) {
228         return $carry + $item->count();
229       };
230       return array_reduce($tree, $sum);
231     };
232     $delta = max($count($tree), 50);
233
234     $form['links'] = [
235       '#type' => 'table',
236       '#theme' => 'table__menu_overview',
237       '#header' => [
238         $this->t('Menu link'),
239         [
240           'data' => $this->t('Enabled'),
241           'class' => ['checkbox'],
242         ],
243         $this->t('Weight'),
244         [
245           'data' => $this->t('Operations'),
246           'colspan' => 3,
247         ],
248       ],
249       '#attributes' => [
250         'id' => 'menu-overview',
251       ],
252       '#tabledrag' => [
253         [
254           'action' => 'match',
255           'relationship' => 'parent',
256           'group' => 'menu-parent',
257           'subgroup' => 'menu-parent',
258           'source' => 'menu-id',
259           'hidden' => TRUE,
260           'limit' => \Drupal::menuTree()->maxDepth() - 1,
261         ],
262         [
263           'action' => 'order',
264           'relationship' => 'sibling',
265           'group' => 'menu-weight',
266         ],
267       ],
268     ];
269
270     $form['links']['#empty'] = $this->t('There are no menu links yet. <a href=":url">Add link</a>.', [
271       ':url' => $this->url('entity.menu.add_link_form', ['menu' => $this->entity->id()], [
272         'query' => ['destination' => $this->entity->url('edit-form')],
273       ]),
274     ]);
275     $links = $this->buildOverviewTreeForm($tree, $delta);
276     foreach (Element::children($links) as $id) {
277       if (isset($links[$id]['#item'])) {
278         $element = $links[$id];
279
280         $form['links'][$id]['#item'] = $element['#item'];
281
282         // TableDrag: Mark the table row as draggable.
283         $form['links'][$id]['#attributes'] = $element['#attributes'];
284         $form['links'][$id]['#attributes']['class'][] = 'draggable';
285
286         // TableDrag: Sort the table row according to its existing/configured weight.
287         $form['links'][$id]['#weight'] = $element['#item']->link->getWeight();
288
289         // Add special classes to be used for tabledrag.js.
290         $element['parent']['#attributes']['class'] = ['menu-parent'];
291         $element['weight']['#attributes']['class'] = ['menu-weight'];
292         $element['id']['#attributes']['class'] = ['menu-id'];
293
294         $form['links'][$id]['title'] = [
295           [
296             '#theme' => 'indentation',
297             '#size' => $element['#item']->depth - 1,
298           ],
299           $element['title'],
300         ];
301         $form['links'][$id]['enabled'] = $element['enabled'];
302         $form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled'];
303
304         $form['links'][$id]['weight'] = $element['weight'];
305
306         // Operations (dropbutton) column.
307         $form['links'][$id]['operations'] = $element['operations'];
308
309         $form['links'][$id]['id'] = $element['id'];
310         $form['links'][$id]['parent'] = $element['parent'];
311       }
312     }
313
314     return $form;
315   }
316
317   /**
318    * Recursive helper function for buildOverviewForm().
319    *
320    * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
321    *   The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load().
322    * @param int $delta
323    *   The default number of menu items used in the menu weight selector is 50.
324    *
325    * @return array
326    *   The overview tree form.
327    */
328   protected function buildOverviewTreeForm($tree, $delta) {
329     $form = &$this->overviewTreeForm;
330     $tree_access_cacheability = new CacheableMetadata();
331     foreach ($tree as $element) {
332       $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
333
334       // Only render accessible links.
335       if (!$element->access->isAllowed()) {
336         continue;
337       }
338
339       /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
340       $link = $element->link;
341       if ($link) {
342         $id = 'menu_plugin_id:' . $link->getPluginId();
343         $form[$id]['#item'] = $element;
344         $form[$id]['#attributes'] = $link->isEnabled() ? ['class' => ['menu-enabled']] : ['class' => ['menu-disabled']];
345         $form[$id]['title'] = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toRenderable();
346         if (!$link->isEnabled()) {
347           $form[$id]['title']['#suffix'] = ' (' . $this->t('disabled') . ')';
348         }
349         // @todo Remove this in https://www.drupal.org/node/2568785.
350         elseif ($id === 'menu_plugin_id:user.logout') {
351           $form[$id]['title']['#suffix'] = ' (' . $this->t('<q>Log in</q> for anonymous users') . ')';
352         }
353         // @todo Remove this in https://www.drupal.org/node/2568785.
354         elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') {
355           $form[$id]['title']['#suffix'] = ' (' . $this->t('logged in users only') . ')';
356         }
357
358         $form[$id]['enabled'] = [
359           '#type' => 'checkbox',
360           '#title' => $this->t('Enable @title menu link', ['@title' => $link->getTitle()]),
361           '#title_display' => 'invisible',
362           '#default_value' => $link->isEnabled(),
363         ];
364         $form[$id]['weight'] = [
365           '#type' => 'weight',
366           '#delta' => $delta,
367           '#default_value' => $link->getWeight(),
368           '#title' => $this->t('Weight for @title', ['@title' => $link->getTitle()]),
369           '#title_display' => 'invisible',
370         ];
371         $form[$id]['id'] = [
372           '#type' => 'hidden',
373           '#value' => $link->getPluginId(),
374         ];
375         $form[$id]['parent'] = [
376           '#type' => 'hidden',
377           '#default_value' => $link->getParent(),
378         ];
379         // Build a list of operations.
380         $operations = [];
381         $operations['edit'] = [
382           'title' => $this->t('Edit'),
383         ];
384         // Allow for a custom edit link per plugin.
385         $edit_route = $link->getEditRoute();
386         if ($edit_route) {
387           $operations['edit']['url'] = $edit_route;
388           // Bring the user back to the menu overview.
389           $operations['edit']['query'] = $this->getDestinationArray();
390         }
391         else {
392           // Fall back to the standard edit link.
393           $operations['edit'] += [
394             'url' => Url::fromRoute('menu_ui.link_edit', ['menu_link_plugin' => $link->getPluginId()]),
395           ];
396         }
397         // Links can either be reset or deleted, not both.
398         if ($link->isResettable()) {
399           $operations['reset'] = [
400             'title' => $this->t('Reset'),
401             'url' => Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $link->getPluginId()]),
402           ];
403         }
404         elseif ($delete_link = $link->getDeleteRoute()) {
405           $operations['delete']['url'] = $delete_link;
406           $operations['delete']['query'] = $this->getDestinationArray();
407           $operations['delete']['title'] = $this->t('Delete');
408         }
409         if ($link->isTranslatable()) {
410           $operations['translate'] = [
411             'title' => $this->t('Translate'),
412             'url' => $link->getTranslateRoute(),
413           ];
414         }
415         $form[$id]['operations'] = [
416           '#type' => 'operations',
417           '#links' => $operations,
418         ];
419       }
420
421       if ($element->subtree) {
422         $this->buildOverviewTreeForm($element->subtree, $delta);
423       }
424     }
425
426     $tree_access_cacheability
427       ->merge(CacheableMetadata::createFromRenderArray($form))
428       ->applyTo($form);
429
430     return $form;
431   }
432
433   /**
434    * Submit handler for the menu overview form.
435    *
436    * This function takes great care in saving parent items first, then items
437    * underneath them. Saving items in the incorrect order can break the tree.
438    */
439   protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) {
440     // Form API supports constructing and validating self-contained sections
441     // within forms, but does not allow to handle the form section's submission
442     // equally separated yet. Therefore, we use a $form_state key to point to
443     // the parents of the form section.
444     $parents = $form_state->get('menu_overview_form_parents');
445     $input = NestedArray::getValue($form_state->getUserInput(), $parents);
446     $form = &NestedArray::getValue($complete_form, $parents);
447
448     // When dealing with saving menu items, the order in which these items are
449     // saved is critical. If a changed child item is saved before its parent,
450     // the child item could be saved with an invalid path past its immediate
451     // parent. To prevent this, save items in the form in the same order they
452     // are sent, ensuring parents are saved first, then their children.
453     // See https://www.drupal.org/node/181126#comment-632270.
454     $order = is_array($input) ? array_flip(array_keys($input)) : [];
455     // Update our original form with the new order.
456     $form = array_intersect_key(array_merge($order, $form), $form);
457
458     $fields = ['weight', 'parent', 'enabled'];
459     $form_links = $form['links'];
460     foreach (Element::children($form_links) as $id) {
461       if (isset($form_links[$id]['#item'])) {
462         $element = $form_links[$id];
463         $updated_values = [];
464         // Update any fields that have changed in this menu item.
465         foreach ($fields as $field) {
466           if ($element[$field]['#value'] != $element[$field]['#default_value']) {
467             $updated_values[$field] = $element[$field]['#value'];
468           }
469         }
470         if ($updated_values) {
471           // Use the ID from the actual plugin instance since the hidden value
472           // in the form could be tampered with.
473           $this->menuLinkManager->updateDefinition($element['#item']->link->getPLuginId(), $updated_values);
474         }
475       }
476     }
477   }
478
479 }