Commit 1a0cdcd3 authored by effulgentsia's avatar effulgentsia

Issue #2525910 by dawehner, effulgentsia, Berdir, lauriii, larowlan,...

Issue #2525910 by dawehner, effulgentsia, Berdir, lauriii, larowlan, timmillwood, Wim Leers, chx, arlinsandbulte, Fabianx, Gábor Hojtsy, Dave Reid, alexpott, catch: Ensure token replacements have cacheability + attachments metadata and that it is bubbled in any case
parent 4f60dd53
...@@ -1185,7 +1185,7 @@ services: ...@@ -1185,7 +1185,7 @@ services:
- { name: service_collector, tag: breadcrumb_builder, call: addBuilder } - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
token: token:
class: Drupal\Core\Utility\Token class: Drupal\Core\Utility\Token
arguments: ['@module_handler', '@cache.discovery', '@language_manager', '@cache_tags.invalidator'] arguments: ['@module_handler', '@cache.discovery', '@language_manager', '@cache_tags.invalidator', '@renderer']
batch.storage: batch.storage:
class: Drupal\Core\Batch\BatchStorage class: Drupal\Core\Batch\BatchStorage
arguments: ['@database', '@session', '@csrf_token'] arguments: ['@database', '@session', '@csrf_token']
......
...@@ -132,6 +132,35 @@ public function setCacheMaxAge($max_age) { ...@@ -132,6 +132,35 @@ public function setCacheMaxAge($max_age) {
return $this; return $this;
} }
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $other_object
* The dependency. If the object implements CacheableDependencyInterface,
* then its cacheability metadata will be used. Otherwise, the passed in
* object must be assumed to be uncacheable, so max-age 0 is set.
*
* @return $this
*/
public function addCacheableDependency($other_object) {
if ($other_object instanceof CacheableDependencyInterface) {
$this->addCacheTags($other_object->getCacheTags());
$this->addCacheContexts($other_object->getCacheContexts());
if ($this->maxAge === Cache::PERMANENT) {
$this->maxAge = $other_object->getCacheMaxAge();
}
elseif (($max_age = $other_object->getCacheMaxAge()) && $max_age !== Cache::PERMANENT) {
$this->maxAge = Cache::mergeMaxAges($this->maxAge, $max_age);
}
}
else {
// Not a cacheable dependency, this can not be cached.
$this->maxAge = 0;
}
return $this;
}
/** /**
* Merges the values of another CacheableMetadata object with this one. * Merges the values of another CacheableMetadata object with this one.
* *
......
...@@ -94,6 +94,19 @@ public static function createFromObject($object) { ...@@ -94,6 +94,19 @@ public static function createFromObject($object) {
return $meta; return $meta;
} }
/**
* {@inheritdoc}
*/
public function addCacheableDependency($other_object) {
parent::addCacheableDependency($other_object);
if ($other_object instanceof AttachmentsInterface) {
$this->addAttachments($other_object->getAttachments());
}
return $this;
}
/** /**
* Merges two attachments arrays (which live under the '#attached' key). * Merges two attachments arrays (which live under the '#attached' key).
* *
......
...@@ -8,11 +8,15 @@ ...@@ -8,11 +8,15 @@
namespace Drupal\Core\Utility; namespace Drupal\Core\Utility;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RendererInterface;
/** /**
* Drupal placeholder/token replacement system. * Drupal placeholder/token replacement system.
...@@ -104,6 +108,13 @@ class Token { ...@@ -104,6 +108,13 @@ class Token {
*/ */
protected $cacheTagsInvalidator; protected $cacheTagsInvalidator;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/** /**
* Constructs a new class instance. * Constructs a new class instance.
* *
...@@ -115,12 +126,15 @@ class Token { ...@@ -115,12 +126,15 @@ class Token {
* The language manager. * The language manager.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The 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) { public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, CacheTagsInvalidatorInterface $cache_tags_invalidator, RendererInterface $renderer) {
$this->cache = $cache; $this->cache = $cache;
$this->languageManager = $language_manager; $this->languageManager = $language_manager;
$this->moduleHandler = $module_handler; $this->moduleHandler = $module_handler;
$this->cacheTagsInvalidator = $cache_tags_invalidator; $this->cacheTagsInvalidator = $cache_tags_invalidator;
$this->renderer = $renderer;
} }
/** /**
...@@ -152,19 +166,39 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend ...@@ -152,19 +166,39 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend
* \Drupal\Component\Utility\Xss::filter(), * \Drupal\Component\Utility\Xss::filter(),
* \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate * \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate
* scrubbing functions before displaying data to users. * scrubbing functions before displaying data to users.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata|null
* (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 * @return string
* Text with tokens replaced. * Text with tokens replaced.
*/ */
public function replace($text, array $data = array(), array $options = array()) { public function replace($text, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata = NULL) {
$text_tokens = $this->scan($text); $text_tokens = $this->scan($text);
if (empty($text_tokens)) { if (empty($text_tokens)) {
return $text; return $text;
} }
$bubbleable_metadata_is_passed_in = (bool) $bubbleable_metadata;
$bubbleable_metadata = $bubbleable_metadata ?: new BubbleableMetadata();
$replacements = array(); $replacements = array();
foreach ($text_tokens as $type => $tokens) { foreach ($text_tokens as $type => $tokens) {
$replacements += $this->generate($type, $tokens, $data, $options); $replacements += $this->generate($type, $tokens, $data, $options, $bubbleable_metadata);
if (!empty($options['clear'])) { if (!empty($options['clear'])) {
$replacements += array_fill_keys($tokens, ''); $replacements += array_fill_keys($tokens, '');
} }
...@@ -173,12 +207,20 @@ public function replace($text, array $data = array(), array $options = array()) ...@@ -173,12 +207,20 @@ public function replace($text, array $data = array(), array $options = array())
// Optionally alter the list of replacement values. // Optionally alter the list of replacement values.
if (!empty($options['callback'])) { if (!empty($options['callback'])) {
$function = $options['callback']; $function = $options['callback'];
$function($replacements, $data, $options); $function($replacements, $data, $options, $bubbleable_metadata);
} }
$tokens = array_keys($replacements); $tokens = array_keys($replacements);
$values = array_values($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); return str_replace($tokens, $values, $text);
} }
...@@ -226,14 +268,14 @@ public function scan($text) { ...@@ -226,14 +268,14 @@ public function scan($text) {
* An array of tokens to be replaced, keyed by the literal text of the token * An array of tokens to be replaced, keyed by the literal text of the token
* as it appeared in the source text. * as it appeared in the source text.
* @param array $data * @param array $data
* (optional) An array of keyed objects. For simple replacement scenarios * An array of keyed objects. For simple replacement scenarios: 'node',
* 'node', 'user', and others are common keys, with an accompanying node or * 'user', and others are common keys, with an accompanying node or user
* user object being the value. Some token types, like 'site', do not require * 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 * any explicit information from $data and can be replaced even if it is
* empty. * empty.
* @param array $options * @param array $options
* (optional) A keyed array of settings and flags to control the token * A keyed array of settings and flags to control the token replacement
* replacement process. Supported options are: * process. Supported options are:
* - langcode: A language code to be used when generating locale-sensitive * - langcode: A language code to be used when generating locale-sensitive
* tokens. * tokens.
* - callback: A callback function that will be used to post-process the * - callback: A callback function that will be used to post-process the
...@@ -245,6 +287,9 @@ public function scan($text) { ...@@ -245,6 +287,9 @@ public function scan($text) {
* responsibility for running \Drupal\Component\Utility\Xss::filter(), * responsibility for running \Drupal\Component\Utility\Xss::filter(),
* \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate * \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate
* scrubbing functions before displaying data to users. * scrubbing functions before displaying data to users.
* @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 * @return array
* An associative array of replacement values, keyed by the original 'raw' * An associative array of replacement values, keyed by the original 'raw'
...@@ -254,9 +299,16 @@ public function scan($text) { ...@@ -254,9 +299,16 @@ public function scan($text) {
* @see hook_tokens() * @see hook_tokens()
* @see hook_tokens_alter() * @see hook_tokens_alter()
*/ */
public function generate($type, array $tokens, array $data = array(), array $options = array()) { public function generate($type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$options += array('sanitize' => TRUE); $options += array('sanitize' => TRUE);
$replacements = $this->moduleHandler->invokeAll('tokens', array($type, $tokens, $data, $options));
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. // Allow other modules to alter the replacements.
$context = array( $context = array(
...@@ -265,7 +317,7 @@ public function generate($type, array $tokens, array $data = array(), array $opt ...@@ -265,7 +317,7 @@ public function generate($type, array $tokens, array $data = array(), array $opt
'data' => $data, 'data' => $data,
'options' => $options, 'options' => $options,
); );
$this->moduleHandler->alter('tokens', $replacements, $context); $this->moduleHandler->alter('tokens', $replacements, $context, $bubbleable_metadata);
return $replacements; return $replacements;
} }
......
...@@ -34,22 +34,43 @@ ...@@ -34,22 +34,43 @@
* An array of tokens to be replaced. The keys are the machine-readable token * An array of tokens to be replaced. The keys are the machine-readable token
* names, and the values are the raw [type:token] strings that appeared in the * names, and the values are the raw [type:token] strings that appeared in the
* original text. * original text.
* @param $data * @param array $data
* (optional) An associative array of data objects to be used when generating * An associative array of data objects to be used when generating replacement
* replacement values, as supplied in the $data parameter to * values, as supplied in the $data parameter to
* \Drupal\Core\Utility\Token::replace(). * \Drupal\Core\Utility\Token::replace().
* @param $options * @param array $options
* (optional) An associative array of options for token replacement; see * An associative array of options for token replacement; see
* \Drupal\Core\Utility\Token::replace() for possible values. * \Drupal\Core\Utility\Token::replace() for possible values.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
* The bubbleable metadata. Prior to invoking this hook,
* \Drupal\Core\Utility\Token::generate() collects metadata for all of the
* data objects in $data. For any data sources not in $data, but that are
* used by the token replacement logic, such as global configuration (e.g.,
* 'system.site') and related objects (e.g., $node->getOwner()),
* implementations of this hook must add the corresponding metadata.
* For example:
* @code
* $bubbleable_metadata->addCacheableDependency(\Drupal::config('system.site'));
* $bubbleable_metadata->addCacheableDependency($node->getOwner());
* @endcode
* *
* @return * Additionally, implementations of this hook, must forward
* $bubbleable_metadata to the chained tokens that they invoke.
* For example:
* @code
* if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
* $replacements = $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata);
* }
* @endcode
*
* @return array
* An associative array of replacement values, keyed by the raw [type:token] * An associative array of replacement values, keyed by the raw [type:token]
* strings from the original text. * strings from the original text.
* *
* @see hook_token_info() * @see hook_token_info()
* @see hook_tokens_alter() * @see hook_tokens_alter()
*/ */
function hook_tokens($type, $tokens, array $data = array(), array $options = array()) { function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token(); $token_service = \Drupal::token();
$url_options = array('absolute' => TRUE); $url_options = array('absolute' => TRUE);
...@@ -87,6 +108,7 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr ...@@ -87,6 +108,7 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr
case 'author': case 'author':
$account = $node->getOwner() ? $node->getOwner() : User::load(0); $account = $node->getOwner() ? $node->getOwner() : User::load(0);
$replacements[$original] = $sanitize ? SafeMarkup::checkPlain($account->label()) : $account->label(); $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($account->label()) : $account->label();
$bubbleable_metadata->addCacheableDependency($account);
break; break;
case 'created': case 'created':
...@@ -96,11 +118,11 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr ...@@ -96,11 +118,11 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr
} }
if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) { if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
$replacements = $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options); $replacements = $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata);
} }
if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) { if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
$replacements = $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options); $replacements = $token_service->generate('date', $created_tokens, array('date' => $node->getCreatedTime()), $options, $bubbleable_metadata);
} }
} }
...@@ -120,10 +142,14 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr ...@@ -120,10 +142,14 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr
* - 'tokens' * - 'tokens'
* - 'data' * - 'data'
* - 'options' * - 'options'
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
* The bubbleable metadata. In case you alter an existing token based upon
* a data source that isn't in $context['data'], you must add that
* dependency to $bubbleable_metadata.
* *
* @see hook_tokens() * @see hook_tokens()
*/ */
function hook_tokens_alter(array &$replacements, array $context) { function hook_tokens_alter(array &$replacements, array $context, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
$options = $context['options']; $options = $context['options'];
if (isset($options['langcode'])) { if (isset($options['langcode'])) {
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Render\BubbleableMetadata;
/** /**
* Implements hook_token_info(). * Implements hook_token_info().
...@@ -105,7 +107,7 @@ function comment_token_info() { ...@@ -105,7 +107,7 @@ function comment_token_info() {
/** /**
* Implements hook_tokens(). * Implements hook_tokens().
*/ */
function comment_tokens($type, $tokens, array $data = array(), array $options = array()) { function comment_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$token_service = \Drupal::token(); $token_service = \Drupal::token();
$url_options = array('absolute' => TRUE); $url_options = array('absolute' => TRUE);
...@@ -138,6 +140,11 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = ...@@ -138,6 +140,11 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
case 'mail': case 'mail':
$mail = $comment->getAuthorEmail(); $mail = $comment->getAuthorEmail();
// Add the user cacheability metadata in case the author of the comment
// is not the anonymous user.
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
$replacements[$original] = $sanitize ? SafeMarkup::checkPlain($mail) : $mail; $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($mail) : $mail;
break; break;
...@@ -170,26 +177,37 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = ...@@ -170,26 +177,37 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
case 'author': case 'author':
$name = $comment->getAuthorName(); $name = $comment->getAuthorName();
// Add the user cacheability metadata in case the author of the comment
// is not the anonymous user.
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
$replacements[$original] = $sanitize ? Xss::filter($name) : $name; $replacements[$original] = $sanitize ? Xss::filter($name) : $name;
break; break;
case 'parent': case 'parent':
if ($comment->hasParentComment()) { if ($comment->hasParentComment()) {
$parent = $comment->getParentComment(); $parent = $comment->getParentComment();
$bubbleable_metadata->addCacheableDependency($parent);
$replacements[$original] = $sanitize ? Xss::filter($parent->getSubject()) : $parent->getSubject(); $replacements[$original] = $sanitize ? Xss::filter($parent->getSubject()) : $parent->getSubject();
} }
break; break;
case 'created': case 'created':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = format_date($comment->getCreatedTime(), 'medium', '', NULL, $langcode); $replacements[$original] = format_date($comment->getCreatedTime(), 'medium', '', NULL, $langcode);
break; break;
case 'changed': case 'changed':
$date_format = DateFormat::load('medium');
$bubbleable_metadata->addCacheableDependency($date_format);
$replacements[$original] = format_date($comment->getChangedTime(), 'medium', '', NULL, $langcode); $replacements[$original] = format_date($comment->getChangedTime(), 'medium', '', NULL, $langcode);
break; break;
case 'entity': case 'entity':
$entity = $comment->getCommentedEntity(); $entity = $comment->getCommentedEntity();
$bubbleable_metadata->addCacheableDependency($entity);
$title = $entity->label(); $title = $entity->label();
$replacements[$original] = $sanitize ? Xss::filter($title) : $title; $replacements[$original] = $sanitize ? Xss::filter($title) : $title;
break; break;
...@@ -199,23 +217,23 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = ...@@ -199,23 +217,23 @@ function comment_tokens($type, $tokens, array $data = array(), array $options =
// Chained token relationships. // Chained token relationships.
if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) { if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) {
$entity = $comment->getCommentedEntity(); $entity = $comment->getCommentedEntity();
$replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, array($comment->getCommentedEntityTypeId() => $entity), $options); $replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, array($comment->getCommentedEntityTypeId() => $entity), $options, $bubbleable_metadata);
} }
if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) { if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) {
$replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getCreatedTime()), $options); $replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getCreatedTime()), $options, $bubbleable_metadata);
} }
if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) { if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) {
$replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getChangedTime()), $options); $replacements += $token_service->generate('date', $date_tokens, array('date' => $comment->getChangedTime()), $options, $bubbleable_metadata);
} }
if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && $parent = $comment->getParentComment()) { if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && $parent = $comment->getParentComment()) {
$replacements += $token_service->generate('comment', $parent_tokens, array('comment' => $parent), $options); $replacements += $token_service->generate('comment', $parent_tokens, array('comment' => $parent), $options, $bubbleable_metadata);
} }
if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && $account = $comment->getOwner()) { if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && $account = $comment->getOwner()) {
$replacements += $token_service->generate('user', $author_tokens, array('user' => $account), $options); $replacements += $token_service->generate('user', $author_tokens, array('user' => $account), $options, $bubbleable_metadata);
} }
} }
elseif ($type == 'entity' & !empty($data['entity'])) { elseif ($type == 'entity' & !empty($data['entity'])) {
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\comment\Entity\Comment; use Drupal\comment\Entity\Comment;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\node\Entity\Node; use Drupal\node\Entity\Node;
/** /**
...@@ -60,6 +61,7 @@ function testCommentTokenReplacement() { ...@@ -60,6 +61,7 @@ function testCommentTokenReplacement() {
$tests['[comment:langcode]'] = SafeMarkup::checkPlain($comment->language()->getId()); $tests['[comment:langcode]'] = SafeMarkup::checkPlain($comment->language()->getId());
$tests['[comment:url]'] = $comment->url('canonical', $url_options + array('fragment' => 'comment-' . $comment->id())); $tests['[comment:url]'] = $comment->url('canonical', $url_options + array('fragment' => 'comment-' . $comment->id()));
$tests['[comment:edit-url]'] = $comment->url('edit-form', $url_options); $tests['[comment:edit-url]'] = $comment->url('edit-form', $url_options);
$tests['[comment:created]'] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', array('langcode' => $language_interface->getId()));
$tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getCreatedTime(), array('langcode' => $language_interface->getId())); $tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getCreatedTime(), array('langcode' => $language_interface->getId()));
$tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getChangedTimeAcrossTranslations(), array('langcode' => $language_interface->getId())); $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getChangedTimeAcrossTranslations(), array('langcode' => $language_interface->getId()));
$tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL; $tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL;
...@@ -71,12 +73,48 @@ function testCommentTokenReplacement() { ...@@ -71,12 +73,48 @@ function testCommentTokenReplacement() {
$tests['[comment:author:uid]'] = $comment->getOwnerId(); $tests['[comment:author:uid]'] = $comment->getOwnerId();
$tests['[comment:author:name]'] = SafeMarkup::checkPlain($this->adminUser->getUsername()); $tests['[comment:author:name]'] = SafeMarkup::checkPlain($this->adminUser->getUsername());
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($comment);
$metadata_tests = [];
$metadata_tests['[comment:cid]'] = $base_bubbleable_metadata;
$metadata_tests['[comment:hostname]'] = $base_bubbleable_metadata;
$bubbleable_metadata = clone $base_bubbleable_metadata;
$bubbleable_metadata->addCacheableDependency($this->adminUser);
$metadata_tests['[comment:author]'] = $bubbleable_metadata;