Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / token / token.tokens.inc
1 <?php
2
3 /**
4  * @file
5  * Token callbacks for the token module.
6  */
7
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Core\Entity\FieldableEntityInterface;
10 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
11 use Drupal\Core\Render\BubbleableMetadata;
12 use Drupal\Core\Render\Element;
13 use Drupal\Component\Utility\Crypt;
14 use Drupal\Component\Utility\Html;
15 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
16 use Drupal\Core\Url;
17 use Drupal\field\FieldStorageConfigInterface;
18 use Drupal\menu_link_content\MenuLinkContentInterface;
19 use Drupal\node\Entity\Node;
20 use Drupal\node\Entity\NodeType;
21 use Drupal\node\NodeInterface;
22 use Drupal\system\Entity\Menu;
23 use Drupal\user\UserInterface;
24 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
25 use Drupal\Core\TypedData\PrimitiveInterface;
26 use Drupal\Core\Field\FieldStorageDefinitionInterface;
27 use Drupal\Core\Entity\ContentEntityTypeInterface;
28 use Drupal\image\Entity\ImageStyle;
29
30 /**
31  * Implements hook_token_info_alter().
32  */
33 function token_token_info_alter(&$info) {
34   // Force 'date' type tokens to require input and add a 'current-date' type.
35   // @todo Remove when http://drupal.org/node/943028 is fixed.
36   $info['types']['date']['needs-data'] = 'date';
37   $info['types']['current-date'] = [
38     'name' => t('Current date'),
39     'description' => t('Tokens related to the current date and time.'),
40     'type' => 'date',
41   ];
42
43   // Add a 'dynamic' key to any tokens that have chained but dynamic tokens.
44   $info['tokens']['date']['custom']['dynamic'] = TRUE;
45
46   // The [file:size] may not always return in kilobytes.
47   // @todo Remove when http://drupal.org/node/1193044 is fixed.
48   if (!empty($info['tokens']['file']['size'])) {
49     $info['tokens']['file']['size']['description'] = t('The size of the file.');
50   }
51
52   // Remove deprecated tokens from being listed.
53   unset($info['tokens']['node']['tnid']);
54   unset($info['tokens']['node']['type']);
55   unset($info['tokens']['node']['type-name']);
56
57   // Support 'url' type tokens for core tokens.
58   if (isset($info['tokens']['comment']['url']) && \Drupal::moduleHandler()->moduleExists('comment')) {
59     $info['tokens']['comment']['url']['type'] = 'url';
60   }
61   if (isset($info['tokens']['node']['url']) && \Drupal::moduleHandler()->moduleExists('node')) {
62     $info['tokens']['node']['url']['type'] = 'url';
63   }
64   if (isset($info['tokens']['term']['url']) && \Drupal::moduleHandler()->moduleExists('taxonomy')) {
65     $info['tokens']['term']['url']['type'] = 'url';
66   }
67   $info['tokens']['user']['url']['type'] = 'url';
68
69   // Add [token:url] tokens for any URI-able entities.
70   $entities = \Drupal::entityTypeManager()->getDefinitions();
71   foreach ($entities as $entity => $entity_info) {
72     // Do not generate tokens if the entity doesn't define a token type or is
73     // not a content entity.
74     if (!$entity_info->get('token_type') || (!$entity_info instanceof ContentEntityTypeInterface)) {
75       continue;
76     }
77
78     $token_type = $entity_info->get('token_type');
79     if (!isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
80       // Define tokens for entity type's without their own integration.
81       $info['types'][$entity_info->id()] = [
82         'name' => $entity_info->getLabel(),
83         'needs-data' => $entity_info->id(),
84         'module' => 'token',
85       ];
86     }
87
88     // Add [entity:url] tokens if they do not already exist.
89     // @todo Support entity:label
90     if (!isset($info['tokens'][$token_type]['url'])) {
91       $info['tokens'][$token_type]['url'] = [
92         'name' => t('URL'),
93         'description' => t('The URL of the @entity.', ['@entity' => mb_strtolower($entity_info->getLabel())]),
94         'module' => 'token',
95         'type' => 'url',
96       ];
97     }
98
99     // Add [entity:original] tokens if they do not already exist.
100     if (!isset($info['tokens'][$token_type]['original'])) {
101       $info['tokens'][$token_type]['original'] = [
102         'name' => t('Original @entity', ['@entity' => mb_strtolower($entity_info->getLabel())]),
103         'description' => t('The original @entity data if the @entity is being updated or saved.', ['@entity' => mb_strtolower($entity_info->getLabel())]),
104         'module' => 'token',
105         'type' => $token_type,
106       ];
107     }
108   }
109
110   // Add support for custom date formats.
111   // @todo Remove when http://drupal.org/node/1173706 is fixed.
112   $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple();
113   foreach ($date_format_types as $date_format_type => $date_format_type_info) {
114     /* @var \Drupal\system\Entity\DateFormat $date_format_type_info */
115     if (!isset($info['tokens']['date'][$date_format_type])) {
116       $info['tokens']['date'][$date_format_type] = [
117         'name' => Html::escape($date_format_type_info->label()),
118         'description' => t("A date in '@type' format. (%date)", ['@type' => $date_format_type, '%date' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), $date_format_type)]),
119         'module' => 'token',
120       ];
121     }
122   }
123 }
124
125 /**
126  * Implements hook_token_info().
127  */
128 function token_token_info() {
129   // Node tokens.
130   $info['tokens']['node']['source'] = [
131     'name' => t('Translation source node'),
132     'description' => t("The source node for this current node's translation set."),
133     'type' => 'node',
134   ];
135   $info['tokens']['node']['log'] = [
136     'name' => t('Revision log message'),
137     'description' => t('The explanation of the most recent changes made to the node.'),
138   ];
139   $info['tokens']['node']['content-type'] = [
140     'name' => t('Content type'),
141     'description' => t('The content type of the node.'),
142     'type' => 'content-type',
143   ];
144
145   // Content type tokens.
146   $info['types']['content-type'] = [
147     'name' => t('Content types'),
148     'description' => t('Tokens related to content types.'),
149     'needs-data' => 'node_type',
150   ];
151   $info['tokens']['content-type']['name'] = [
152     'name' => t('Name'),
153     'description' => t('The name of the content type.'),
154   ];
155   $info['tokens']['content-type']['machine-name'] = [
156     'name' => t('Machine-readable name'),
157     'description' => t('The unique machine-readable name of the content type.'),
158   ];
159   $info['tokens']['content-type']['description'] = [
160     'name' => t('Description'),
161     'description' => t('The optional description of the content type.'),
162   ];
163   $info['tokens']['content-type']['node-count'] = [
164     'name' => t('Node count'),
165     'description' => t('The number of nodes belonging to the content type.'),
166   ];
167   $info['tokens']['content-type']['edit-url'] = [
168     'name' => t('Edit URL'),
169     'description' => t("The URL of the content type's edit page."),
170     // 'type' => 'url',
171   ];
172
173   // Taxonomy term and vocabulary tokens.
174   if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
175     $info['tokens']['term']['edit-url'] = [
176       'name' => t('Edit URL'),
177       'description' => t("The URL of the taxonomy term's edit page."),
178       // 'type' => 'url',
179     ];
180     $info['tokens']['term']['parents'] = [
181       'name' => t('Parents'),
182       'description' => t("An array of all the term's parents, starting with the root."),
183       'type' => 'array',
184     ];
185     $info['tokens']['term']['root'] = [
186       'name' => t('Root term'),
187       'description' => t("The root term of the taxonomy term."),
188       'type' => 'term',
189     ];
190
191     $info['tokens']['vocabulary']['machine-name'] = [
192       'name' => t('Machine-readable name'),
193       'description' => t('The unique machine-readable name of the vocabulary.'),
194     ];
195     $info['tokens']['vocabulary']['edit-url'] = [
196       'name' => t('Edit URL'),
197       'description' => t("The URL of the vocabulary's edit page."),
198       // 'type' => 'url',
199     ];
200   }
201
202   // File tokens.
203   $info['tokens']['file']['basename'] = [
204     'name' => t('Base name'),
205     'description' => t('The base name of the file.'),
206   ];
207   $info['tokens']['file']['extension'] = [
208     'name' => t('Extension'),
209     'description' => t('The extension of the file.'),
210   ];
211   $info['tokens']['file']['size-raw'] = [
212     'name' => t('File byte size'),
213     'description' => t('The size of the file, in bytes.'),
214   ];
215
216   // User tokens.
217   // Add information on the restricted user tokens.
218   $info['tokens']['user']['cancel-url'] = [
219     'name' => t('Account cancellation URL'),
220     'description' => t('The URL of the confirm delete page for the user account.'),
221     'restricted' => TRUE,
222     // 'type' => 'url',
223   ];
224   $info['tokens']['user']['one-time-login-url'] = [
225     'name' => t('One-time login URL'),
226     'description' => t('The URL of the one-time login page for the user account.'),
227     'restricted' => TRUE,
228     // 'type' => 'url',
229   ];
230   $info['tokens']['user']['roles'] = [
231     'name' => t('Roles'),
232     'description' => t('The user roles associated with the user account.'),
233     'type' => 'array',
234   ];
235
236   // Current user tokens.
237   $info['tokens']['current-user']['ip-address'] = [
238     'name' => t('IP address'),
239     'description' => t('The IP address of the current user.'),
240   ];
241
242   // Menu link tokens (work regardless if menu module is enabled or not).
243   $info['types']['menu-link'] = [
244     'name' => t('Menu links'),
245     'description' => t('Tokens related to menu links.'),
246     'needs-data' => 'menu-link',
247   ];
248   $info['tokens']['menu-link']['mlid'] = [
249     'name' => t('Link ID'),
250     'description' => t('The unique ID of the menu link.'),
251   ];
252   $info['tokens']['menu-link']['title'] = [
253     'name' => t('Title'),
254     'description' => t('The title of the menu link.'),
255   ];
256   $info['tokens']['menu-link']['url'] = [
257     'name' => t('URL'),
258     'description' => t('The URL of the menu link.'),
259     'type' => 'url',
260   ];
261   $info['tokens']['menu-link']['parent'] = [
262     'name' => t('Parent'),
263     'description' => t("The menu link's parent."),
264     'type' => 'menu-link',
265   ];
266   $info['tokens']['menu-link']['parents'] = [
267     'name' => t('Parents'),
268     'description' => t("An array of all the menu link's parents, starting with the root."),
269     'type' => 'array',
270   ];
271   $info['tokens']['menu-link']['root'] = [
272     'name' => t('Root'),
273     'description' => t("The menu link's root."),
274     'type' => 'menu-link',
275   ];
276
277   // Current page tokens.
278   $info['types']['current-page'] = [
279     'name' => t('Current page'),
280     'description' => t('Tokens related to the current page request.'),
281   ];
282   $info['tokens']['current-page']['title'] = [
283     'name' => t('Title'),
284     'description' => t('The title of the current page.'),
285   ];
286   $info['tokens']['current-page']['url'] = [
287     'name' => t('URL'),
288     'description' => t('The URL of the current page.'),
289     'type' => 'url',
290   ];
291   $info['tokens']['current-page']['page-number'] = [
292     'name' => t('Page number'),
293     'description' => t('The page number of the current page when viewing paged lists.'),
294   ];
295   $info['tokens']['current-page']['query'] = [
296     'name' => t('Query string value'),
297     'description' => t('The value of a specific query string field of the current page.'),
298     'dynamic' => TRUE,
299   ];
300
301   // URL tokens.
302   $info['types']['url'] = [
303     'name' => t('URL'),
304     'description' => t('Tokens related to URLs.'),
305     'needs-data' => 'path',
306   ];
307   $info['tokens']['url']['path'] = [
308     'name' => t('Path'),
309     'description' => t('The path component of the URL.'),
310   ];
311   $info['tokens']['url']['relative'] = [
312     'name' => t('Relative URL'),
313     'description' => t('The relative URL.'),
314   ];
315   $info['tokens']['url']['absolute'] = [
316     'name' => t('Absolute URL'),
317     'description' => t('The absolute URL.'),
318   ];
319   $info['tokens']['url']['brief'] = [
320     'name' => t('Brief URL'),
321     'description' => t('The URL without the protocol and trailing backslash.'),
322   ];
323   $info['tokens']['url']['unaliased'] = [
324     'name' => t('Unaliased URL'),
325     'description' => t('The unaliased URL.'),
326     'type' => 'url',
327   ];
328   $info['tokens']['url']['args'] = [
329     'name' => t('Arguments'),
330     'description' => t("The specific argument of the current page (e.g. 'arg:1' on the page 'node/1' returns '1')."),
331     'type' => 'array',
332   ];
333
334   // Array tokens.
335   $info['types']['array'] = [
336     'name' => t('Array'),
337     'description' => t('Tokens related to arrays of strings.'),
338     'needs-data' => 'array',
339     'nested' => TRUE,
340   ];
341   $info['tokens']['array']['first'] = [
342     'name' => t('First'),
343     'description' => t('The first element of the array.'),
344   ];
345   $info['tokens']['array']['last'] = [
346     'name' => t('Last'),
347     'description' => t('The last element of the array.'),
348   ];
349   $info['tokens']['array']['count'] = [
350     'name' => t('Count'),
351     'description' => t('The number of elements in the array.'),
352   ];
353   $info['tokens']['array']['reversed'] = [
354     'name' => t('Reversed'),
355     'description' => t('The array reversed.'),
356     'type' => 'array',
357   ];
358   $info['tokens']['array']['keys'] = [
359     'name' => t('Keys'),
360     'description' => t('The array of keys of the array.'),
361     'type' => 'array',
362   ];
363   $info['tokens']['array']['join'] = [
364     'name' => t('Imploded'),
365     'description' => t('The values of the array joined together with a custom string in-between each value.'),
366     'dynamic' => TRUE,
367   ];
368   $info['tokens']['array']['value'] = [
369     'name' => t('Value'),
370     'description' => t('The specific value of the array.'),
371     'dynamic' => TRUE,
372   ];
373
374   // Random tokens.
375   $info['types']['random'] = [
376     'name' => t('Random'),
377     'description' => t('Tokens related to random data.'),
378   ];
379   $info['tokens']['random']['number'] = [
380     'name' => t('Number'),
381     'description' => t('A random number from 0 to @max.', ['@max' => mt_getrandmax()]),
382   ];
383   $info['tokens']['random']['hash'] = [
384     'name' => t('Hash'),
385     'description' => t('A random hash. The possible hashing algorithms are: @hash-algos.', ['@hash-algos' => implode(', ', hash_algos())]),
386     'dynamic' => TRUE,
387   ];
388
389   // Define image_with_image_style token type.
390   if (\Drupal::moduleHandler()->moduleExists('image')) {
391     $info['types']['image_with_image_style'] = [
392       'name' => t('Image with image style'),
393       'needs-data' => 'image_with_image_style',
394       'module' => 'token',
395       'nested' => TRUE,
396     ];
397
398     // Provide tokens for the ImageStyle attributes.
399     $info['tokens']['image_with_image_style']['mimetype'] = [
400       'name' => t('MIME type'),
401       'description' => t('The MIME type (image/png, image/bmp, etc.) of the image.'),
402     ];
403     $info['tokens']['image_with_image_style']['filesize'] = [
404       'name' => t('File size'),
405       'description' => t('The file size of the image.'),
406     ];
407     $info['tokens']['image_with_image_style']['height'] = [
408       'name' => t('Height'),
409       'description' => t('The height the image, in pixels.'),
410     ];
411     $info['tokens']['image_with_image_style']['width'] = [
412       'name' => t('Width'),
413       'description' => t('The width of the image, in pixels.'),
414     ];
415     $info['tokens']['image_with_image_style']['uri'] = [
416       'name' => t('URI'),
417       'description' => t('The URI to the image.'),
418     ];
419     $info['tokens']['image_with_image_style']['url'] = [
420       'name' => t('URL'),
421       'description' => t('The URL to the image.'),
422     ];
423   }
424
425   return $info;
426 }
427
428 /**
429  * Implements hook_tokens().
430  */
431 function token_tokens($type, array $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
432   $replacements = [];
433   $language_manager = \Drupal::languageManager();
434   $url_options = ['absolute' => TRUE];
435   if (isset($options['langcode'])) {
436     $url_options['language'] = $language_manager->getLanguage($options['langcode']);
437     $langcode = $options['langcode'];
438   }
439   else {
440     $langcode = $language_manager->getCurrentLanguage()->getId();
441   }
442
443   // Date tokens.
444   if ($type == 'date') {
445     $date = !empty($data['date']) ? $data['date'] : \Drupal::time()->getRequestTime();
446
447     // @todo Remove when http://drupal.org/node/1173706 is fixed.
448     $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple();
449     foreach ($tokens as $name => $original) {
450       if (isset($date_format_types[$name]) && _token_module('date', $name) == 'token') {
451         $replacements[$original] = \Drupal::service('date.formatter')->format($date, $name, '', NULL, $langcode);
452       }
453     }
454   }
455
456   // Current date tokens.
457   // @todo Remove when http://drupal.org/node/943028 is fixed.
458   if ($type == 'current-date') {
459     $replacements += \Drupal::token()->generate('date', $tokens, ['date' => \Drupal::time()->getRequestTime()], $options, $bubbleable_metadata);
460   }
461
462   // Comment tokens.
463   if ($type == 'comment' && !empty($data['comment'])) {
464     /* @var \Drupal\comment\CommentInterface $comment */
465     $comment = $data['comment'];
466
467     // Chained token relationships.
468     if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
469       // Add fragment to url options.
470       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $comment->toUrl('canonical', ['fragment' => "comment-{$comment->id()}"])], $options, $bubbleable_metadata);
471     }
472   }
473
474   // Node tokens.
475   if ($type == 'node' && !empty($data['node'])) {
476     /* @var \Drupal\node\NodeInterface $node */
477     $node = $data['node'];
478
479     foreach ($tokens as $name => $original) {
480       switch ($name) {
481         case 'log':
482           $replacements[$original] = (string) $node->revision_log->value;
483           break;
484
485         case 'content-type':
486           $type_name = \Drupal::entityTypeManager()->getStorage('node_type')->load($node->getType())->label();
487           $replacements[$original] = $type_name;
488           break;
489       }
490     }
491
492     // Chained token relationships.
493     if (($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'source')) && $source_node = $node->getUntranslated()) {
494       $replacements += \Drupal::token()->generate('node', $parent_tokens, ['node' => $source_node], $options, $bubbleable_metadata);
495     }
496     if (($node_type_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-type')) && $node_type = NodeType::load($node->bundle())) {
497       $replacements += \Drupal::token()->generate('content-type', $node_type_tokens, ['node_type' => $node_type], $options, $bubbleable_metadata);
498     }
499     if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
500       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $node->toUrl()], $options, $bubbleable_metadata);
501     }
502   }
503
504   // Content type tokens.
505   if ($type == 'content-type' && !empty($data['node_type'])) {
506     /* @var \Drupal\node\NodeTypeInterface $node_type */
507     $node_type = $data['node_type'];
508
509     foreach ($tokens as $name => $original) {
510       switch ($name) {
511         case 'name':
512           $replacements[$original] = $node_type->label();
513           break;
514
515         case 'machine-name':
516           $replacements[$original] = $node_type->id();
517           break;
518
519         case 'description':
520           $replacements[$original] = $node_type->getDescription();
521           break;
522
523         case 'node-count':
524           $count = \Drupal::entityQueryAggregate('node')
525             ->aggregate('nid', 'COUNT')
526             ->condition('type', $node_type->id())
527             ->execute();
528           $replacements[$original] = (int) $count;
529           break;
530
531         case 'edit-url':
532           $replacements[$original] = $node_type->toUrl('edit-form', $url_options)->toString();
533           break;
534       }
535     }
536   }
537
538   // Taxonomy term tokens.
539   if ($type == 'term' && !empty($data['term'])) {
540     /* @var \Drupal\taxonomy\TermInterface $term */
541     $term = $data['term'];
542
543     /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
544     $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
545
546     foreach ($tokens as $name => $original) {
547       switch ($name) {
548         case 'edit-url':
549           $replacements[$original] = Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $term->id()], $url_options)->toString();
550           break;
551
552         case 'parents':
553           if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) {
554             $replacements[$original] = token_render_array($parents, $options);
555           }
556           break;
557
558         case 'root':
559           $parents = $term_storage->loadAllParents($term->id());
560           $root_term = end($parents);
561           if ($root_term->id() != $term->id()) {
562             $root_term = \Drupal::service('entity.repository')->getTranslationFromContext($root_term, $langcode);
563             $replacements[$original] = $root_term->label();
564           }
565           break;
566       }
567     }
568
569     // Chained token relationships.
570     if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) {
571       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $term->toUrl()], $options, $bubbleable_metadata);
572     }
573     // [term:parents:*] chained tokens.
574     if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
575       if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) {
576         $replacements += \Drupal::token()->generate('array', $parents_tokens, ['array' => $parents], $options, $bubbleable_metadata);
577       }
578     }
579     if ($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) {
580       $parents = $term_storage->loadAllParents($term->id());
581       $root_term = end($parents);
582       if ($root_term->tid != $term->id()) {
583         $replacements += \Drupal::token()->generate('term', $root_tokens, ['term' => $root_term], $options, $bubbleable_metadata);
584       }
585     }
586   }
587
588   // Vocabulary tokens.
589   if ($type == 'vocabulary' && !empty($data['vocabulary'])) {
590     $vocabulary = $data['vocabulary'];
591
592     foreach ($tokens as $name => $original) {
593       switch ($name) {
594         case 'machine-name':
595           $replacements[$original] = $vocabulary->id();
596           break;
597
598         case 'edit-url':
599           $replacements[$original] = Url::fromRoute('entity.taxonomy_vocabulary.edit_form', ['taxonomy_vocabulary' => $vocabulary->id()], $url_options)->toString();
600           break;
601       }
602     }
603   }
604
605   // File tokens.
606   if ($type == 'file' && !empty($data['file'])) {
607     $file = $data['file'];
608
609     foreach ($tokens as $name => $original) {
610       switch ($name) {
611         case 'basename':
612           $basename = pathinfo($file->uri->value, PATHINFO_BASENAME);
613           $replacements[$original] = $basename;
614           break;
615
616         case 'extension':
617           $extension = pathinfo($file->uri->value, PATHINFO_EXTENSION);
618           $replacements[$original] = $extension;
619           break;
620
621         case 'size-raw':
622           $replacements[$original] = (int) $file->filesize->value;
623           break;
624       }
625     }
626   }
627
628   // User tokens.
629   if ($type == 'user' && !empty($data['user'])) {
630     /* @var \Drupal\user\UserInterface $account */
631     $account = $data['user'];
632
633     foreach ($tokens as $name => $original) {
634       switch ($name) {
635         case 'picture':
636           if ($account instanceof UserInterface && $account->hasField('user_picture')) {
637             /** @var \Drupal\Core\Render\RendererInterface $renderer */
638             $renderer = \Drupal::service('renderer');
639             $output = [
640               '#theme' => 'user_picture',
641               '#account' => $account,
642             ];
643             $replacements[$original] = $renderer->renderPlain($output);
644           }
645           break;
646
647         case 'roles':
648           $roles = $account->getRoles();
649           $roles_names = array_combine($roles, $roles);
650           $replacements[$original] = token_render_array($roles_names, $options);
651           break;
652       }
653     }
654
655     // Chained token relationships.
656     if ($account instanceof UserInterface && $account->hasField('user_picture') && ($picture_tokens = \Drupal::token()->findWithPrefix($tokens, 'picture'))) {
657       $replacements += \Drupal::token()->generate('file', $picture_tokens, ['file' => $account->user_picture->entity], $options, $bubbleable_metadata);
658     }
659     if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
660       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $account->toUrl()], $options, $bubbleable_metadata);
661     }
662     if ($role_tokens = \Drupal::token()->findWithPrefix($tokens, 'roles')) {
663       $roles = $account->getRoles();
664       $roles_names = array_combine($roles, $roles);
665       $replacements += \Drupal::token()->generate('array', $role_tokens, ['array' => $roles_names], $options, $bubbleable_metadata);
666     }
667   }
668
669   // Current user tokens.
670   if ($type == 'current-user') {
671     foreach ($tokens as $name => $original) {
672       switch ($name) {
673         case 'ip-address':
674           $ip = \Drupal::request()->getClientIp();
675           $replacements[$original] = $ip;
676           break;
677       }
678     }
679   }
680
681   // Menu link tokens.
682   if ($type == 'menu-link' && !empty($data['menu-link'])) {
683     /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
684     $link = $data['menu-link'];
685     /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
686     $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
687
688     if ($link instanceof MenuLinkContentInterface) {
689       $link = $menu_link_manager->createInstance($link->getPluginId());
690     }
691
692     foreach ($tokens as $name => $original) {
693       switch ($name) {
694         case 'id':
695           $replacements[$original] = $link->getPluginId();
696           break;
697
698         case 'title':
699           $replacements[$original] = token_menu_link_translated_title($link, $langcode);
700           break;
701
702         case 'url':
703           $replacements[$original] = $link->getUrlObject()->setAbsolute()->toString();
704           break;
705
706         case 'parent':
707           /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
708           if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) {
709             $replacements[$original] = token_menu_link_translated_title($parent, $langcode);
710           }
711           break;
712
713         case 'parents':
714           if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) {
715             $replacements[$original] = token_render_array($parents, $options);
716           }
717           break;
718
719         case 'root';
720           if ($link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) {
721             $root = $menu_link_manager->createInstance(array_shift($parent_ids));
722             $replacements[$original] = token_menu_link_translated_title($root, $langcode);
723           }
724           break;
725       }
726     }
727
728     // Chained token relationships.
729     /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */
730     if ($link->getParent() && ($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) && $parent = $menu_link_manager->createInstance($link->getParent())) {
731       $replacements += \Drupal::token()->generate('menu-link', $parent_tokens, ['menu-link' => $parent], $options, $bubbleable_metadata);
732     }
733     // [menu-link:parents:*] chained tokens.
734     if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
735       if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) {
736         $replacements += \Drupal::token()->generate('array', $parents_tokens, ['array' => $parents], $options, $bubbleable_metadata);
737       }
738     }
739     if (($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) && $link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) {
740       $root = $menu_link_manager->createInstance(array_shift($parent_ids));
741       $replacements += \Drupal::token()->generate('menu-link', $root_tokens, ['menu-link' => $root], $options, $bubbleable_metadata);
742     }
743     if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
744       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $link->getUrlObject()], $options, $bubbleable_metadata);
745     }
746
747   }
748
749   // Current page tokens.
750   if ($type == 'current-page') {
751     $request = \Drupal::request();
752     foreach ($tokens as $name => $original) {
753       switch ($name) {
754         case 'title':
755           $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
756           if ($route) {
757             $title = \Drupal::service('title_resolver')->getTitle($request, $route);
758             $replacements[$original] = token_render_array_value($title);
759           }
760           break;
761
762         case 'url':
763           $bubbleable_metadata->addCacheContexts(['url.path']);
764           try {
765             $url = Url::createFromRequest($request)->setOptions($url_options);
766           }
767           catch (\Exception $e) {
768             // Url::createFromRequest() can fail, e.g. on 404 pages.
769             // Fall back and try again with Url::fromUserInput().
770             try {
771               $url = Url::fromUserInput($request->getPathInfo(), $url_options);
772             }
773             catch (\Exception $e) {
774               // Instantiation would fail again on malformed urls.
775             }
776           }
777           if (isset($url)) {
778             $replacements[$original] = $url->toString();
779           }
780           break;
781
782         case 'page-number':
783           if ($page = $request->query->get('page')) {
784             // @see PagerDefault::execute()
785             $pager_page_array = explode(',', $page);
786             $page = $pager_page_array[0];
787           }
788           $replacements[$original] = (int) $page + 1;
789           break;
790       }
791     }
792
793     // @deprecated
794     // [current-page:arg] dynamic tokens.
795     if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'arg')) {
796       $path = ltrim(\Drupal::service('path.current')->getPath(), '/');
797       // Make sure its a system path.
798       $path = \Drupal::service('path.alias_manager')->getPathByAlias($path);
799       foreach ($arg_tokens as $name => $original) {
800         $parts = explode('/', $path);
801         if (is_numeric($name) && isset($parts[$name])) {
802           $replacements[$original] = $parts[$name];
803         }
804       }
805     }
806
807     // [current-page:query] dynamic tokens.
808     if ($query_tokens = \Drupal::token()->findWithPrefix($tokens, 'query')) {
809       $bubbleable_metadata->addCacheContexts(['url.query_args']);
810       foreach ($query_tokens as $name => $original) {
811         if (\Drupal::request()->query->has($name)) {
812           $value = \Drupal::request()->query->get($name);
813           $replacements[$original] = $value;
814         }
815       }
816     }
817
818     // Chained token relationships.
819     if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) {
820       $url = NULL;
821       try {
822         $url = Url::createFromRequest($request)->setOptions($url_options);
823       }
824       catch (\Exception $e) {
825         // Url::createFromRequest() can fail, e.g. on 404 pages.
826         // Fall back and try again with Url::fromUserInput().
827         try {
828           $url = Url::fromUserInput($request->getPathInfo(), $url_options);
829         }
830         catch (\Exception $e) {
831           // Instantiation would fail again on malformed urls.
832         }
833       }
834       // Add cache contexts to ensure this token functions on a per-path basis
835       $bubbleable_metadata->addCacheContexts(['url.path']);
836       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $url], $options, $bubbleable_metadata);
837     }
838   }
839
840   // URL tokens.
841   if ($type == 'url' && !empty($data['url'])) {
842     /** @var \Drupal\Core\Url $url */
843     $url = $data['url'];
844     // To retrieve the correct path, modify a copy of the Url object.
845     $path_url = clone $url;
846     $path = '/';
847     // Ensure the URL is routed to avoid throwing an exception.
848     if ($url->isRouted()) {
849       $path .= $path_url->setAbsolute(FALSE)->setOption('fragment', NULL)->getInternalPath();
850     }
851
852     foreach ($tokens as $name => $original) {
853       switch ($name) {
854         case 'path':
855           $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
856           $replacements[$original] = $value;
857           break;
858
859         case 'alias':
860           // @deprecated
861           $alias = \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode);
862           $replacements[$original] = $alias;
863           break;
864
865         case 'absolute':
866           $replacements[$original] = $url->setAbsolute()->toString();
867           break;
868
869         case 'relative':
870           $replacements[$original] = $url->setAbsolute(FALSE)->toString();
871           break;
872
873         case 'brief':
874           $replacements[$original] = preg_replace(['!^https?://!', '!/$!'], '', $url->setAbsolute()->toString());
875           break;
876
877         case 'unaliased':
878           $unaliased = clone $url;
879           $replacements[$original] = $unaliased->setAbsolute()->setOption('alias', TRUE)->toString();
880           break;
881
882         case 'args':
883           $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
884           $replacements[$original] = token_render_array(explode('/', $value), $options);
885           break;
886       }
887     }
888
889     // [url:args:*] chained tokens.
890     if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'args')) {
891       $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path;
892       $replacements += \Drupal::token()->generate('array', $arg_tokens, ['array' => explode('/', ltrim($value, '/'))], $options, $bubbleable_metadata);
893     }
894
895     // [url:unaliased:*] chained tokens.
896     if ($unaliased_tokens = \Drupal::token()->findWithPrefix($tokens, 'unaliased')) {
897       $url->setOption('alias', TRUE);
898       $replacements += \Drupal::token()->generate('url', $unaliased_tokens, ['url' => $url], $options, $bubbleable_metadata);
899     }
900   }
901
902   // Entity tokens.
903   if (!empty($data[$type]) && $entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) {
904     /* @var \Drupal\Core\Entity\EntityInterface $entity */
905     $entity = $data[$type];
906
907     foreach ($tokens as $name => $original) {
908       switch ($name) {
909         case 'url':
910           if (_token_module($type, 'url') === 'token' && !$entity->isNew() && $entity->hasLinkTemplate('canonical')) {
911             $replacements[$original] = $entity->toUrl('canonical')->toString();
912           }
913           break;
914
915         case 'original':
916           if (_token_module($type, 'original') == 'token' && !empty($entity->original)) {
917             $label = $entity->original->label();
918             $replacements[$original] = $label;
919           }
920           break;
921       }
922     }
923
924     // [entity:url:*] chained tokens.
925     if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) && _token_module($type, 'url') == 'token') {
926       $replacements += \Drupal::token()->generate('url', $url_tokens, ['url' => $entity->toUrl()], $options, $bubbleable_metadata);
927     }
928
929     // [entity:original:*] chained tokens.
930     if (($original_tokens = \Drupal::token()->findWithPrefix($tokens, 'original')) && _token_module($type, 'original') == 'token' && !empty($entity->original)) {
931       $replacements += \Drupal::token()->generate($type, $original_tokens, [$type => $entity->original], $options, $bubbleable_metadata);
932     }
933
934     // Pass through to an generic 'entity' token type generation.
935     $entity_data = [
936       'entity_type' => $entity_type,
937       'entity' => $entity,
938       'token_type' => $type,
939     ];
940     // @todo Investigate passing through more data like everything from entity_extract_ids().
941     $replacements += \Drupal::token()->generate('entity', $tokens, $entity_data, $options, $bubbleable_metadata);
942   }
943
944   // Array tokens.
945   if ($type == 'array' && !empty($data['array']) && is_array($data['array'])) {
946     $array = $data['array'];
947
948     $sort = isset($options['array sort']) ? $options['array sort'] : TRUE;
949     $keys = token_element_children($array, $sort);
950
951     /** @var \Drupal\Core\Render\RendererInterface $renderer */
952     $renderer = \Drupal::service('renderer');
953
954     foreach ($tokens as $name => $original) {
955       switch ($name) {
956         case 'first':
957           $value = $array[$keys[0]];
958           $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
959           $replacements[$original] = $value;
960           break;
961
962         case 'last':
963           $value = $array[$keys[count($keys) - 1]];
964           $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
965           $replacements[$original] = $value;
966           break;
967
968         case 'count':
969           $replacements[$original] = count($keys);
970           break;
971
972         case 'keys':
973           $replacements[$original] = token_render_array($keys, $options);
974           break;
975
976         case 'reversed':
977           $reversed = array_reverse($array, TRUE);
978           $replacements[$original] = token_render_array($reversed, $options);
979           break;
980
981         case 'join':
982           $replacements[$original] = token_render_array($array, ['join' => ''] + $options);
983           break;
984       }
985     }
986
987     // [array:value:*] dynamic tokens.
988     if ($value_tokens = \Drupal::token()->findWithPrefix($tokens, 'value')) {
989       foreach ($value_tokens as $key => $original) {
990         if ($key[0] !== '#' && isset($array[$key])) {
991           $replacements[$original] = token_render_array_value($array[$key], $options);
992         }
993       }
994     }
995
996     // [array:join:*] dynamic tokens.
997     if ($join_tokens = \Drupal::token()->findWithPrefix($tokens, 'join')) {
998       foreach ($join_tokens as $join => $original) {
999         $replacements[$original] = token_render_array($array, ['join' => $join] + $options);
1000       }
1001     }
1002
1003     // [array:keys:*] chained tokens.
1004     if ($key_tokens = \Drupal::token()->findWithPrefix($tokens, 'keys')) {
1005       $replacements += \Drupal::token()->generate('array', $key_tokens, ['array' => $keys], $options, $bubbleable_metadata);
1006     }
1007
1008     // [array:reversed:*] chained tokens.
1009     if ($reversed_tokens = \Drupal::token()->findWithPrefix($tokens, 'reversed')) {
1010       $replacements += \Drupal::token()->generate('array', $reversed_tokens, ['array' => array_reverse($array, TRUE)], ['array sort' => FALSE] + $options, $bubbleable_metadata);
1011     }
1012
1013     // @todo Handle if the array values are not strings and could be chained.
1014   }
1015
1016   // Random tokens.
1017   if ($type == 'random') {
1018     foreach ($tokens as $name => $original) {
1019       switch ($name) {
1020         case 'number':
1021           $replacements[$original] = mt_rand();
1022           break;
1023       }
1024     }
1025
1026     // [custom:hash:*] dynamic token.
1027     if ($hash_tokens = \Drupal::token()->findWithPrefix($tokens, 'hash')) {
1028       $algos = hash_algos();
1029       foreach ($hash_tokens as $name => $original) {
1030         if (in_array($name, $algos)) {
1031           $replacements[$original] = hash($name, Crypt::randomBytes(55));
1032         }
1033       }
1034     }
1035   }
1036
1037   // If $type is a token type, $data[$type] is empty but $data[$entity_type] is
1038   // not, re-run token replacements.
1039   if (empty($data[$type]) && ($entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) && $entity_type != $type && !empty($data[$entity_type]) && empty($options['recursive'])) {
1040     $data[$type] = $data[$entity_type];
1041     $options['recursive'] = TRUE;
1042     $replacements += \Drupal::moduleHandler()->invokeAll('tokens', [$type, $tokens, $data, $options, $bubbleable_metadata]);
1043   }
1044
1045   // If the token type specifics a 'needs-data' value, and the value is not
1046   // present in $data, then throw an error.
1047   if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
1048     // Only check when tests are running.
1049     $type_info = \Drupal::token()->getTypeInfo($type);
1050     if (!empty($type_info['needs-data']) && !isset($data[$type_info['needs-data']])) {
1051       trigger_error(t('Attempting to perform token replacement for token type %type without required data', ['%type' => $type]), E_USER_WARNING);
1052     }
1053   }
1054
1055   return $replacements;
1056 }
1057
1058 /**
1059  * Implements hook_token_info() on behalf of book.module.
1060  */
1061 function book_token_info() {
1062   $info['types']['book'] = [
1063     'name' => t('Book'),
1064     'description' => t('Tokens related to books.'),
1065     'needs-data' => 'book',
1066   ];
1067
1068   $info['tokens']['book']['title'] = [
1069     'name' => t('Title'),
1070     'description' => t('Title of the book.'),
1071   ];
1072   $info['tokens']['book']['author'] = [
1073     'name' => t('Author'),
1074     'description' => t('The author of the book.'),
1075     'type' => 'user',
1076   ];
1077   $info['tokens']['book']['root'] = [
1078     'name' => t('Root'),
1079     'description' => t('Top level of the book.'),
1080     'type' => 'node',
1081   ];
1082   $info['tokens']['book']['parent'] = [
1083     'name' => t('Parent'),
1084     'description' => t('Parent of the current page.'),
1085     'type' => 'node',
1086   ];
1087   $info['tokens']['book']['parents'] = [
1088     'name' => t('Parents'),
1089     'description' => t("An array of all the node's parents, starting with the root."),
1090     'type' => 'array',
1091   ];
1092
1093   $info['tokens']['node']['book'] = [
1094     'name' => t('Book'),
1095     'description' => t('The book page associated with the node.'),
1096     'type' => 'book',
1097   ];
1098   return $info;
1099 }
1100
1101 /**
1102  * Implements hook_tokens() on behalf of book.module.
1103  */
1104 function book_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1105   $replacements = [];
1106
1107   // Node tokens.
1108   if ($type == 'node' && !empty($data['node'])) {
1109     $book = $data['node']->book;
1110
1111     if (!empty($book['bid'])) {
1112       if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'book')) {
1113         $child_node = Node::load($book['nid']);
1114         $replacements += \Drupal::token()->generate('book', $book_tokens, ['book' => $child_node], $options, $bubbleable_metadata);
1115       }
1116     }
1117   }
1118   // Book tokens.
1119   elseif ($type == 'book' && !empty($data['book'])) {
1120     $book = $data['book']->book;
1121
1122     if (!empty($book['bid'])) {
1123       $book_node = Node::load($book['bid']);
1124
1125       foreach ($tokens as $name => $original) {
1126         switch ($name) {
1127           case 'root':
1128           case 'title':
1129             $replacements[$original] = $book_node->getTitle();
1130             break;
1131
1132           case 'parent':
1133             if (!empty($book['pid'])) {
1134               $parent_node = Node::load($book['pid']);
1135               $replacements[$original] = $parent_node->getTitle();
1136             }
1137             break;
1138
1139           case 'parents':
1140             if ($parents = token_book_load_all_parents($book)) {
1141               $replacements[$original] = token_render_array($parents, $options);
1142             }
1143             break;
1144         }
1145       }
1146
1147       if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'author')) {
1148         $replacements += \Drupal::token()->generate('user', $book_tokens, ['user' => $book_node->getOwner()], $options, $bubbleable_metadata);
1149       }
1150       if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) {
1151         $replacements += \Drupal::token()->generate('node', $book_tokens, ['node' => $book_node], $options, $bubbleable_metadata);
1152       }
1153       if (!empty($book['pid']) && $book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) {
1154         $parent_node = Node::load($book['pid']);
1155         $replacements += \Drupal::token()->generate('node', $book_tokens, ['node' => $parent_node], $options, $bubbleable_metadata);
1156       }
1157       if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) {
1158         $parents = token_book_load_all_parents($book);
1159         $replacements += \Drupal::token()->generate('array', $book_tokens, ['array' => $parents], $options, $bubbleable_metadata);
1160       }
1161     }
1162   }
1163
1164   return $replacements;
1165 }
1166
1167 /**
1168  * Implements hook_token_info() on behalf of menu_ui.module.
1169  */
1170 function menu_ui_token_info() {
1171   // Menu tokens.
1172   $info['types']['menu'] = [
1173     'name' => t('Menus'),
1174     'description' => t('Tokens related to menus.'),
1175     'needs-data' => 'menu',
1176   ];
1177   $info['tokens']['menu']['name'] = [
1178     'name' => t('Name'),
1179     'description' => t("The name of the menu."),
1180   ];
1181   $info['tokens']['menu']['machine-name'] = [
1182     'name' => t('Machine-readable name'),
1183     'description' => t("The unique machine-readable name of the menu."),
1184   ];
1185   $info['tokens']['menu']['description'] = [
1186     'name' => t('Description'),
1187     'description' => t('The optional description of the menu.'),
1188   ];
1189   $info['tokens']['menu']['menu-link-count'] = [
1190     'name' => t('Menu link count'),
1191     'description' => t('The number of menu links belonging to the menu.'),
1192   ];
1193   $info['tokens']['menu']['edit-url'] = [
1194     'name' => t('Edit URL'),
1195     'description' => t("The URL of the menu's edit page."),
1196   ];
1197
1198   $info['tokens']['menu-link']['menu'] = [
1199     'name' => t('Menu'),
1200     'description' => t('The menu of the menu link.'),
1201     'type' => 'menu',
1202   ];
1203   $info['tokens']['menu-link']['edit-url'] = [
1204     'name' => t('Edit URL'),
1205     'description' => t("The URL of the menu link's edit page."),
1206   ];
1207   $info['tokens']['node']['menu-link'] = [
1208     'name' => t('Menu link'),
1209     'description' => t("The menu link for this node."),
1210     'type' => 'menu-link',
1211   ];
1212
1213   return $info;
1214 }
1215
1216 /**
1217  * Implements hook_tokens() on behalf of menu_ui.module.
1218  */
1219 function menu_ui_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1220   $replacements = [];
1221
1222   /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
1223   $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
1224
1225   $url_options = ['absolute' => TRUE];
1226   if (isset($options['langcode'])) {
1227     $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
1228     $langcode = $options['langcode'];
1229   }
1230   else {
1231     $langcode = NULL;
1232   }
1233
1234   // Node tokens.
1235   if ($type == 'node' && !empty($data['node'])) {
1236     /** @var \Drupal\node\NodeInterface $node */
1237     $node = $data['node'];
1238
1239     foreach ($tokens as $name => $original) {
1240       switch ($name) {
1241         case 'menu-link':
1242           // On node-form save we populate a calculated field with a menu_link
1243           // references.
1244           // @see token_node_menu_link_submit()
1245           if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) {
1246             /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
1247             $replacements[$original] = $menu_link->getTitle();
1248           }
1249           else {
1250             $url = $node->toUrl();
1251             if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) {
1252               $link = _token_menu_link_best_match($node, $links);
1253               $replacements[$original] = token_menu_link_translated_title($link, $langcode);
1254             }
1255           }
1256           break;
1257       }
1258
1259       // Chained token relationships.
1260       if ($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu-link')) {
1261         if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) {
1262           /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */
1263           $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, ['menu-link' => $menu_link], $options, $bubbleable_metadata);
1264         }
1265         else {
1266           $url = $node->toUrl();
1267           if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) {
1268             $link = _token_menu_link_best_match($node, $links);
1269             $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, ['menu-link' => $link], $options, $bubbleable_metadata);
1270           }
1271         }
1272       }
1273     }
1274   }
1275
1276   // Menu link tokens.
1277   if ($type == 'menu-link' && !empty($data['menu-link'])) {
1278     /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
1279     $link = $data['menu-link'];
1280
1281     if ($link instanceof MenuLinkContentInterface) {
1282       $link = $menu_link_manager->createInstance($link->getPluginId());
1283     }
1284
1285     foreach ($tokens as $name => $original) {
1286       switch ($name) {
1287         case 'menu':
1288           if ($menu = Menu::load($link->getMenuName())) {
1289             $replacements[$original] = $menu->label();
1290           }
1291           break;
1292
1293         case 'edit-url':
1294           $replacements[$original] = $link->getEditRoute()->setOptions($url_options)->toString();
1295           break;
1296       }
1297     }
1298
1299     // Chained token relationships.
1300     if (($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu')) && $menu = Menu::load($link->getMenuName())) {
1301       $replacements += \Drupal::token()->generate('menu', $menu_tokens, ['menu' => $menu], $options, $bubbleable_metadata);
1302     }
1303   }
1304
1305   // Menu tokens.
1306   if ($type == 'menu' && !empty($data['menu'])) {
1307     /** @var \Drupal\system\MenuInterface $menu */
1308     $menu = $data['menu'];
1309
1310     foreach ($tokens as $name => $original) {
1311       switch ($name) {
1312         case 'name':
1313           $replacements[$original] = $menu->label();
1314           break;
1315
1316         case 'machine-name':
1317           $replacements[$original] = $menu->id();
1318           break;
1319
1320         case 'description':
1321           $replacements[$original] = $menu->getDescription();
1322           break;
1323
1324         case 'menu-link-count':
1325           $replacements[$original] = $menu_link_manager->countMenuLinks($menu->id());
1326           break;
1327
1328         case 'edit-url':
1329           $replacements[$original] = Url::fromRoute('entity.menu.edit_form', ['menu' => $menu->id()], $url_options)->toString();
1330           break;
1331       }
1332     }
1333   }
1334
1335   return $replacements;
1336 }
1337
1338 /**
1339  * Returns a best matched link for a given node.
1340  *
1341  * If the url exists in multiple menus, default to the one set on the node
1342  * itself.
1343  *
1344  * @param \Drupal\node\NodeInterface $node
1345  *   The node to look up the default menu settings from.
1346  * @param array $links
1347  *   An array of instances keyed by plugin ID.
1348  *
1349  * @return \Drupal\Core\Menu\MenuLinkInterface
1350  *   A Link instance.
1351  */
1352 function _token_menu_link_best_match(NodeInterface $node, array $links) {
1353   // Get the menu ui defaults so we can determine what menu was
1354   // selected for this node. This ensures that if the node was added
1355   // to the menu via the node UI, we use that as a default. If it
1356   // was not added via the node UI then grab the first in the
1357   // retrieved array.
1358   $defaults = menu_ui_get_menu_link_defaults($node);
1359   if (isset($defaults['id']) && isset($links[$defaults['id']])) {
1360     $link = $links[$defaults['id']];
1361   }
1362   else {
1363     $link = reset($links);
1364   }
1365   return $link;
1366 }
1367
1368 /**
1369  * Implements hook_token_info_alter() on behalf of field.module.
1370  *
1371  * We use hook_token_info_alter() rather than hook_token_info() as other
1372  * modules may already have defined some field tokens.
1373  */
1374 function field_token_info_alter(&$info) {
1375   $type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
1376
1377   // Attach field tokens to their respecitve entity tokens.
1378   foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
1379     if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) {
1380       continue;
1381     }
1382
1383     // Make sure a token type exists for this entity.
1384     $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id);
1385     if (empty($token_type) || !isset($info['types'][$token_type])) {
1386       continue;
1387     }
1388
1389     $fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id);
1390     foreach ($fields as $field_name => $field) {
1391       /** @var \Drupal\field\FieldStorageConfigInterface $field */
1392
1393       // Ensure the token implements FieldStorageConfigInterface or is defined
1394       // in token module.
1395       $provider = '';
1396       if (isset($info['types'][$token_type]['module'])) {
1397         $provider = $info['types'][$token_type]['module'];
1398       }
1399       if (!($field instanceof FieldStorageConfigInterface) && $provider != 'token') {
1400         continue;
1401       }
1402
1403       // If a token already exists for this field, then don't add it.
1404       if (isset($info['tokens'][$token_type][$field_name])) {
1405         continue;
1406       }
1407
1408       if ($token_type == 'comment' && $field_name == 'comment_body') {
1409         // Core provides the comment field as [comment:body].
1410         continue;
1411       }
1412
1413       // Do not define the token type if the field has no properties.
1414       if (!$field->getPropertyDefinitions()) {
1415         continue;
1416       }
1417
1418       // Generate a description for the token.
1419       $labels = _token_field_label($entity_type_id, $field_name);
1420       $label = array_shift($labels);
1421       $params['@type'] = $type_info[$field->getType()]['label'];
1422       if (!empty($labels)) {
1423         $params['%labels'] = implode(', ', $labels);
1424         $description = t('@type field. Also known as %labels.', $params);
1425       }
1426       else {
1427         $description = t('@type field.', $params);
1428       }
1429
1430       $cardinality = $field->getCardinality();
1431       $cardinality = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $cardinality > 3) ? 3 : $cardinality;
1432       $field_token_name = $token_type . '-' . $field_name;
1433       $info['tokens'][$token_type][$field_name] = [
1434         'name' => Html::escape($label),
1435         'description' => $description,
1436         'module' => 'token',
1437         // For multivalue fields the field token is a list type.
1438         'type' => $cardinality > 1 ? "list<$field_token_name>" : $field_token_name,
1439       ];
1440
1441       // Field token type.
1442       $info['types'][$field_token_name] = [
1443         'name' => Html::escape($label),
1444         'description' => t('@label tokens.', ['@label' => Html::escape($label)]),
1445         'needs-data' => $field_token_name,
1446         'nested' => TRUE,
1447       ];
1448       // Field list token type.
1449       if ($cardinality > 1) {
1450         $info['types']["list<$field_token_name>"] = [
1451           'name' => t('List of @type values', ['@type' => Html::escape($label)]),
1452           'description' => t('Tokens for lists of @type values.', ['@type' => Html::escape($label)]),
1453           'needs-data' => "list<$field_token_name>",
1454           'nested' => TRUE,
1455         ];
1456       }
1457
1458       // Show a different token for each field delta.
1459       if ($cardinality > 1) {
1460         for ($delta = 0; $delta < $cardinality; $delta++) {
1461           $info['tokens']["list<$field_token_name>"][$delta] = [
1462             'name' => t('@type type with delta @delta', ['@type' => Html::escape($label), '@delta' => $delta]),
1463             'module' => 'token',
1464             'type' => $field_token_name,
1465           ];
1466         }
1467       }
1468
1469       // Property tokens.
1470       foreach ($field->getPropertyDefinitions() as $property => $property_definition) {
1471         if (is_subclass_of($property_definition->getClass(), 'Drupal\Core\TypedData\PrimitiveInterface')) {
1472           $info['tokens'][$field_token_name][$property] = [
1473             'name' => $property_definition->getLabel(),
1474             'description' => $property_definition->getDescription(),
1475             'module' => 'token',
1476           ];
1477         }
1478         elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) {
1479           $referenced_entity_type = $property_definition->getTargetDefinition()->getEntityTypeId();
1480           $referenced_token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_entity_type);
1481           $info['tokens'][$field_token_name][$property] = [
1482             'name' => $property_definition->getLabel(),
1483             'description' => $property_definition->getDescription(),
1484             'module' => 'token',
1485             'type' => $referenced_token_type,
1486           ];
1487         }
1488       }
1489       // Provide image_with_image_style tokens for image fields.
1490       if ($field->getType() == 'image') {
1491         $image_styles = image_style_options(FALSE);
1492         foreach ($image_styles as $style => $description) {
1493           $info['tokens'][$field_token_name][$style] = [
1494             'name' => $description,
1495             'description' => t('Represents the image in the given image style.'),
1496             'type' => 'image_with_image_style',
1497           ];
1498         }
1499       }
1500       // Provide format token for datetime fields.
1501       if ($field->getType() == 'datetime') {
1502         $info['tokens'][$field_token_name]['date'] = $info['tokens'][$field_token_name]['value'];
1503         $info['tokens'][$field_token_name]['date']['name'] .= ' ' . t('format');
1504         $info['tokens'][$field_token_name]['date']['type'] = 'date';
1505       }
1506       if ($field->getType() == 'daterange' || $field->getType() == 'date_recur') {
1507         $info['tokens'][$field_token_name]['start_date'] = $info['tokens'][$field_token_name]['value'];
1508         $info['tokens'][$field_token_name]['start_date']['name'] .= ' ' . t('format');
1509         $info['tokens'][$field_token_name]['start_date']['type'] = 'date';
1510         $info['tokens'][$field_token_name]['end_date'] = $info['tokens'][$field_token_name]['end_value'];
1511         $info['tokens'][$field_token_name]['end_date']['name'] .= ' ' . t('format');
1512         $info['tokens'][$field_token_name]['end_date']['type'] = 'date';
1513       }
1514     }
1515   }
1516 }
1517
1518 /**
1519  * Returns the label of a certain field.
1520  *
1521  * Therefore it looks up in all bundles to find the most used instance.
1522  *
1523  * Based on views_entity_field_label().
1524  *
1525  * @todo Resync this method with views_entity_field_label().
1526  */
1527 function _token_field_label($entity_type, $field_name) {
1528   $labels = [];
1529   // Count the amount of instances per label per field.
1530   foreach (array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type)) as $bundle) {
1531     $bundle_instances = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle);
1532     if (isset($bundle_instances[$field_name])) {
1533       $instance = $bundle_instances[$field_name];
1534       $label = (string) $instance->getLabel();
1535       $labels[$label] = isset($labels[$label]) ? ++$labels[$label] : 1;
1536     }
1537   }
1538
1539   if (empty($labels)) {
1540     return [$field_name];
1541   }
1542
1543   // Sort the field labels by it most used label and return the labels.
1544   arsort($labels);
1545   return array_keys($labels);
1546 }
1547
1548 /**
1549  * Implements hook_tokens() on behalf of field.module.
1550  */
1551 function field_tokens($type, $tokens, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata) {
1552   $replacements = [];
1553   $langcode = isset($options['langcode']) ? $options['langcode'] : NULL;
1554   // Entity tokens.
1555   if ($type == 'entity' && !empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) {
1556     /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
1557     $entity = $data['entity'];
1558     if (!($entity instanceof ContentEntityInterface)) {
1559       return $replacements;
1560     }
1561
1562     if (!isset($options['langcode'])) {
1563       // Set the active language in $options, so that it is passed along.
1564       $langcode = $options['langcode'] = $entity->language()->getId();
1565     }
1566     // Obtain the entity with the correct language.
1567     $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
1568
1569     $view_mode_name = $entity->getEntityTypeId() . '.' . $entity->bundle() . '.token';
1570     $view_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load($view_mode_name);
1571     $token_view_display = (!empty($view_display) && $view_display->status());
1572     foreach ($tokens as $name => $original) {
1573       // For the [entity:field_name] token.
1574       if (strpos($name, ':') === FALSE) {
1575         $field_name = $name;
1576         $token_name = $name;
1577       }
1578       // For [entity:field_name:0], [entity:field_name:0:value] and
1579       // [entity:field_name:value] tokens.
1580       else {
1581         list($field_name, $delta) = explode(':', $name, 2);
1582         if (!is_numeric($delta)) {
1583           unset($delta);
1584         }
1585         $token_name = $field_name;
1586       }
1587       // Ensure the entity has the requested field and that the token for it is
1588       // defined by token.module.
1589       if (!$entity->hasField($field_name) || _token_module($data['token_type'], $token_name) != 'token') {
1590         continue;
1591       }
1592
1593       $display_options = 'token';
1594       // Do not continue if the field is empty.
1595       if ($entity->get($field_name)->isEmpty()) {
1596         continue;
1597       }
1598       // Handle [entity:field_name] and [entity:field_name:0] tokens.
1599       if ($field_name === $name || isset($delta)) {
1600         if (!$token_view_display) {
1601           // We don't have the token view display and should fall back on
1602           // default formatters. If the field has specified a specific formatter
1603           // to be used by default with tokens, use that, otherwise use the
1604           // default formatter.
1605           /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
1606           $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
1607           $field_type_definition = $field_type_manager->getDefinition($entity->getFieldDefinition($field_name)->getType());
1608           $display_options = [
1609             'type' => !empty($field_type_definition['default_token_formatter']) ? $field_type_definition['default_token_formatter'] : $field_type_definition['default_formatter'],
1610             'label' => 'hidden',
1611           ];
1612         }
1613
1614         // Render only one delta.
1615         if (isset($delta)) {
1616           if ($field_delta = $entity->{$field_name}[$delta]) {
1617             $field_output = $field_delta->view($display_options);
1618           }
1619           // If no such delta exists, let's not replace the token.
1620           else {
1621             continue;
1622           }
1623         }
1624         // Render the whole field (with all deltas).
1625         else {
1626           $field_output = $entity->$field_name->view($display_options);
1627           // If we are displaying all field items we need this #pre_render
1628           // callback.
1629           $field_output['#pre_render'][] = 'token_pre_render_field_token';
1630         }
1631         $field_output['#token_options'] = $options;
1632         $replacements[$original] = \Drupal::service('renderer')->renderPlain($field_output);
1633       }
1634       // Handle [entity:field_name:value] and [entity:field_name:0:value]
1635       // tokens.
1636       elseif ($field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name)) {
1637         $property_token_data = [
1638           'field_property' => TRUE,
1639           $data['entity_type'] . '-' . $field_name => $entity->$field_name,
1640           'field_name' => $data['entity_type'] . '-' . $field_name,
1641         ];
1642         $replacements += \Drupal::token()->generate($field_name, $field_tokens, $property_token_data, $options, $bubbleable_metadata);
1643       }
1644     }
1645
1646     // Remove the cloned object from memory.
1647     unset($entity);
1648   }
1649   elseif (!empty($data['field_property'])) {
1650     foreach ($tokens as $token => $original) {
1651       $filtered_tokens = $tokens;
1652       $delta = 0;
1653       $parts = explode(':', $token);
1654       if (is_numeric($parts[0])) {
1655         if (count($parts) > 1) {
1656           $delta = $parts[0];
1657           $property_name = $parts[1];
1658           // Pre-filter the tokens to select those with the correct delta.
1659           $filtered_tokens = \Drupal::token()->findWithPrefix($tokens, $delta);
1660           // Remove the delta to unify between having and not having one.
1661           array_shift($parts);
1662         }
1663         else {
1664           // Token is fieldname:delta, which is invalid.
1665           continue;
1666         }
1667       }
1668       else {
1669         $property_name = $parts[0];
1670       }
1671
1672       if (isset($data[$data['field_name']][$delta])) {
1673         $field_item = $data[$data['field_name']][$delta];
1674       }
1675       else {
1676         // The field has no such delta, abort replacement.
1677         continue;
1678       }
1679
1680       if (isset($field_item->$property_name) && ($field_item->$property_name instanceof FieldableEntityInterface)) {
1681         // Entity reference field.
1682         $entity = $field_item->$property_name;
1683         // Obtain the referenced entity with the correct language.
1684         $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
1685
1686         if (count($parts) > 1) {
1687           $field_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name);
1688           $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity->getEntityTypeId(), TRUE);
1689           $replacements += \Drupal::token()->generate($token_type, $field_tokens, [$token_type => $entity], $options, $bubbleable_metadata);
1690         }
1691         else {
1692           $replacements[$original] = $entity->label();
1693         }
1694       }
1695       elseif (($field_item->getFieldDefinition()->getType() == 'image') && ($style = ImageStyle::load($property_name))) {
1696         // Handle [node:field_name:image_style:property] tokens and multivalued
1697         // [node:field_name:delta:image_style:property] tokens. If the token is
1698         // of the form [node:field_name:image_style], provide the URL as a
1699         // replacement.
1700         $property_name = isset($parts[1]) ? $parts[1] : 'url';
1701         $entity = $field_item->entity;
1702         if (!empty($field_item->entity)) {
1703           $original_uri = $entity->getFileUri();
1704
1705           // Only generate the image derivative if needed.
1706           if ($property_name === 'width' || $property_name === 'height') {
1707             $dimensions = [
1708               'width' => $field_item->width,
1709               'height' => $field_item->height,
1710             ];
1711             $style->transformDimensions($dimensions, $original_uri);
1712             $replacements[$original] = $dimensions[$property_name];
1713           }
1714           elseif ($property_name === 'uri') {
1715             $replacements[$original] = $style->buildUri($original_uri);
1716           }
1717           elseif ($property_name === 'url') {
1718             $replacements[$original] = $style->buildUrl($original_uri);
1719           }
1720           else {
1721             // Generate the image derivative, if it doesn't already exist.
1722             $derivative_uri = $style->buildUri($original_uri);
1723             $derivative_exists = TRUE;
1724             if (!file_exists($derivative_uri)) {
1725               $derivative_exists = $style->createDerivative($original_uri, $derivative_uri);
1726             }
1727             if ($derivative_exists) {
1728               $image = \Drupal::service('image.factory')->get($derivative_uri);
1729               // Provide the replacement.
1730               switch ($property_name) {
1731                 case 'mimetype':
1732                   $replacements[$original] = $image->getMimeType();
1733                   break;
1734
1735                 case 'filesize':
1736                   $replacements[$original] = $image->getFileSize();
1737                   break;
1738               }
1739             }
1740           }
1741         }
1742       }
1743       elseif (in_array($field_item->getFieldDefinition()->getType(), ['datetime', 'daterange', 'date_recur']) && in_array($property_name, ['date', 'start_date', 'end_date']) && !empty($field_item->$property_name)) {
1744         $timestamp = $field_item->{$property_name}->getTimestamp();
1745         // If the token is an exact match for the property or the delta and the
1746         // property, use the timestamp as-is.
1747         if ($property_name == $token || "$delta:$property_name" == $token) {
1748           $replacements[$original] = $timestamp;
1749         }
1750         else {
1751           $date_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name);
1752           $replacements += \Drupal::token()->generate('date', $date_tokens, ['date' => $timestamp], $options, $bubbleable_metadata);
1753         }
1754       }
1755       else {
1756         $replacements[$original] = $field_item->$property_name;
1757       }
1758     }
1759   }
1760   return $replacements;
1761 }
1762
1763 /**
1764  * Pre-render callback for field output used with tokens.
1765  */
1766 function token_pre_render_field_token($elements) {
1767   // Remove the field theme hook, attachments, and JavaScript states.
1768   unset($elements['#theme']);
1769   unset($elements['#states']);
1770   unset($elements['#attached']);
1771
1772   // Prevent multi-value fields from appearing smooshed together by appending
1773   // a join suffix to all but the last value.
1774   $deltas = Element::getVisibleChildren($elements);
1775   $count = count($deltas);
1776   if ($count > 1) {
1777     $join = isset($elements['#token_options']['join']) ? $elements['#token_options']['join'] : ", ";
1778     foreach ($deltas as $index => $delta) {
1779       // Do not add a suffix to the last item.
1780       if ($index < ($count - 1)) {
1781         $elements[$delta] += ['#suffix' => $join];
1782       }
1783     }
1784   }
1785   return $elements;
1786 }