Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / token / token.module
1 <?php
2
3 /**
4  * @file
5  * Enhances the token API in core: adds a browseable UI, missing tokens, etc.
6  */
7
8 use Drupal\Component\Render\PlainTextOutput;
9 use Drupal\Core\Block\BlockPluginInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Menu\MenuLinkInterface;
12 use Drupal\Core\Render\BubbleableMetadata;
13 use Drupal\Core\Render\Element;
14 use Drupal\Core\Routing\RouteMatchInterface;
15 use Drupal\Core\Entity\EntityTypeInterface;
16 use Drupal\Core\Field\BaseFieldDefinition;
17 use Drupal\Core\TypedData\TranslatableInterface;
18 use Drupal\menu_link_content\Entity\MenuLinkContent;
19 use Drupal\menu_link_content\MenuLinkContentInterface;
20 use Drupal\node\Entity\Node;
21 use Drupal\node\NodeInterface;
22
23 /**
24  * Implements hook_help().
25  */
26 function token_help($route_name, RouteMatchInterface $route_match) {
27   if ($route_name == 'help.page.token') {
28     $token_tree = \Drupal::service('token.tree_builder')->buildAllRenderable([
29       'click_insert' => FALSE,
30       'show_restricted' => TRUE,
31       'show_nested' => FALSE,
32     ]);
33     $output = '<h3>' . t('About') . '</h3>';
34     $output .= '<p>' . t('The <a href=":project">Token</a> module provides a user interface for the site token system. It also adds some additional tokens that are used extensively during site development. Tokens are specially formatted chunks of text that serve as placeholders for a dynamically generated value. For more information, covering both the token system and the additional tools provided by the Token module, see the <a href=":online">online documentation</a>.', [':online' => 'https://www.drupal.org/documentation/modules/token', ':project' => 'https://www.drupal.org/project/token']) . '</p>';
35     $output .= '<h3>' . t('Uses') . '</h3>';
36     $output .= '<p>' . t('Your website uses a shared token system for exposing and using placeholder tokens and their appropriate replacement values. This allows for any module to provide placeholder tokens for strings without having to reinvent the wheel. It also ensures consistency in the syntax used for tokens, making the system as a whole easier for end users to use.') . '</p>';
37     $output .= '<dl>';
38     $output .= '<dt>' . t('The list of the currently available tokens on this site are shown below.') . '</dt>';
39     $output .= '<dd>' . \Drupal::service('renderer')->render($token_tree) . '</dd>';
40     $output .= '</dl>';
41     return $output;
42   }
43 }
44
45 /**
46  * Return an array of the core modules supported by token.module.
47  */
48 function _token_core_supported_modules() {
49   return ['book', 'field', 'menu_ui'];
50 }
51
52 /**
53  * Implements hook_theme().
54  */
55 function token_theme() {
56   $info['token_tree_link'] = [
57     'variables' => [
58       'token_types' => [],
59       'global_types' => TRUE,
60       'click_insert' => TRUE,
61       'show_restricted' => FALSE,
62       'show_nested' => FALSE,
63       'recursion_limit' => 3,
64       'text' => NULL,
65       'options' => [],
66     ],
67     'file' => 'token.pages.inc',
68   ];
69
70   return $info;
71 }
72
73 /**
74  * Implements hook_block_view_alter().
75  */
76 function token_block_view_alter(&$build, BlockPluginInterface $block) {
77   if (isset($build['#configuration'])) {
78     $label = $build['#configuration']['label'];
79     if ($label != '<none>') {
80       // The label is automatically escaped, avoid escaping it twice.
81       // @todo https://www.drupal.org/node/2580723 will add a method or option
82       //   to the token API to do this, use that when available.
83       $bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
84       $build['#configuration']['label'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($label, [], [], $bubbleable_metadata));
85       $bubbleable_metadata->applyTo($build);
86     }
87   }
88 }
89
90 /**
91  * Implements hook_form_FORM_ID_alter().
92  */
93 function token_form_block_form_alter(&$form, FormStateInterface $form_state) {
94   $token_tree = [
95     '#theme' => 'token_tree_link',
96     '#token_types' => [],
97   ];
98   $rendered_token_tree = \Drupal::service('renderer')->render($token_tree);
99   $form['settings']['label']['#description'] = t('This field supports tokens. @browse_tokens_link', [
100     '@browse_tokens_link' => $rendered_token_tree,
101   ]);
102   $form['settings']['label']['#element_validate'][] = 'token_element_validate';
103   $form['settings']['label'] += ['#token_types' => []];
104 }
105
106 /**
107  * Implements hook_field_info_alter().
108  */
109 function token_field_info_alter(&$info) {
110   $defaults = [
111     'taxonomy_term_reference' => 'taxonomy_term_reference_plain',
112     'number_integer' => 'number_unformatted',
113     'number_decimal' => 'number_unformatted',
114     'number_float' => 'number_unformatted',
115     'file' => 'file_url_plain',
116     'image' => 'file_url_plain',
117     'text' => 'text_default',
118     'text_long' => 'text_default',
119     'text_with_summary' => 'text_default',
120     'list_integer' => 'list_default',
121     'list_float' => 'list_default',
122     'list_string' => 'list_default',
123     'list_boolean' => 'list_default',
124   ];
125   foreach ($defaults as $field_type => $default_token_formatter) {
126     if (isset($info[$field_type])) {
127       $info[$field_type] += ['default_token_formatter' => $default_token_formatter];
128     }
129   }
130 }
131
132 /**
133  * Implements hook_date_format_insert().
134  */
135 function token_date_format_insert() {
136   token_clear_cache();
137 }
138
139 /**
140  * Implements hook_date_format_delete().
141  */
142 function token_date_format_delete() {
143   token_clear_cache();
144 }
145
146 /**
147  * Implements hook_field_storage_config_presave().
148  */
149 function token_field_config_presave($instance) {
150   token_clear_cache();
151 }
152
153 /**
154  * Implements hook_field_storage_config_delete().
155  */
156 function token_field_config_delete($instance) {
157   token_clear_cache();
158 }
159
160 /**
161  * Clear token caches and static variables.
162  */
163 function token_clear_cache() {
164   \Drupal::token()->resetInfo();
165   \Drupal::service('token.entity_mapper')->resetInfo();
166   drupal_static_reset('token_menu_link_load_all_parents');
167   drupal_static_reset('token_book_link_load');
168 }
169
170 /**
171  * Implements hook_entity_type_alter().
172  *
173  * Because some token types to do not match their entity type names, we have to
174  * map them to the proper type. This is purely for other modules' benefit.
175  *
176  * @see \Drupal\token\TokenEntityMapperInterface::getEntityTypeMappings()
177  * @see http://drupal.org/node/737726
178  */
179 function token_entity_type_alter(array &$entity_types) {
180   $devel_exists = \Drupal::moduleHandler()->moduleExists('devel');
181   /* @var $entity_types EntityTypeInterface[] */
182   foreach ($entity_types as $entity_type_id => $entity_type) {
183     if (!$entity_type->get('token_type')) {
184       // Fill in default token types for entities.
185       switch ($entity_type_id) {
186         case 'taxonomy_term':
187         case 'taxonomy_vocabulary':
188           // Stupid taxonomy token types...
189           $entity_type->set('token_type', str_replace('taxonomy_', '', $entity_type_id));
190           break;
191
192         default:
193           // By default the token type is the same as the entity type.
194           $entity_type->set('token_type', $entity_type_id);
195           break;
196       }
197     }
198
199     if ($devel_exists
200       && $entity_type->hasViewBuilderClass()
201       && ($canonical = $entity_type->getLinkTemplate('canonical'))
202       && !$entity_type->hasLinkTemplate('token-devel')) {
203       $entity_type->setLinkTemplate('token-devel', $canonical . '/devel/token');
204     }
205   }
206 }
207
208 /**
209  * Implements hook_entity_view_modes_info().
210  */
211
212 /**
213  * Implements hook_module_implements_alter().
214  *
215  * Adds missing token support for core modules.
216  */
217 function token_module_implements_alter(&$implementations, $hook) {
218   \Drupal::moduleHandler()->loadInclude('token', 'inc', 'token.tokens');
219
220   if ($hook == 'tokens' || $hook == 'token_info' || $hook == 'token_info_alter' || $hook == 'tokens_alter') {
221     foreach (_token_core_supported_modules() as $module) {
222       if (\Drupal::moduleHandler()->moduleExists($module) && function_exists($module . '_' . $hook)) {
223         $implementations[$module] = TRUE;
224       }
225     }
226     // Move token.module to get included first since it is responsible for
227     // other modules.
228     if (isset($implementations['token'])) {
229       unset($implementations['token']);
230       $implementations = array_merge(['token' => 'tokens'], $implementations);
231     }
232   }
233 }
234
235 /**
236  * Return the module responsible for a token.
237  *
238  * @param string $type
239  *   The token type.
240  * @param string $name
241  *   The token name.
242  *
243  * @return mixed
244  *   The value of $info['tokens'][$type][$name]['module'] from token info, or
245  *   NULL if the value does not exist.
246  */
247 function _token_module($type, $name) {
248   $token_info = \Drupal::token()->getTokenInfo($type, $name);
249   return isset($token_info['module']) ? $token_info['module'] : NULL;
250 }
251
252 /**
253  * Validate a form element that should have tokens in it.
254  *
255  * Form elements that want to add this validation should have the #token_types
256  * parameter defined.
257  *
258  * For example:
259  * @code
260  * $form['my_node_text_element'] = [
261  *   '#type' => 'textfield',
262  *   '#title' => t('Some text to token-ize that has a node context.'),
263  *   '#default_value' => 'The title of this node is [node:title].',
264  *   '#element_validate' => ['token_element_validate'],
265  *   '#token_types' => ['node'],
266  *   '#min_tokens' => 1,
267  *   '#max_tokens' => 10,
268  * ];
269  * @endcode
270  */
271 function token_element_validate($element, FormStateInterface $form_state) {
272   $value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
273
274   if (!mb_strlen($value)) {
275     // Empty value needs no further validation since the element should depend
276     // on using the '#required' FAPI property.
277     return $element;
278   }
279
280   $tokens = \Drupal::token()->scan($value);
281   $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
282
283   // Validate if an element must have a minimum number of tokens.
284   if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) {
285     $error = \Drupal::translation()->formatPlural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', ['%name' => $title]);
286     $form_state->setError($element, $error);
287   }
288
289   // Validate if an element must have a maximum number of tokens.
290   if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) {
291     $error = \Drupal::translation()->formatPlural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', ['%name' => $title]);
292     $form_state->setError($element, $error);
293   }
294
295   // Check if the field defines specific token types.
296   if (isset($element['#token_types'])) {
297     $invalid_tokens = \Drupal::token()->getInvalidTokensByContext($tokens, $element['#token_types']);
298     if ($invalid_tokens) {
299       $form_state->setError($element, t('%name is using the following invalid tokens: @invalid-tokens.', ['%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)]));
300     }
301   }
302
303   return $element;
304 }
305
306 /**
307  * Implements hook_form_FORM_ID_alter().
308  */
309 function token_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
310   $field_config = $form_state->getFormObject()->getEntity();
311   $field_storage = $field_config->getFieldStorageDefinition();
312   if ($field_storage->isLocked()) {
313     return;
314   }
315   $field_type = $field_storage->getType();
316   if (($field_type == 'file' || $field_type == 'image') && isset($form['settings']['file_directory'])) {
317     // GAH! We can only support global tokens in the upload file directory path.
318     $form['settings']['file_directory']['#element_validate'][] = 'token_element_validate';
319     // Date support needs to be implicitly added, as while technically it's not
320     // a global token, it is a not only used but is the default value.
321     // https://www.drupal.org/node/2642160
322     $form['settings']['file_directory'] += ['#token_types' => ['date']];
323     $form['settings']['file_directory']['#description'] .= ' ' . t('This field supports tokens.');
324   }
325
326   // Note that the description is tokenized via token_field_widget_form_alter().
327   $form['description']['#element_validate'][] = 'token_element_validate';
328   $form['description'] += ['#token_types' => []];
329
330   $form['token_tree'] = [
331     '#theme' => 'token_tree_link',
332     '#token_types' => [],
333     '#weight' => $form['description']['#weight'] + 0.5,
334   ];
335 }
336
337 /**
338  * Implements hook_form_BASE_FORM_ID_alter().
339  *
340  * Alters the configure action form to add token context validation and
341  * adds the token tree for a better token UI and selection.
342  */
343 function token_form_action_form_alter(&$form, $form_state) {
344   if (isset($form['plugin'])) {
345     switch ($form['plugin']['#value']) {
346       case 'action_message_action':
347       case 'action_send_email_action':
348       case 'action_goto_action':
349         $form['token_tree'] = [
350           '#theme' => 'token_tree_link',
351           '#token_types' => 'all',
352           '#weight' => 100,
353         ];
354         $form['actions']['#weight'] = 101;
355         // @todo Add token validation to the action fields that can use tokens.
356         break;
357     }
358   }
359 }
360
361 /**
362  * Implements hook_form_FORM_ID_alter().
363  *
364  * Alters the user e-mail fields to add token context validation and
365  * adds the token tree for a better token UI and selection.
366  */
367 function token_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) {
368   $email_token_help = t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
369
370   foreach (Element::children($form) as $key) {
371     $element = &$form[$key];
372
373     // Remove the crummy default token help text.
374     if (!empty($element['#description'])) {
375       $element['#description'] = trim(str_replace($email_token_help, t('The list of available tokens that can be used in e-mails is provided below.'), $element['#description']));
376     }
377
378     switch ($key) {
379       case 'email_admin_created':
380       case 'email_pending_approval':
381       case 'email_no_approval_required':
382       case 'email_password_reset':
383       case 'email_cancel_confirm':
384         // Do nothing, but allow execution to continue.
385         break;
386
387       case 'email_activated':
388       case 'email_blocked':
389       case 'email_canceled':
390         // These fieldsets have their e-mail elements inside a 'settings'
391         // sub-element, so switch to that element instead.
392         $element = &$form[$key]['settings'];
393         break;
394
395       default:
396         continue 2;
397     }
398
399     foreach (Element::children($element) as $sub_key) {
400       if (!isset($element[$sub_key]['#type'])) {
401         continue;
402       }
403       elseif ($element[$sub_key]['#type'] == 'textfield' && substr($sub_key, -8) === '_subject') {
404         // Add validation to subject textfields.
405         $element[$sub_key]['#element_validate'][] = 'token_element_validate';
406         $element[$sub_key] += ['#token_types' => ['user']];
407       }
408       elseif ($element[$sub_key]['#type'] == 'textarea' && substr($sub_key, -5) === '_body') {
409         // Add validation to body textareas.
410         $element[$sub_key]['#element_validate'][] = 'token_element_validate';
411         $element[$sub_key] += ['#token_types' => ['user']];
412       }
413     }
414   }
415
416   // Add the token tree UI.
417   $form['email']['token_tree'] = [
418     '#theme' => 'token_tree_link',
419     '#token_types' => ['user'],
420     '#show_restricted' => TRUE,
421     '#show_nested' => FALSE,
422     '#weight' => 90,
423   ];
424 }
425
426 /**
427  * Prepare a string for use as a valid token name.
428  *
429  * @param $name
430  *   The token name to clean.
431  * @return
432  *   The cleaned token name.
433  */
434 function token_clean_token_name($name) {
435   static $names = [];
436
437   if (!isset($names[$name])) {
438     $cleaned_name = strtr($name, [' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '']);
439     $cleaned_name = preg_replace('/[^\w\-]/i', '', $cleaned_name);
440     $cleaned_name = trim($cleaned_name, '-');
441     $names[$name] = $cleaned_name;
442   }
443
444   return $names[$name];
445 }
446
447 /**
448  * Do not use this function yet. Its API has not been finalized.
449  */
450 function token_render_array(array $array, array $options = []) {
451   $rendered = [];
452
453   /** @var \Drupal\Core\Render\RendererInterface $renderer */
454   $renderer = \Drupal::service('renderer');
455
456   foreach (token_element_children($array) as $key) {
457     $value = $array[$key];
458     $rendered[] = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
459   }
460   $join = isset($options['join']) ? $options['join'] : ', ';
461   return implode($join, $rendered);
462 }
463
464 /**
465  * Do not use this function yet. Its API has not been finalized.
466  */
467 function token_render_array_value($value, array $options = []) {
468   /** @var \Drupal\Core\Render\RendererInterface $renderer */
469   $renderer = \Drupal::service('renderer');
470
471   $rendered = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
472   return $rendered;
473 }
474
475 /**
476  * Copy of drupal_render_cache_get() that does not care about request method.
477  */
478 function token_render_cache_get($elements) {
479   if (!$cid = drupal_render_cid_create($elements)) {
480     return FALSE;
481   }
482   $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
483
484   if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
485     // Add additional libraries, JavaScript, CSS and other data attached
486     // to this element.
487     if (isset($cache->data['#attached'])) {
488       drupal_process_attached($cache->data);
489     }
490     // Return the rendered output.
491     return $cache->data['#markup'];
492   }
493   return FALSE;
494 }
495
496 /**
497  * Coyp of drupal_render_cache_set() that does not care about request method.
498  */
499 function token_render_cache_set(&$markup, $elements) {
500   // This should only run of drupal_render_cache_set() did not.
501   if (in_array(\Drupal::request()->server->get('REQUEST_METHOD'), ['GET', 'HEAD'])) {
502     return FALSE;
503   }
504
505   $original_method = \Drupal::request()->server->get('REQUEST_METHOD');
506   \Drupal::request()->server->set('REQUEST_METHOD', 'GET');
507   drupal_render_cache_set($markup, $elements);
508   \Drupal::request()->server->set('REQUEST_METHOD', $original_method);
509 }
510
511 /**
512  * Loads menu link titles for all purents of a menu link plugin ID.
513  *
514  * @param string $plugin_id
515  *   The menu link plugin ID.
516  * @param string $langcode
517  *   The language code.
518  *
519  * @return string[]
520  *   List of menu link parent titles.
521  */
522 function token_menu_link_load_all_parents($plugin_id, $langcode) {
523   $cache = &drupal_static(__FUNCTION__, []);
524
525   if (!isset($cache[$plugin_id][$langcode])) {
526     $cache[$plugin_id][$langcode] = [];
527     /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
528     $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
529     $parent_ids = $menu_link_manager->getParentIds($plugin_id);
530     // Remove the current plugin ID from the parents.
531     unset($parent_ids[$plugin_id]);
532     foreach ($parent_ids as $parent_id) {
533       $parent = $menu_link_manager->createInstance($parent_id);
534       $cache[$plugin_id][$langcode] = [$parent_id => token_menu_link_translated_title($parent, $langcode)] + $cache[$plugin_id][$langcode];
535     }
536   }
537
538   return $cache[$plugin_id][$langcode];
539 }
540
541 /**
542  * Returns the translated link of a menu title.
543  *
544  * If the underlying entity is a content menu item, load it to get the
545  * translated menu item title.
546  *
547  * @todo Remove this when there is a better way to get a translated menu
548  *   item title in core: https://www.drupal.org/node/2795143
549  *
550  * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
551  *   The menu link.
552  * @param string|null $langcode
553  *   (optional) The langcode, defaults to the current language.
554  *
555  * @return string
556  *   The menu link title.
557  */
558 function token_menu_link_translated_title(MenuLinkInterface $menu_link, $langcode = NULL) {
559   $metadata = $menu_link->getMetaData();
560   if (isset($metadata['entity_id']) && $menu_link->getProvider() == 'menu_link_content') {
561     /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
562     $entity = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($metadata['entity_id']);
563     $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
564     return $entity->getTitle();
565   }
566   return $menu_link->getTitle();
567 }
568
569 /**
570  * Loads all the parents of the term in the specified language.
571  *
572  * @param int $tid
573  *   The term id.
574  * @param string $langcode
575  *   The language code.
576  *
577  * @return string[]
578  *   The term parents collection.
579  */
580 function token_taxonomy_term_load_all_parents($tid, $langcode) {
581   $cache = &drupal_static(__FUNCTION__, []);
582
583   if (!is_numeric($tid)) {
584     return [];
585   }
586
587   if (!isset($cache[$langcode][$tid])) {
588     $cache[$langcode][$tid] = [];
589     /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
590     $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
591     $parents = $term_storage->loadAllParents($tid);
592     // Remove this term from the array.
593     array_shift($parents);
594     $parents = array_reverse($parents);
595     foreach ($parents as $term) {
596       $translation = \Drupal::service('entity.repository')->getTranslationFromContext($term, $langcode);
597       $cache[$langcode][$tid][$term->id()] = $translation->label();
598     }
599   }
600
601   return $cache[$langcode][$tid];
602 }
603
604 function token_element_children(&$elements, $sort = FALSE) {
605   // Do not attempt to sort elements which have already been sorted.
606   $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
607
608   // Filter out properties from the element, leaving only children.
609   $children = [];
610   $sortable = FALSE;
611   foreach ($elements as $key => $value) {
612     if ($key === '' || $key[0] !== '#') {
613       $children[$key] = $value;
614       if (is_array($value) && isset($value['#weight'])) {
615         $sortable = TRUE;
616       }
617     }
618   }
619   // Sort the children if necessary.
620   if ($sort && $sortable) {
621     uasort($children, 'Drupal\Component\Utility\SortArray::sortByWeightProperty');
622     // Put the sorted children back into $elements in the correct order, to
623     // preserve sorting if the same element is passed through
624     // element_children() twice.
625     foreach ($children as $key => $child) {
626       unset($elements[$key]);
627       $elements[$key] = $child;
628     }
629     $elements['#sorted'] = TRUE;
630   }
631
632   return array_keys($children);
633 }
634
635 /**
636  * Loads all the parents of the book page.
637  *
638  * @param array $book
639  *   The book data. The 'nid' key points to the current page of the book.
640  *   The 'p1' ... 'p9' keys point to parents of the page, if they exist, with 'p1'
641  *   pointing to the book itself and the last defined pX to the current page.
642  *
643  * @return string[]
644  *   List of node titles of the book parents.
645  */
646 function token_book_load_all_parents(array $book) {
647   $cache = &drupal_static(__FUNCTION__, []);
648
649   if (empty($book['nid'])) {
650     return [];
651   }
652   $nid = $book['nid'];
653
654   if (!isset($cache[$nid])) {
655     $cache[$nid] = [];
656     $i = 1;
657     while ($book["p$i"] != $nid) {
658       $cache[$nid][] = Node::load($book["p$i"])->getTitle();
659       $i++;
660     }
661   }
662
663   return $cache[$nid];
664 }
665
666 /**
667  * Implements hook_entity_base_field_info().
668  */
669 function token_entity_base_field_info(EntityTypeInterface $entity_type) {
670   // We add a pseudo entity-reference field to track the menu entry created
671   // from the node add/edit form so that tokens generated at that time that
672   // reference the menu link can access the yet to be saved menu link.
673   // @todo Revisit when https://www.drupal.org/node/2315773 is resolved.
674   if ($entity_type->id() === 'node' && \Drupal::moduleHandler()->moduleExists('menu_ui')) {
675     $fields['menu_link'] = BaseFieldDefinition::create('entity_reference')
676       ->setLabel(t('Menu link'))
677       ->setDescription(t('Computed menu link for the node (only available during node saving).'))
678       ->setRevisionable(TRUE)
679       ->setSetting('target_type', 'menu_link_content')
680       ->setTranslatable(TRUE)
681       ->setDisplayOptions('view', [
682         'label' => 'hidden',
683         'region' => 'hidden',
684       ])
685       ->setComputed(TRUE)
686       ->setDisplayOptions('form', [
687         'region' => 'hidden',
688       ]);
689
690     return $fields;
691   }
692   return [];
693 }
694
695 /**
696  * Implements hook_form_BASE_FORM_ID_alter() for node_form.
697  *
698  * Populates menu_link field on nodes from the menu item on unsaved nodes.
699  *
700  * @see menu_ui_form_node_form_submit()
701  * @see token_entity_base_field_info()
702  */
703 function token_form_node_form_alter(&$form, FormStateInterface $form_state) {
704   if (!\Drupal::moduleHandler()->moduleExists('menu_ui')) {
705     return;
706   }
707   /** @var \Drupal\node\NodeForm $form_object */
708   if (!\Drupal::currentUser()->hasPermission('administer menu')) {
709     // We're only interested in when the node is unsaved and the editor has
710     // permission to create new menu links.
711     return;
712   }
713   $form['#entity_builders'][] = 'token_node_menu_link_submit';
714 }
715
716 /**
717  * Entity builder.
718  */
719 function token_node_menu_link_submit($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) {
720   // Entity builders run twice, once during validation and again during
721   // submission, so we only run this code after validation has been performed.
722   if (!$form_state->isValueEmpty('menu') && $form_state->getTemporaryValue('entity_validated')) {
723     $values = $form_state->getValue('menu');
724     if (!empty($values['enabled']) && trim($values['title'])) {
725       if (!empty($values['menu_parent'])) {
726         list($menu_name, $parent) = explode(':', $values['menu_parent'], 2);
727         $values['menu_name'] = $menu_name;
728         $values['parent'] = $parent;
729       }
730       // Construct an unsaved entity.
731       if ($entity_id = $form_state->getValue(['menu', 'entity_id'])) {
732         // Use the existing menu_link_content entity.
733         $entity = MenuLinkContent::load($entity_id);
734         // If the loaded MenuLinkContent doesn't have a translation for the
735         // Node's active langcode, create a new translation.
736         if ($entity->isTranslatable()) {
737           if (!$entity->hasTranslation($node->language()->getId())) {
738             $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
739           }
740           else {
741             $entity = $entity->getTranslation($node->language()->getId());
742           }
743         }
744       }
745       else {
746         if ($node->isNew()) {
747           // Create a new menu_link_content entity.
748           $entity = MenuLinkContent::create([
749             // Lets just reference the UUID for now, the link is not important for
750             // token generation.
751             'link' => ['uri' => 'internal:/node/' . $node->uuid()],
752             'langcode' => $node->language()->getId(),
753           ]);
754         }
755         else {
756           // Create a new menu_link_content entity.
757           $entity = MenuLinkContent::create([
758             'link' => ['uri' => 'entity:node/' . $node->id()],
759             'langcode' => $node->language()->getId(),
760           ]);
761         }
762       }
763       $entity->title->value = trim($values['title']);
764       $entity->description->value = trim($values['description']);
765       $entity->menu_name->value = $values['menu_name'];
766       $entity->parent->value = $values['parent'];
767       $entity->weight->value = isset($values['weight']) ? $values['weight'] : 0;
768       $entity->save();
769       $node->menu_link = $entity;
770       // Leave this for _menu_ui_node_save() to pick up so we don't end up with
771       // duplicate menu-links.
772       $form_state->setValue(['menu', 'entity_id'], $entity->id());
773     }
774   }
775 }
776
777 /**
778  * Implements hook_ENTITY_TYPE_insert for node entities.
779  */
780 function token_node_insert(NodeInterface $node) {
781   if ($node->hasField('menu_link') && $menu_link = $node->menu_link->entity) {
782     // Update the menu-link to point to the now saved node.
783     $menu_link->link = 'entity:node/' . $node->id();
784     $menu_link->save();
785   }
786 }
787
788 /**
789  * Implements hook_ENTITY_TYPE_presave() for menu_link_content.
790  */
791 function token_menu_link_content_presave(MenuLinkContentInterface $menu_link_content) {
792   drupal_static_reset('token_menu_link_load_all_parents');
793 }