$node, 'user' => $user); * $result = $token_service->replace($text, $data); * return $result * @endcode * * Some tokens may be chained in the form of [$type:$pointer:$name], where $type * is a normal token type, $pointer is a reference to another token type, and * $name is the name of a given placeholder. For example, [node:author:mail]. In * that example, 'author' is a pointer to the 'user' account that created the * node, and 'mail' is a placeholder available for any 'user'. * * @see Token::replace() * @see hook_tokens() * @see hook_token_info() */ class Token { /** * The tag to cache token info with. */ const TOKEN_INFO_CACHE_TAG = 'token_info'; /** * The token cache. * * @var \Drupal\Core\Cache\CacheBackendInterface */ protected $cache; /** * The language manager. * * @var \Drupal\Core\Language\LanguageManagerInterface */ protected $languageManager; /** * Token definitions. * * @var array[]|null * An array of token definitions, or NULL when the definitions are not set. * * @see self::setInfo() * @see self::getInfo() * @see self::resetInfo() */ protected $tokenInfo; /** * The module handler service. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected $moduleHandler; /** * The cache tags invalidator. * * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface */ protected $cacheTagsInvalidator; /** * The renderer. * * @var \Drupal\Core\Render\RendererInterface */ protected $renderer; /** * Constructs a new class instance. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The token cache. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator * The cache tags invalidator. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. */ public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, CacheTagsInvalidatorInterface $cache_tags_invalidator, RendererInterface $renderer) { $this->cache = $cache; $this->languageManager = $language_manager; $this->moduleHandler = $module_handler; $this->cacheTagsInvalidator = $cache_tags_invalidator; $this->renderer = $renderer; } /** * Replaces all tokens in given markup with appropriate values. * * @param string $markup * An HTML string containing replaceable tokens. * @param array $data * (optional) An array of keyed objects. For simple replacement scenarios * 'node', 'user', and others are common keys, with an accompanying node or * user object being the value. Some token types, like 'site', do not require * any explicit information from $data and can be replaced even if it is * empty. * @param array $options * (optional) A keyed array of settings and flags to control the token * replacement process. Supported options are: * - langcode: A language code to be used when generating locale-sensitive * tokens. * - callback: A callback function that will be used to post-process the * array of token replacements after they are generated. * - clear: A boolean flag indicating that tokens should be removed from the * final text if no replacement value can be generated. * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) An object to which static::generate() and the hooks and * functions that it invokes will add their required bubbleable metadata. * * To ensure that the metadata associated with the token replacements gets * attached to the same render array that contains the token-replaced text, * callers of this method are encouraged to pass in a BubbleableMetadata * object and apply it to the corresponding render array. For example: * @code * $bubbleable_metadata = new BubbleableMetadata(); * $build['#markup'] = $token_service->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], [], $bubbleable_metadata); * $bubbleable_metadata->applyTo($build); * @endcode * * When the caller does not pass in a BubbleableMetadata object, this * method creates a local one, and applies the collected metadata to the * Renderer's currently active render context. * * @return string * The token result is the entered HTML text with tokens replaced. The * caller is responsible for choosing the right sanitization, for example * the result can be put into #markup, in which case it would be sanitized * by Xss::filterAdmin(). * * The return value must be treated as unsafe even if the input was safe * markup. This is necessary because an attacker could craft an input * string and token value that, although each safe individually, would be * unsafe when combined by token replacement. * * @see static::replacePlain() */ public function replace($markup, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { return $this->doReplace(TRUE, (string) $markup, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given plain text string with appropriate values. * * @param string $plain * Plain text string. * @param array $data * (optional) An array of keyed objects. See replace(). * @param array $options * (optional) A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The entered plain text with tokens replaced. */ public function replacePlain(string $plain, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL): string { return $this->doReplace(FALSE, $plain, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given string with appropriate values. * * @param bool $markup * TRUE to convert token values to markup, FALSE to convert to plain text. * @param string $text * A string containing replaceable tokens. * @param array $data * An array of keyed objects. See replace(). * @param array $options * A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The token result is the entered string with tokens replaced. */ protected function doReplace(bool $markup, string $text, array $data, array $options, BubbleableMetadata $bubbleable_metadata = NULL): string { $text_tokens = $this->scan($text); if (empty($text_tokens)) { return $text; } $bubbleable_metadata_is_passed_in = (bool) $bubbleable_metadata; $bubbleable_metadata = $bubbleable_metadata ?: new BubbleableMetadata(); $replacements = []; foreach ($text_tokens as $type => $tokens) { $replacements += $this->generate($type, $tokens, $data, $options, $bubbleable_metadata); if (!empty($options['clear'])) { $replacements += array_fill_keys($tokens, ''); } } // Each token value is markup if it implements MarkupInterface otherwise it // is plain text. Convert them, but only if needed. It can cause corruption // to render a string that's already plain text or to escape a string // that's already markup. foreach ($replacements as $token => $value) { if ($markup) { // Escape plain text tokens. $replacements[$token] = $value instanceof MarkupInterface ? $value : new HtmlEscapedText($value); } else { // Render markup tokens to plain text. $replacements[$token] = $value instanceof MarkupInterface ? PlainTextOutput::renderFromHtml($value) : $value; } } // Optionally alter the list of replacement values. if (!empty($options['callback'])) { $function = $options['callback']; $function($replacements, $data, $options, $bubbleable_metadata); } $tokens = array_keys($replacements); $values = array_values($replacements); // If a local $bubbleable_metadata object was created, apply the metadata // it collected to the renderer's currently active render context. if (!$bubbleable_metadata_is_passed_in && $this->renderer->hasRenderContext()) { $build = []; $bubbleable_metadata->applyTo($build); $this->renderer->render($build); } return str_replace($tokens, $values, $text); } /** * Builds a list of all token-like patterns that appear in the text. * * @param string $text * The text to be scanned for possible tokens. * * @return array * An associative array of discovered tokens, grouped by type. */ public function scan(string $text) { // Matches tokens with the following pattern: [$type:$name] // $type and $name may not contain [ ] characters. // $type may not contain : or whitespace characters, but $name may. preg_match_all('/ \[ # [ - pattern start ([^\s\[\]:]+) # match $type not containing whitespace : [ or ] : # : - separator ([^\[\]]+) # match $name not containing [ or ] \] # ] - pattern end /x', $text, $matches); $types = $matches[1]; $tokens = $matches[2]; // Iterate through the matches, building an associative array containing // $tokens grouped by $types, pointing to the version of the token found in // the source text. For example, $results['node']['title'] = '[node:title]'; $results = []; for ($i = 0; $i < count($tokens); $i++) { $results[$types[$i]][$tokens[$i]] = $matches[0][$i]; } return $results; } /** * Generates replacement values for a list of tokens. * * @param string $type * The type of token being replaced. 'node', 'user', and 'date' are common. * @param array $tokens * An array of tokens to be replaced, keyed by the literal text of the token * as it appeared in the source text. * @param array $data * An array of keyed objects. For simple replacement scenarios: 'node', * 'user', and others are common keys, with an accompanying node or user * object being the value. Some token types, like 'site', do not require * any explicit information from $data and can be replaced even if it is * empty. * @param array $options * A keyed array of settings and flags to control the token replacement * process. Supported options are: * - langcode: A language code to be used when generating locale-sensitive * tokens. * - callback: A callback function that will be used to post-process the * array of token replacements after they are generated. Can be used when * modules require special formatting of token text, for example URL * encoding or truncation to a specific length. * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata * The bubbleable metadata. This is passed to the token replacement * implementations so that they can attach their metadata. * * @return array * An associative array of replacement values, keyed by the original 'raw' * tokens that were found in the source text. For example: * $results['[node:title]'] = 'My new node'; * * @see hook_tokens() * @see hook_tokens_alter() */ public function generate($type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { foreach ($data as $object) { if ($object instanceof CacheableDependencyInterface || $object instanceof AttachmentsInterface) { $bubbleable_metadata->addCacheableDependency($object); } } $replacements = $this->moduleHandler->invokeAll('tokens', [$type, $tokens, $data, $options, $bubbleable_metadata]); // Allow other modules to alter the replacements. $context = [ 'type' => $type, 'tokens' => $tokens, 'data' => $data, 'options' => $options, ]; $this->moduleHandler->alter('tokens', $replacements, $context, $bubbleable_metadata); return $replacements; } /** * Returns a list of tokens that begin with a specific prefix. * * Used to extract a group of 'chained' tokens (such as [node:author:name]) * from the full list of tokens found in text. For example: * @code * $data = array( * 'author:name' => '[node:author:name]', * 'title' => '[node:title]', * 'created' => '[node:created]', * ); * $results = Token::findWithPrefix($data, 'author'); * $results == array('name' => '[node:author:name]'); * @endcode * * @param array $tokens * A keyed array of tokens, and their original raw form in the source text. * @param string $prefix * A textual string to be matched at the beginning of the token. * @param string $delimiter * (optional) A string containing the character that separates the prefix from * the rest of the token. Defaults to ':'. * * @return array * An associative array of discovered tokens, with the prefix and delimiter * stripped from the key. */ public function findWithPrefix(array $tokens, $prefix, $delimiter = ':') { $results = []; foreach ($tokens as $token => $raw) { $parts = explode($delimiter, $token, 2); if (count($parts) == 2 && $parts[0] == $prefix) { $results[$parts[1]] = $raw; } } return $results; } /** * Returns metadata describing supported tokens. * * The metadata array contains token type, name, and description data as well * as an optional pointer indicating that the token chains to another set of * tokens. * * @return array * An associative array of token information, grouped by token type. The * array structure is identical to that of hook_token_info(). * * @see hook_token_info() */ public function getInfo() { if (is_null($this->tokenInfo)) { $cache_id = 'token_info:' . $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); $cache = $this->cache->get($cache_id); if ($cache) { $this->tokenInfo = $cache->data; } else { $this->tokenInfo = $this->moduleHandler->invokeAll('token_info'); $this->moduleHandler->alter('token_info', $this->tokenInfo); $this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, [ static::TOKEN_INFO_CACHE_TAG, ]); } } return $this->tokenInfo; } /** * Sets metadata describing supported tokens. * * @param array $tokens * Token metadata that has an identical structure to the return value of * hook_token_info(). * * @see hook_token_info() */ public function setInfo(array $tokens) { $this->tokenInfo = $tokens; } /** * Resets metadata describing supported tokens. */ public function resetInfo() { $this->tokenInfo = NULL; $this->cacheTagsInvalidator->invalidateTags([static::TOKEN_INFO_CACHE_TAG]); } }