clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Clears cache for tokens.
*/
class TokenCacheController extends ControllerBase {
/**
* Clear caches and redirect back to the frontpage.
*/
public function flush() {
token_clear_cache();
$this->messenger()->addMessage($this->t('Token registry caches cleared.'));
return $this->redirect('<front>');
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\token\TokenEntityMapperInterface;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Devel integration for tokens.
*/
class TokenDevelController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
public function __construct(TreeBuilderInterface $tree_builder, TokenEntityMapperInterface $entity_mapper) {
$this->treeBuilder = $tree_builder;
$this->entityMapper = $entity_mapper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder'),
$container->get('token.entity_mapper')
);
}
/**
* Prints the loaded structure of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityTokens(RouteMatchInterface $route_match) {
$output = [];
$parameter_name = $route_match->getRouteObject()->getOption('_token_entity_type_id');
$entity = $route_match->getParameter($parameter_name);
if ($entity && $entity instanceof EntityInterface) {
$output = $this->renderTokenTree($entity);
}
return $output;
}
/**
* Render the token tree for the specified entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which the token tree should be rendered.
*
* @return array
* Render array of the token tree for the $entity.
*
* @see static::entityLoad
*/
protected function renderTokenTree(EntityInterface $entity) {
$this->moduleHandler()->loadInclude('token', 'pages.inc');
$entity_type = $entity->getEntityTypeId();
$token_type = $this->entityMapper->getTokenTypeForEntityType($entity_type);
$options = [
'flat' => TRUE,
'values' => TRUE,
'data' => [$token_type => $entity],
];
$token_tree = [
$token_type => [
'tokens' => $this->treeBuilder->buildTree($token_type, $options),
],
];
// foreach ($tree as $token => $token_info) {
// if (!isset($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
// continue;
// }
// }
$build['tokens'] = [
'#type' => 'token_tree_table',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => TRUE,
'#token_tree' => $token_tree,
'#columns' => ['token', 'value'],
'#empty' => $this->t('No tokens available.'),
];
return $build;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns tree responses for tokens.
*/
class TokenTreeController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
public function __construct(TreeBuilderInterface $tree_builder) {
$this->treeBuilder = $tree_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder')
);
}
/**
* Page callback to output a token tree as an empty page.
*/
function outputTree(Request $request) {
$options = $request->query->has('options') ? Json::decode($request->query->get('options')) : [];
// The option token_types may only be an array OR 'all'. If it is not set,
// we assume that only global token types are requested.
$token_types = !empty($options['token_types']) ? $options['token_types'] : [];
if ($token_types == 'all') {
$build = $this->treeBuilder->buildAllRenderable($options);
}
else {
$build = $this->treeBuilder->buildRenderable($token_types, $options);
}
$build['#cache']['contexts'][] = 'url.query_args:options';
$build['#title'] = $this->t('Available tokens');
// If this is an AJAX/modal request, add a wrapping div to the contents so
// that Drupal.behaviors.tokenTree and Drupal.behaviors.tokenAttach can
// still find the elements they need to.
// @see https://www.drupal.org/project/token/issues/2994671
// @see https://www.drupal.org/node/2940704
// @see http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/
if ($request->isXmlHttpRequest()) {
$build['#prefix'] = '<div>';
$build['#suffix'] = '</div>';
}
return $build;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\token\Drush\Commands;
use Drupal\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drush\Commands\DrushCommands;
/**
* TokenCommands provides the Drush hook implementation for cache clears.
*/
class TokenCommands extends DrushCommands {
/**
* The module_handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* TokenCommands constructor.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module_handler service.
*/
public function __construct(ModuleHandlerInterface $moduleHandler) {
$this->moduleHandler = $moduleHandler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new static(
$container->get('module_handler')
);
}
/**
* Adds a cache clear option for tokens.
*
* @param array $types
* The Drush clear types to make available.
* @param bool $includeBootstrappedTypes
* Whether to include types only available in a bootstrapped Drupal or not.
*
* @hook on-event cache-clear
*/
public function cacheClear(array &$types, $includeBootstrappedTypes) {
if (!$includeBootstrappedTypes || !$this->moduleHandler->moduleExists('token')) {
return;
}
$types['token'] = 'token_clear_cache';
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Drupal\token\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\Table;
/**
* Provides a render element for a token tree table.
*
* @RenderElement("token_tree_table")
*/
class TokenTreeTable extends Table {
protected static $cssFilter = [' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '', ':' => '--', '?' => '', '<' => '-', '>' => '-'];
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#header' => [],
'#rows' => [],
'#token_tree' => [],
'#columns' => ['name', 'token', 'description'],
'#empty' => '',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => FALSE,
'#click_insert' => TRUE,
'#sticky' => FALSE,
'#responsive' => TRUE,
'#input' => FALSE,
'#pre_render' => [
[$class, 'preRenderTokenTree'],
[$class, 'preRenderTable'],
],
'#theme' => 'table__token_tree',
'#attached' => [
'library' => [
'token/token',
],
],
];
}
/**
* Pre-render the token tree to transform rows in the token tree.
*
* @param array $element
*
* @return array
* The processed element.
*/
public static function preRenderTokenTree($element) {
$multiple_token_types = count($element['#token_tree']) > 1;
foreach ($element['#token_tree'] as $token_type => $type_info) {
// Do not show nested tokens.
if (!empty($type_info['nested']) && empty($element['#show_nested'])) {
continue;
}
if ($multiple_token_types) {
$row = static::formatRow($token_type, $type_info, $element['#columns'], TRUE);
$element['#rows'][] = $row;
}
foreach ($type_info['tokens'] as $token => $token_info) {
if (!empty($token_info['restricted']) && empty($element['#show_restricted'])) {
continue;
}
if ($element['#skip_empty_values'] && empty($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
continue;
}
if ($multiple_token_types && !isset($token_info['parent'])) {
$token_info['parent'] = $token_type;
}
$row = static::formatRow($token, $token_info, $element['#columns']);
$element['#rows'][] = $row;
}
}
// Fill headers if one is not specified.
if (empty($element['#header'])) {
$column_map = [
'name' => t('Name'),
'token' => t('Token'),
'value' => t('Value'),
'description' => t('Description'),
];
foreach ($element['#columns'] as $col) {
$element['#header'][] = $column_map[$col];
}
}
$element['#attributes']['class'][] = 'token-tree';
if ($element['#click_insert']) {
$element['#caption'] = t('Click a token to insert it into the field you\'ve last clicked.');
$element['#attributes']['class'][] = 'token-click-insert';
}
return $element;
}
protected static function cleanCssIdentifier($id) {
return 'token-' . Html::cleanCssIdentifier(trim($id, '[]'), static::$cssFilter);
}
protected static function formatRow($token, $token_info, $columns, $is_group = FALSE) {
$row = [
'id' => static::cleanCssIdentifier($token),
'data-tt-id' => static::cleanCssIdentifier($token),
'class' => [],
'data' => [],
];
foreach ($columns as $col) {
switch ($col) {
case 'name':
$row['data'][$col] = $token_info['name'];
break;
case 'token':
$row['data'][$col]['data'] = $token;
$row['data'][$col]['class'][] = 'token-key';
break;
case 'description':
$row['data'][$col] = isset($token_info['description']) ? $token_info['description'] : '';
break;
case 'value':
$row['data'][$col] = !$is_group && isset($token_info['value']) ? $token_info['value'] : '';
break;
}
}
if ($is_group) {
// This is a token type/group.
$row['class'][] = 'token-group';
}
elseif (!empty($token_info['parent'])) {
$row['data-tt-parent-id'] = static::cleanCssIdentifier($token_info['parent']);
}
return $row;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\token;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* Defines a menu link list class for storing menu link information.
*
* @see token_entity_base_field_info()
*/
class MenuLinkFieldItemList extends EntityReferenceFieldItemList {
use ComputedItemListTrait;
/**
* {@inheritdoc}
*/
protected function computeValue() {
// This field does not really compute anything, it is used to store
// the referenced menu link.
return NULL;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\token\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasLinkTemplate('token-devel')) {
$this->derivatives["$entity_type_id.token_devel_tab"] = [
'route_name' => "entity.$entity_type_id.token_devel",
'weight' => 110,
'title' => $this->t('Tokens'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
];
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\token\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Devel routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($devel_render = $entity_type->getLinkTemplate('token-devel')) {
$options = [
'_admin_route' => TRUE,
'_token_entity_type_id' => $entity_type_id,
'parameters' => [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
],
];
$route = new Route(
$devel_render,
[
'_controller' => '\Drupal\token\Controller\TokenDevelController::entityTokens',
'_title' => 'Devel Tokens',
],
[
'_permission' => 'access devel information',
'_module_dependencies' => 'devel',
],
$options
);
$collection->add("entity.$entity_type_id.token_devel", $route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::ALTER] = array('onAlterRoutes', 100);
return $events;
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Utility\Token as TokenBase;
/**
* Service to retrieve token information.
*
* This service replaces the core's token service and provides the same
* functionality by extending it. It also provides additional functionality
* commonly required by the additional support provided by token module and
* other modules.
*/
class Token extends TokenBase implements TokenInterface {
/**
* Token definitions.
*
* @var array[]|null
* An array of token definitions, or NULL when the definitions are not set.
*
* @see self::resetInfo()
*/
protected $globalTokenTypes;
/**
* {@inheritdoc}
*/
public function getInfo() {
if (empty($this->tokenInfo)) {
$cache_id = 'token_info_sorted:' . $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$cache = $this->cache->get($cache_id);
if ($cache) {
$this->tokenInfo = $cache->data;
}
else {
$token_info = $this->moduleHandler->invokeAll('token_info');
$this->moduleHandler->alter('token_info', $token_info);
foreach (array_keys($token_info['types']) as $type_key) {
if (isset($token_info['types'][$type_key]['type'])) {
$base_type = $token_info['types'][$type_key]['type'];
// If this token type extends another token type, then merge in
// the base token type's tokens.
if (isset($token_info['tokens'][$base_type])) {
$token_info['tokens'] += [$type_key => []];
$token_info['tokens'][$type_key] += $token_info['tokens'][$base_type];
}
}
else {
// Add a 'type' value to each token type information.
$token_info['types'][$type_key]['type'] = $type_key;
}
}
// Pre-sort tokens.
$by_name = $this->prepareMultisort($token_info['types']);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['types']);
foreach (array_keys($token_info['tokens']) as $type) {
$by_name = $this->prepareMultisort($token_info['tokens'][$type]);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['tokens'][$type]);
}
$this->tokenInfo = $token_info;
$this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, [
static::TOKEN_INFO_CACHE_TAG,
]);
}
}
return $this->tokenInfo;
}
/**
* Extracts data from the token data for use in array_multisort().
*
* @param array $token_info
* List of tokens or token types, each element must have a name key.
*
* @return string[]
* List of the names keyed by the token key.
*/
protected function prepareMultisort($token_info) {
$by_name = [];
foreach ($token_info as $key => $token_info_element) {
// Check if the "name" key exists before accessing it.
$by_name[$key] = $token_info_element['name'] ?? NULL;
}
return $by_name;
}
/**
* {@inheritdoc}
*/
public function getTokenInfo($token_type, $token) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['tokens'][$token_type][$token]) ? $this->tokenInfo['tokens'][$token_type][$token] : NULL;
}
/**
* {@inheritdoc}
*/
public function getTypeInfo($token_type) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['types'][$token_type]) ? $this->tokenInfo['types'][$token_type] : NULL;
}
/**
* {@inheritdoc}
*/
public function getGlobalTokenTypes() {
if (empty($this->globalTokenTypes)) {
$token_info = $this->getInfo();
foreach ($token_info['types'] as $type => $type_info) {
// If the token types has not specified that 'needs-data' => TRUE, then
// it is a global token type that will always be replaced in any context.
if (empty($type_info['needs-data'])) {
$this->globalTokenTypes[] = $type;
}
}
}
return $this->globalTokenTypes;
}
/**
* {@inheritdoc}
*/
function getInvalidTokens($type, $tokens) {
$token_info = $this->getInfo();
$invalid_tokens = [];
foreach ($tokens as $token => $full_token) {
if (isset($token_info['tokens'][$type][$token])) {
continue;
}
// Split token up if it has chains.
$parts = explode(':', $token, 2);
if (!isset($token_info['tokens'][$type][$parts[0]])) {
// This is an invalid token (not defined).
$invalid_tokens[] = $full_token;
}
elseif (count($parts) == 2) {
$sub_token_info = $token_info['tokens'][$type][$parts[0]];
if (!empty($sub_token_info['dynamic'])) {
// If this token has been flagged as a dynamic token, skip it.
continue;
}
elseif (empty($sub_token_info['type'])) {
// If the token has chains, but does not support it, it is invalid.
$invalid_tokens[] = $full_token;
}
else {
// Recursively check the chained tokens.
$sub_tokens = $this->findWithPrefix([$token => $full_token], $parts[0]);
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($sub_token_info['type'], $sub_tokens));
}
}
}
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function getInvalidTokensByContext($value, array $valid_types = []) {
if (in_array('all', $valid_types)) {
$info = $this->getInfo();
$valid_types = array_keys($info['types']);
}
else {
// Add the token types that are always valid in global context.
$valid_types = array_merge($valid_types, $this->getGlobalTokenTypes());
}
$invalid_tokens = [];
$value_tokens = is_string($value) ? $this->scan($value) : $value;
foreach ($value_tokens as $type => $tokens) {
if (!in_array($type, $valid_types)) {
// If the token type is not a valid context, its tokens are invalid.
$invalid_tokens = array_merge($invalid_tokens, array_values($tokens));
}
else {
// Check each individual token for validity.
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($type, $tokens));
}
}
array_unique($invalid_tokens);
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
parent::resetInfo();
$this->globalTokenTypes = NULL;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\token;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Service to provide mappings between entity and token types.
*
* Why do we need this? Because when the token API was moved to core we did not
* reuse the entity type as the base name for taxonomy terms and vocabulary
* tokens.
*/
class TokenEntityMapper implements TokenEntityMapperInterface {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var array
*/
protected $entityMappings;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function getEntityTypeMappings() {
if (empty($this->entityMappings)) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $info) {
$this->entityMappings[$entity_type] = $info->get('token_type') ?: $entity_type;
}
// Allow modules to alter the mapping array.
$this->moduleHandler->alter('token_entity_mapping', $this->entityMappings);
}
return $this->entityMappings;
}
/**
* {@inheritdoc}
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
$return = array_search($token_type, $this->entityMappings);
return $return !== FALSE ? $return : ($fallback ? $token_type : FALSE);
}
/**
* {@inheritdoc}
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
return isset($this->entityMappings[$entity_type]) ? $this->entityMappings[$entity_type] : ($fallback ? $entity_type : FALSE);
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
$this->entityMappings = NULL;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\token;
interface TokenEntityMapperInterface {
/**
* Return an array of entity type to token type mappings.
*
* @return array
* An array of mappings with entity type mapping to token type.
*/
public function getEntityTypeMappings();
/**
* Return the entity type of a particular token type.
*
* @param string $token_type
* The token type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The entity type of the token type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE);
/**
* Return the token type of a particular entity type.
*
* @param string $entity_type
* The entity type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The token type of the entity type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE);
/**
* Resets metadata describing token and entity mappings.
*/
public function resetInfo();
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\token;
use Drupal\Core\Render\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
class TokenFieldRender implements TrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRender'];
}
/**
* Pre-render callback for field output used with tokens.
*/
public static function preRender($elements) {
// Remove the field theme hook, attachments, and JavaScript states.
unset($elements['#theme']);
unset($elements['#states']);
unset($elements['#attached']);
// Prevent multi-value fields from appearing smooshed together by appending
// a join suffix to all but the last value.
$deltas = Element::getVisibleChildren($elements);
$count = count($deltas);
if ($count > 1) {
$join = isset($elements['#token_options']['join']) ? $elements['#token_options']['join'] : ", ";
foreach ($deltas as $index => $delta) {
// Do not add a suffix to the last item.
if ($index < ($count - 1)) {
$elements[$delta] += ['#suffix' => $join];
}
}
}
return $elements;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\token;
interface TokenInterface {
/**
* Returns metadata describing supported token types.
*
* @param $token_type
* The token type for which the metadata is required.
*
* @return array[]
* An array of token type information from hook_token_info() for the
* specified token type.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*/
public function getTypeInfo($token_type);
/**
* Returns metadata describing supported a token.
*
* @param $token_type
* The token type for which the metadata is required.
* @param $token
* The token name for which the metadata is required.
*
* @return array[]
* An array of information from hook_token_info() for the specified token.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*/
public function getTokenInfo($token_type, $token);
/**
* Get a list of token types that can be used without any context (global).
*
* @return array[]
* An array of global token types.
*/
public function getGlobalTokenTypes();
/**
* Validate an array of tokens based on their token type.
*
* @param string $type
* The type of tokens to validate (e.g. 'node', etc.)
* @param string[] $tokens
* A keyed array of tokens, and their original raw form in the source text.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
function getInvalidTokens($type, $tokens);
/**
* Validate tokens in raw text based on possible contexts.
*
* @param string|string[] $value
* A string with the raw text containing the raw tokens, or an array of
* tokens from token_scan().
* @param string[] $valid_types
* An array of token types that will be used when token replacement is
* performed.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
public function getInvalidTokensByContext($value, array $valid_types = []);
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Lock\LockBackendInterface;
/**
* Token module provider.
*/
class TokenModuleProvider extends CacheCollector {
/**
* Separator for the cache key.
*
* @internal
*/
const SEPARATOR = '::';
/**
* The token service.
*
* @var \Drupal\token\TokenInterface
*/
protected TokenInterface $token;
/**
* {@inheritdoc}
*/
public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, TokenInterface $token) {
parent::__construct('token_module', $cache, $lock, [Token::TOKEN_INFO_CACHE_TAG]);
$this->token = $token;
}
/**
* Return the module responsible for a token.
*
* @param string $type
* The token type.
* @param string $name
* The token name.
*
* @return string|null
* The value of $info['tokens'][$type][$name]['module'] from token info, or
* NULL if the value does not exist.
*/
public function getTokenModule(string $type, string $name): ?string {
return $this->get($type . static::SEPARATOR . $name);
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($key) {
[$type, $name] = explode(static::SEPARATOR, $key, 2);
$token_info = $this->token->getTokenInfo($type, $name);
$this->storage[$key] = $token_info['module'] ?? NULL;
$this->persist($key);
return $this->storage[$key];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\token;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Replace core's token service with our own.
*/
class TokenServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$definition = $container->getDefinition('token');
$definition->setClass('\Drupal\token\Token');
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
class TreeBuilder implements TreeBuilderInterface {
use StringTranslationTrait;
/**
* @var \Drupal\token\Token
*/
protected $tokenService;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Cache already built trees.
*
* @var array
*/
protected $builtTrees;
public function __construct(TokenInterface $token_service, TokenEntityMapperInterface $entity_mapper, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) {
$this->tokenService = $token_service;
$this->entityMapper = $entity_mapper;
$this->cacheBackend = $cache_backend;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function buildRenderable(array $token_types, array $options = []) {
// Set default options.
$options += [
'global_types' => TRUE,
'click_insert' => TRUE,
'show_restricted' => FALSE,
'show_nested' => FALSE,
'recursion_limit' => 3,
];
$info = $this->tokenService->getInfo();
if ($options['global_types']) {
$token_types = array_merge($token_types, $this->tokenService->getGlobalTokenTypes());
}
$element = [
/*'#cache' => [
'cid' => 'tree-rendered:' . hash('sha256', serialize(['token_types' => $token_types, 'global_types' => NULL] + $variables)),
'tags' => [Token::TOKEN_INFO_CACHE_TAG],
],*/
];
// @todo Find a way to use the render cache for this.
$tree_options = [
'flat' => TRUE,
'restricted' => $options['show_restricted'],
'nested' => $options['show_nested'],
'depth' => $options['recursion_limit'],
];
$token_tree = [];
foreach ($info['types'] as $type => $type_info) {
if (!in_array($type, $token_types)) {
continue;
}
$token_tree[$type] = $type_info;
$token_tree[$type]['tokens'] = $this->buildTree($type, $tree_options);
}
$element += [
'#type' => 'token_tree_table',
'#token_tree' => $token_tree,
'#show_restricted' => $options['show_restricted'],
'#show_nested' => $options['show_nested'],
'#click_insert' => $options['click_insert'],
'#columns' => ['name', 'token', 'description'],
'#empty' => $this->t('No tokens available'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function buildAllRenderable(array $options = []) {
$info = $this->tokenService->getInfo();
$token_types = array_keys($info['types']);
// Disable merging in global types as we will be adding in all token types
// explicitly. There is no difference in leaving this set to TRUE except for
// an additional method call which is unnecessary.
$options['global_types'] = FALSE;
return $this->buildRenderable($token_types, $options);
}
/**
* {@inheritdoc}
*/
public function buildTree($token_type, array $options = []) {
$options += [
'restricted' => FALSE,
'depth' => 4,
'data' => [],
'values' => FALSE,
'flat' => FALSE,
];
// Do not allow past the maximum token information depth.
$options['depth'] = min($options['depth'], static::MAX_DEPTH);
// If $token_type is an entity, make sure we are using the actual token type.
if ($entity_token_type = $this->entityMapper->getTokenTypeForEntityType($token_type)) {
$token_type = $entity_token_type;
}
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$tree_cid = "token_tree:{$token_type}:{$langcode}:{$options['depth']}";
// If we do not have this base tree in the static cache, check the cache
// otherwise generate and store it in the cache.
if (!isset($this->builtTrees[$tree_cid])) {
if ($cache = $this->cacheBackend->get($tree_cid)) {
$this->builtTrees[$tree_cid] = $cache->data;
}
else {
$options['parents'] = [];
$this->builtTrees[$tree_cid] = $this->getTokenData($token_type, $options);
$this->cacheBackend->set($tree_cid, $this->builtTrees[$tree_cid], Cache::PERMANENT, [Token::TOKEN_INFO_CACHE_TAG]);
}
}
$tree = $this->builtTrees[$tree_cid];
// If the user has requested a flat tree, convert it.
if (!empty($options['flat'])) {
$tree = $this->flattenTree($tree);
}
// Fill in token values.
if (!empty($options['values'])) {
$token_values = [];
foreach ($tree as $token => $token_info) {
if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) {
continue;
}
elseif (!isset($token_info['value'])) {
$token_values[$token_info['token']] = $token;
}
}
if (!empty($token_values)) {
$token_values = $this->tokenService->generate($token_type, $token_values, $options['data'], [], new BubbleableMetadata());
foreach ($token_values as $token => $replacement) {
$tree[$token]['value'] = $replacement;
}
}
}
return $tree;
}
/**
* {@inheritdoc}
*/
public function flattenTree(array $tree) {
$result = [];
foreach ($tree as $token => $token_info) {
$result[$token] = $token_info;
if (isset($token_info['children']) && is_array($token_info['children'])) {
$result += $this->flattenTree($token_info['children']);
}
}
return $result;
}
/**
* Generate a token tree.
*
* @param string $token_type
* The token type.
* @param array $options
* An associative array of additional options. See documentation for
* TreeBuilderInterface::buildTree() for more information.
*
* @return array
* The token data for the specified $token_type.
*
* @internal
*/
protected function getTokenData($token_type, array $options) {
$options += [
'parents' => [],
];
$info = $this->tokenService->getInfo();
if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
return [];
}
$tree = [];
foreach ($info['tokens'][$token_type] as $token => $token_info) {
// Build the raw token string.
$token_parents = $options['parents'];
if (empty($token_parents)) {
// If the parents array is currently empty, assume the token type is its
// parent.
$token_parents[] = $token_type;
}
// The 'entity' token will be repeated on nested entity reference fields.
elseif ($token !== 'entity' && in_array($token, array_slice($token_parents, 1), TRUE)) {
// Prevent duplicate recursive tokens. For example, this will prevent
// the tree from generating the following tokens or deeper:
// [comment:parent:parent]
// [comment:parent:root:parent]
continue;
}
$token_parents[] = $token;
if (!empty($token_info['dynamic'])) {
$token_parents[] = '?';
}
$raw_token = '[' . implode(':', $token_parents) . ']';
$tree[$raw_token] = $token_info;
$tree[$raw_token]['raw token'] = $raw_token;
// Add the token's real name (leave out the base token type).
$tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1));
// Add the token's parent as its raw token value.
if (!empty($options['parents'])) {
$tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']';
}
// Fetch the child tokens.
if (!empty($token_info['type'])) {
$child_options = $options;
$child_options['depth']--;
$child_options['parents'] = $token_parents;
$tree[$raw_token]['children'] = $this->getTokenData($token_info['type'], $child_options);
}
}
return $tree;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\token;
interface TreeBuilderInterface {
/**
* The maximum depth for token tree recursion.
*/
const MAX_DEPTH = 9;
/**
* Build a tree array of tokens used for theming or information.
*
* @param string $token_type
* The token type.
* @param array $options
* (optional) An associative array of additional options, with the following
* elements:
* - 'flat' (defaults to FALSE): Set to true to generate a flat list of
* token information. Otherwise, child tokens will be inside the
* 'children' parameter of a token.
* - 'restricted' (defaults to FALSE): Set to true to how restricted tokens.
* - 'depth' (defaults to 4): Maximum number of token levels to recurse.
*
* @return array
* The token information constructed in a tree or flat list form depending
* on $options['flat'].
*/
public function buildTree($token_type, array $options = []);
/**
* Flatten a token tree.
*
* @param array $tree
* The tree array as returned by TreeBuilderInterface::buildTree().
*
* @return array
* The flattened version of the tree.
*/
public function flattenTree(array $tree);
/**
* Build a render array with token tree built as per specified options.
*
* @param array $token_types
* An array containing token types that should be shown in the tree.
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are:
* - 'global_types' (defaults to TRUE): Show all global token types along
* with the specified types.
* - 'click_insert' (defaults to TRUE): Include classes and caption to show
* allow inserting tokens in fields by clicking on them.
* - 'show_restricted' (defaults to FALSE): Show restricted tokens in the
* tree.
* - 'show_nested' (defaults to FALSE): If this token is nested and should
* therefore not show on the token browser as a top level token.
* - 'recursion_limit' (defaults to 3): Only show tokens up to the specified
* depth.
*
* @return array
* Render array for the token tree.
*/
public function buildRenderable(array $token_types, array $options = []);
/**
* Build a render array with token tree containing all possible tokens.
*
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are: See
* \Drupal\token\TreeBuilderInterface::buildRenderable() for details.
*
* @return array
* Render array for the token tree.
*/
public function buildAllRenderable(array $options = []);
}