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