mustache_templates-8.x-1.0-beta4/src/MustacheTokenProcessor.php
src/MustacheTokenProcessor.php
<?php
namespace Drupal\mustache;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\BackendChain;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\Utility\Token;
use Drupal\mustache\Plugin\MustacheMagicManager;
use Drupal\mustache\Render\IterableMarkup;
/**
* Takes care of token processing within the render pipeline.
*
* @see \Drupal\mustache\Element\Mustache
*/
class MustacheTokenProcessor {
/**
* The cache bin to store tokenized template content.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $templateCache;
/**
* The cache backend that is an in-memory cache for Mustache data.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $dataMemoryCache;
/**
* The consistent cache backend for Mustache data.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $dataConsistentCache;
/**
* The chained cache backends for Mustache data.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $dataChainedCache;
/**
* The cache contexts manager.
*
* @var \Drupal\Core\Cache\Context\CacheContextsManager
*/
protected $cacheContextsManager;
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The context repository.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* The manager of Mustache magic variable plugins.
*
* @var \Drupal\mustache\Plugin\MustacheMagicManager
*/
protected $magicPluginManager;
/**
* Statically cached context IDs.
*
* @var string[]
*/
protected static $contextIds;
/**
* The MustacheTokenProcessor constructor.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $template_cache
* The cache bin to store tokenized template content.
* @param \Drupal\Core\Cache\CacheBackendInterface $data_memory_cache
* The cache backend that is an in-memory cache for Mustache data.
* @param \Drupal\Core\Cache\CacheBackendInterface $data_consistent_cache
* The consistent cache backend for Mustache data.
* @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
* The cache contexts manager.
* @param \Drupal\Core\Utility\Token $token
* The token service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The context repository.
* @param \Drupal\mustache\Plugin\MustacheMagicManager $magic_plugin_manager
* The manager of Mustache magic variable plugins.
*/
public function __construct(CacheBackendInterface $template_cache, CacheBackendInterface $data_memory_cache, CacheBackendInterface $data_consistent_cache, CacheContextsManager $cache_contexts_manager, Token $token, EntityTypeManagerInterface $entity_type_manager, ContextRepositoryInterface $context_repository, MustacheMagicManager $magic_plugin_manager) {
$this->templateCache = $template_cache;
$this->dataMemoryCache = $data_memory_cache;
$this->dataConsistentCache = $data_consistent_cache;
$this->dataChainedCache = (new BackendChain())
->appendBackend($data_memory_cache)
->appendBackend($data_consistent_cache);
$this->cacheContextsManager = $cache_contexts_manager;
$this->token = $token;
$this->entityTypeManager = $entity_type_manager;
$this->contextRepository = $context_repository;
$this->magicPluginManager = $magic_plugin_manager;
}
/**
* Processes tokens for the given template content.
*
* Before invoking this method, check before whether it is really needed.
*
* @param array &$element
* The current element build array.
* @param string &$template_content
* The current template content.
*
* @\Drupal\mustache\Element\Mustache::processTokens().
*/
public function processTokens(array &$element, &$template_content) {
if (!isset($element['#data'])) {
$element['#data'] = [];
}
if (!is_array($element['#with_tokens'])) {
$element['#with_tokens'] = [];
}
$token_settings = &$element['#with_tokens'];
$token_options = isset($token_settings['options']) ? $token_settings['options'] : [];
$langcode = $token_options['langcode'] ?? \Drupal::languageManager()->getCurrentLanguage()->getId();
// By default, empty or non-matched variables shall be cleared.
if (!isset($token_options['clear'])) {
$token_options['clear'] = TRUE;
}
$template_hash = hash('md4', $template_content);
$tokenized = $this->tokenizeTemplate($template_hash, $template_content);
if (empty($tokenized['tokens'])) {
// No contained tokens were found, thus halt at this point.
return;
}
$template_content = $tokenized['content'];
$template_tokens = $tokenized['tokens'];
$mustache_variables = $tokenized['variables'];
if (!empty($element['#cache'])) {
$bubbleable_metadata = BubbleableMetadata::createFromRenderArray($element);
}
else {
$bubbleable_metadata = new BubbleableMetadata();
}
// Build up the token data.
$token_data = isset($token_settings['data']) ? $token_settings['data'] : [];
$this->processData($token_data, $template_tokens, 'view', $bubbleable_metadata, $langcode);
// Determine cacheability of the Token data.
$data_is_cacheable = empty(array_diff_key($template_tokens, $token_data));
$data_cid = [];
foreach ($token_data as $t_key => $t_value) {
$data_cid[] = $t_key;
$entity = NULL;
if (is_object($t_value)) {
if ($t_value instanceof EntityInterface) {
$entity = $t_value;
}
elseif ($t_value instanceof \JsonSerializable) {
$data_cid[] = json_encode($t_value->jsonSerialize());
}
elseif (method_exists($t_value, 'getEntity')) {
$entity = $t_value->getEntity();
}
elseif (method_exists($t_value, '__toString')) {
$data_cid[] = (string) $t_value;
}
elseif (method_exists($t_value, 'getString')) {
$data_cid[] = $t_value->getString();
}
elseif (method_exists($t_value, 'getId')) {
$data_cid[] = $t_value->getId();
}
else {
$data_is_cacheable = FALSE;
break;
}
}
elseif (is_scalar($t_value)) {
$data_cid[] = (string) $t_value;
}
else {
$data_is_cacheable = FALSE;
break;
}
if ($entity) {
if ($entity->isNew()) {
$data_is_cacheable = FALSE;
break;
}
$data_cid[] = $entity->getEntityTypeId() . ':' . $entity->id();
}
}
// Try to get previously generated data from cache.
$previous_data = NULL;
if ($data_is_cacheable) {
if ($cache_contexts = $bubbleable_metadata->getCacheContexts()) {
$context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($cache_contexts);
$data_cid = array_merge($data_cid, $context_cache_keys->getKeys());
}
$data_cid = 'mustache:token:' . $template_hash . ':' . hash('md4', implode(':', $data_cid));
if ($cached = $this->dataChainedCache->get($data_cid, TRUE)) {
if ($cached->valid) {
$mustache_data = $cached->data;
$this->attachTokenData($element, $mustache_data);
return;
}
else {
$previous_data = $cached->data;
}
}
}
// Generate iterable token replacement values.
$replacements = [];
foreach ($template_tokens as $type => $tokens) {
if ($type === 'iterate') {
$replacements += $this->token->generate($type, $tokens, $token_data, $token_options, $bubbleable_metadata);
}
else {
foreach ($tokens as $token) {
$target = static::getTokenIterate()->getIterableTarget(substr($token, 1, -1), $token_data, $token_options, $bubbleable_metadata);
if ($token_options['clear'] || $target->exists()) {
$replacements[$token] = $target;
}
}
}
if ($token_options['clear']) {
$replacements += array_fill_keys($tokens, '');
}
}
// No explicit escaping of token values is being done here, because
// wrapped Mustache variables will be escaped by default, if not using
// the triple {{{}}} parenthesis.
// Optionally alter the list of replacement values.
if (!empty($token_options['callback'])) {
$function = $token_options['callback'];
$function($replacements, $token_data, $token_options, $bubbleable_metadata);
}
$bubbleable_metadata->applyTo($element);
// Merge the replacement values into one iterable data tree.
$mustache_data = IterableMarkup::create();
foreach ($replacements as $token => $replacement) {
$var_pieces = explode('.', $mustache_variables[$token]);
$token_pieces = explode(':', substr($token, 1, -1));
// Special care taken for the "iterate" token.
$is_iterate = reset($token_pieces) === 'iterate';
$iterable = &$mustache_data;
foreach ($token_pieces as $i => $token_piece) {
$var_piece = $var_pieces[$i];
if (!isset($iterable[$var_piece])) {
$iterable[$var_piece] = IterableMarkup::create([], NULL, $iterable);
}
elseif (!($iterable[$var_piece] instanceof IterableMarkup)) {
$iterable[$var_piece] = IterableMarkup::create([], $iterable[$var_piece], $iterable);
}
$iterable = &$iterable[$var_piece];
if (($replacement instanceof \ArrayAccess) || is_array($replacement)) {
$replacement = isset($replacement[$token_piece]) ? $replacement[$token_piece] : ($is_iterate ? $replacement : IterableMarkup::create([], NULL, $replacement));
continue;
}
if (is_object($replacement)) {
if (method_exists($replacement, $token_piece)) {
$reflection = new \ReflectionMethod($replacement, $token_piece);
if (!$reflection->getNumberOfRequiredParameters()) {
$replacement = $replacement->$token_piece();
}
}
elseif (isset($replacement->$token_piece)) {
$replacement = $replacement->$token_piece;
}
continue;
}
}
if ($replacement instanceof IterableMarkup) {
if (!empty($replacement->string)) {
$iterable->value = $replacement->string;
}
$iterable->items = array_merge($iterable->items, $replacement->items);
$iterable->updatePositions();
}
elseif (is_iterable($replacement)) {
foreach ($replacement as $key => $value) {
// For being able to perform simple checks within the loop of
// iterable data, like using {{#first}} and {{#last}} checks, any
// value is being wrapped by an iterable object too.
if (is_numeric($key)) {
$iterable[] = IterableMarkup::create([], $value, $iterable);
}
else {
$iterable[$key] = IterableMarkup::create([], $value, $iterable);
}
}
}
elseif (is_scalar($replacement) || (is_object($replacement) && method_exists($replacement, '__toString'))) {
// When the replacement value represents a scalar, then the value of
// the current iterable object is being set by the scalar itself.
$iterable->value = (string) $replacement;
}
// Unset to not manipulate the last reference in the next loop.
unset($iterable);
}
// The existence of a "previous" key within $mustache_data indicates the
// attempt to access previous data as Mustache variable. We can safely skip
// adding previous data here as we know it would not be needed.
if (isset($previous_data, $mustache_data->getItems()['previous'])) {
unset($mustache_data['previous']);
if (isset($previous_data['previous']) || (($previous_data instanceof IterableMarkup) && isset($previous_data->getItems()['previous']))) {
$prev_prev = $previous_data['previous'];
// Unset the "previous" key from the previously fetched data, in order
// to avoid unlimited growth.
unset($previous_data['previous']);
if (($previous_data instanceof IterableMarkup && ($previous_data->jsonSerialize() == $mustache_data->jsonSerialize())) || (!($previous_data instanceof IterableMarkup) && ($previous_data == $mustache_data))) {
// Previously fetched data appears to be the same, thus jump once
// more back to previous data of the previously fetched one.
unset($prev_prev['previous']);
$previous_data = $prev_prev;
}
}
$mustache_data['previous'] = $previous_data;
}
if ($data_is_cacheable) {
$bubbleable_metadata->addCacheTags(['mustache:token']);
$this->dataChainedCache->set($data_cid, $mustache_data, $bubbleable_metadata->getCacheMaxAge(), $bubbleable_metadata->getCacheTags());
}
$this->attachTokenData($element, $mustache_data);
}
/**
* Processes given token data by enriching with context and access filtering.
*
* @param array &$token_data
* The token data to process.
* @param array|null $tokens
* (Optional) An array of tokens that are known to be used. Set to NULL if
* they are not known beforehand. This should be set whenever possible,
* otherwise cacheability may be not working properly.
* @param string $check_access
* (Optional) The access check to perform. Set NULL to skip access filter.
* @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata
* (Optional) The object to collect bubbleable metadata.
* @param string|null $langcode
* (Optional) The langcode to use. Leave NULL to use the current language.
*/
public function processData(array &$token_data, $tokens = NULL, $check_access = 'view', $bubbleable_metadata = NULL, $langcode = NULL) {
if (isset($tokens) && empty($tokens)) {
return;
}
if (!isset($bubbleable_metadata)) {
$bubbleable_metadata = new BubbleableMetadata();
}
if (!isset($langcode)) {
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
}
/** @var \Drupal\token\TokenEntityMapperInterface $entity_token_type_mapper */
$entity_token_type_mapper = \Drupal::hasService('token.entity_mapper') ? \Drupal::service('token.entity_mapper') : NULL;
// Merge with static data, e.g. provided by preprocess hooks.
if ($static_token_data = &drupal_static('mustache_tokens', [])) {
foreach ($static_token_data as $type => $value) {
if (isset($value) && !isset($token_data[$type])) {
$token_data[$type] = $value;
}
}
}
$type_id = 'user';
if (!isset($token_data[$type_id]) && (!isset($tokens) || isset($tokens[$type_id]))) {
$token_data[$type_id] = $this->entityTypeManager->getStorage($type_id)->load(\Drupal::currentUser()->id());
}
// Merge the token data with any global context that provides a value.
$context_repository = $this->contextRepository;
$contexts = [];
if (!isset(static::$contextIds)) {
static::$contextIds = array_keys($context_repository->getAvailableContexts());
}
if (!empty(static::$contextIds)) {
foreach ($context_repository->getRuntimeContexts(static::$contextIds) as $context) {
$context_data_type = explode(':', $context->getContextDefinition()->getDataType());
// For "entity:*" data types, we skip its prefixes and only respect
// the last entry of the data type string (e.g. for "entity:node", we
// want to only use "node").
$type = end($context_data_type);
$value = $context->hasContextValue() ? $context->getContextValue() : NULL;
if ($value instanceof EntityInterface) {
if ($entity_token_type_mapper) {
$type = $entity_token_type_mapper->getTokenTypeForEntityType($value->getEntityTypeId(), TRUE);
}
else {
$definition = $this->entityTypeManager->getDefinition($value->getEntityTypeId());
$type = $definition->get('token_type') ?: $value->getEntityTypeId();
}
}
$contexts[$type] = $context;
if (isset($value) && !isset($token_data[$type]) && (!isset($tokens) || isset($tokens[$type]))) {
$token_data[$type] = $value;
}
}
}
// If not yet given otherwise, get entities from route parameters.
foreach (\Drupal::routeMatch()->getParameters() as $parameter) {
if (!($parameter instanceof EntityInterface)) {
continue;
}
$type_id = $parameter->getEntityTypeId();
if (!isset($token_data[$type_id]) && (!isset($tokens) || isset($tokens[$type_id]))) {
$token_data[$type_id] = $parameter;
}
}
// Switch to translations if necessary and available.
// Also re-map entities that may have a different token type defined.
foreach ($token_data as $type => $value) {
if (($value instanceof TranslatableInterface) && ($value->language()->getId() !== $langcode) && ($value->hasTranslation($langcode))) {
$value = $value->getTranslation($langcode);
$token_data[$type] = $value;
}
if (!($value instanceof EntityInterface)) {
continue;
}
/** @var \Drupal\Core\Entity\EntityInterface $value */
if ($entity_token_type_mapper) {
$the_real_token_type = $entity_token_type_mapper->getTokenTypeForEntityType($value->getEntityTypeId(), TRUE);
}
else {
$definition = $this->entityTypeManager->getDefinition($value->getEntityTypeId());
$the_real_token_type = $definition->get('token_type') ?: $value->getEntityTypeId();
}
if (!isset($token_data[$the_real_token_type])) {
if (isset($contexts[$type])) {
$contexts[$the_real_token_type] = $contexts[$type];
}
$token_data[$the_real_token_type] = $value;
}
}
// Filter out data that is not allowed to be viewed.
// Also add cacheability metadata of data that is being used.
foreach ($token_data as $type => $value) {
if ($value instanceof CacheableDependencyInterface) {
$bubbleable_metadata->addCacheableDependency($value);
}
if (isset($contexts[$type])) {
$bubbleable_metadata->addCacheableDependency($contexts[$type]);
}
if ($check_access && $value instanceof AccessibleInterface) {
/** @var \Drupal\Core\Access\AccessResultInterface $access_result */
$access_result = $value->access($check_access, NULL, TRUE);
$bubbleable_metadata->addCacheableDependency($access_result);
if (!$access_result->isAllowed()) {
unset($token_data[$type]);
continue;
}
}
}
}
/**
* Tokenizes the given template content.
*
* @param string $template_name
* A unique name identifies the template content.
* @param string $template_content
* The template content to tokenize.
*
* @return array
* Extracted tokenized results, keyed by 'content' that holds the tokenized
* template content, 'tokens' holds identified tokens, and 'variables' holds
* a mapping of tokens as keys to Mustache variables as values.
*/
public function tokenizeTemplate($template_name, $template_content) {
$template_cid = 'mustache:tokenized:' . $template_name;
if ($cached = $this->templateCache->get($template_cid)) {
return $cached->data;
}
else {
if (strpos($template_content, 'iterate') !== FALSE) {
// Special handling for iterate tokens, because IterableMarkup
// implements methods and properties that could collide by having the
// same name as the token name. Examples: "count", "first" and "last".
$iterate_reflection = new \ReflectionClass(IterableMarkup::class);
foreach ([':', '.'] as $accessor) {
foreach ($iterate_reflection->getProperties() as $property) {
$search = 'iterate' . $accessor . $property->getName();
if (strpos($template_content, $search) !== FALSE) {
$template_content = str_replace($search, 'iterate' . $accessor . '_' . $property->getName(), $template_content);
}
}
foreach ($iterate_reflection->getMethods() as $method) {
$search = 'iterate' . $accessor . $method->getName();
if (strpos($template_content, $search) !== FALSE) {
$template_content = str_replace($search, 'iterate' . $accessor . '_' . $method->getName(), $template_content);
}
}
}
}
// Scan for regular tokens.
$template_tokens = $this->token->scan($template_content);
// Also include Mustache variables as tokens, but exclude magic variables.
$magic_variables = [];
foreach (array_keys($this->magicPluginManager->getDefinitions()) as $plugin_id) {
$plugin_id_parts = explode(':', $plugin_id);
if ($magic_variable = reset($plugin_id_parts)) {
$magic_variables[$magic_variable] = 1;
}
}
preg_match_all('/\{\{[^a-zA-Z\d\_\-\{\#\&\^]*([a-zA-Z\d\_\-\?\<\>\/\.]+)[^a-zA-Z\d\_\-\.\}]*\}\}/x', $template_content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $match) {
$match = trim($match);
$parts = explode('.', $match);
$type = array_shift($parts);
if (empty($type) || empty(reset($parts)) || isset($magic_variables[$type])) {
// A token always consists of at least two parts: The token type as
// first prefix, followed by a ":" and a name (e.g. node:title).
// Therefore, we do not try to include variables that only consist
// of one key (i.e. only include variables with a "." dot).
continue;
}
if (!isset($token_data[$type])) {
$type_hyphened = str_replace('_', '-', $type);
if (isset($token_data[$type_hyphened])) {
$template_content = str_replace($match, $type_hyphened . '.' . implode('.', $parts), $template_content);
$type = $type_hyphened;
}
}
$name = implode(':', $parts);
array_unshift($parts, $type);
$token = '[' . implode(':', $parts) . ']';
$template_tokens[$type][$name] = $token;
}
}
if (empty($template_tokens)) {
$result = [
'content' => $template_content,
'tokens' => $template_tokens,
'variables' => [],
];
$this->templateCache->set($template_cid, $result);
return $result;
}
unset($matches);
// Transform tokens into Mustache variables.
$mustache_variables = [];
foreach ($template_tokens as $type => $tokens) {
foreach ($tokens as $token) {
$variable = str_replace(':', '.', substr($token, 1, -1));
if (strpos($variable, '-') !== FALSE) {
// Transform any hyphens into underscores, so that built up
// data trees always match for given keys, even when hyphens and
// underscores are being mixed up.
$variable_underscore = str_replace('-', '_', $variable);
$template_content = str_replace($variable, $variable_underscore, $template_content);
$variable = $variable_underscore;
}
$mustache_variables[$token] = $variable;
}
}
// Ensure that standalone tokens are being wrapped by Mustache brackets,
// and replace all token versions by their Mustache variable counterparts.
preg_match_all('/(\{\{)?[^a-zA-Z\d\_\-\{\#\&\^]*(\[[^\s\[\]:]+:[^\[\]]+\])[^a-zA-Z\d\_\-\?\.\}]*(\}\})?/x', $template_content, $matches);
if (!empty($matches[2])) {
foreach ($matches[2] as $i => $token) {
if (!isset($mustache_variables[$token])) {
continue;
}
$search = $matches[0][$i];
$variable = $mustache_variables[$token];
if ($matches[1][$i] !== '{{') {
$variable = '{{' . $variable;
}
if ($matches[3][$i] !== '}}') {
$variable .= '}}';
}
$replace = str_replace($token, $variable, $search);
$template_content = str_replace($search, $replace, $template_content);
}
}
$result = [
'content' => $template_content,
'tokens' => $template_tokens,
'variables' => $mustache_variables,
];
// Cache the tokenized results for the next request.
$this->templateCache->set($template_cid, $result);
return $result;
}
}
/**
* Get the memory cache for Mustache data.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* The memory cache.
*/
public function getDataMemoryCache() {
return $this->dataMemoryCache;
}
/**
* Get the consistent cache for Mustache data.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* The consistent cache.
*/
public function getDataConsistentCache() {
return $this->dataConsistentCache;
}
/**
* Get the chained cache for Mustache data.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* The chained cache.
*/
public function getDataChainedCache() {
return $this->dataChainedCache;
}
/**
* Attaches the generated Token data to the render element.
*
* @param array &$element
* The current element build array.
* @param mixed &$mustache_data
* The Token data to attach.
*/
protected function attachTokenData(array &$element, &$mustache_data) {
if (empty($element['#data'])) {
$element['#data'] = $mustache_data;
}
elseif (is_iterable($element['#data'])) {
foreach ($element['#data'] as $key => $value) {
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
$mustache_data[$key] = $value;
}
}
$element['#data'] = $mustache_data;
}
elseif (isset($element['#override_data']) && is_iterable($element['#override_data'])) {
foreach ($element['#override_data'] as $key => $value) {
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
$mustache_data[$key] = $value;
}
}
$element['#override_data'] = $mustache_data;
}
else {
$element['#override_data'] = $mustache_data;
}
}
/**
* Get the token iterate service.
*
* @return \Drupal\mustache\MustacheTokenIterate
* The token iterate service.
*/
protected static function getTokenIterate() {
return \Drupal::service('mustache.token_iterate');
}
}
