From 1a0cdcd32c051fd653d40ee88579511c36c4b4b8 Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Wed, 22 Jul 2015 07:16:01 -0700 Subject: [PATCH] =?UTF-8?q?Issue=20#2525910=20by=20dawehner,=20effulgentsi?= =?UTF-8?q?a,=20Berdir,=20lauriii,=20larowlan,=20timmillwood,=20Wim=20Leer?= =?UTF-8?q?s,=20chx,=20arlinsandbulte,=20Fabianx,=20G=C3=A1bor=20Hojtsy,?= =?UTF-8?q?=20Dave=20Reid,=20alexpott,=20catch:=20Ensure=20token=20replace?= =?UTF-8?q?ments=20have=20cacheability=20+=20attachments=20metadata=20and?= =?UTF-8?q?=20that=20it=20is=20bubbled=20in=20any=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/core.services.yml | 2 +- .../Drupal/Core/Cache/CacheableMetadata.php | 29 ++++ .../Drupal/Core/Render/BubbleableMetadata.php | 13 ++ core/lib/Drupal/Core/Utility/Token.php | 76 ++++++++-- core/lib/Drupal/Core/Utility/token.api.php | 48 +++++-- core/modules/comment/comment.tokens.inc | 30 +++- .../src/Tests/CommentTokenReplaceTest.php | 40 +++++- core/modules/file/file.module | 18 ++- .../file/src/Tests/FileTokenReplaceTest.php | 22 ++- core/modules/node/node.tokens.inc | 15 +- .../node/src/Tests/NodeTokenReplaceTest.php | 26 +++- .../node/src/Tests/Views/FrontPageTest.php | 7 +- core/modules/statistics/statistics.tokens.inc | 6 +- .../src/Tests/System/TokenReplaceUnitTest.php | 22 ++- .../src/Tests/System/TokenReplaceWebTest.php | 46 ++++++ core/modules/system/system.tokens.inc | 41 +++++- .../src/Controller/TestController.php | 86 ++++++++++++ .../modules/token_test/token_test.info.yml | 8 ++ .../modules/token_test/token_test.routing.yml | 12 ++ .../taxonomy/src/Tests/TokenReplaceTest.php | 19 ++- core/modules/taxonomy/taxonomy.tokens.inc | 9 +- .../user/src/Tests/UserTokenReplaceTest.php | 40 +++++- core/modules/user/user.tokens.inc | 19 ++- .../views/src/Tests/TokenReplaceTest.php | 19 ++- core/modules/views/views.tokens.inc | 17 +-- .../Core/Cache/CacheableMetadataTest.php | 22 +++ .../Core/Render/BubbleableMetadataTest.php | 48 +++++++ .../Drupal/Tests/Core/Utility/TokenTest.php | 131 +++++++++++++++++- 28 files changed, 792 insertions(+), 79 deletions(-) create mode 100644 core/modules/system/src/Tests/System/TokenReplaceWebTest.php create mode 100644 core/modules/system/tests/modules/token_test/src/Controller/TestController.php create mode 100644 core/modules/system/tests/modules/token_test/token_test.info.yml create mode 100644 core/modules/system/tests/modules/token_test/token_test.routing.yml diff --git a/core/core.services.yml b/core/core.services.yml index 045a02e35b8..b8323142381 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1185,7 +1185,7 @@ services: - { name: service_collector, tag: breadcrumb_builder, call: addBuilder } 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: class: Drupal\Core\Batch\BatchStorage arguments: ['@database', '@session', '@csrf_token'] diff --git a/core/lib/Drupal/Core/Cache/CacheableMetadata.php b/core/lib/Drupal/Core/Cache/CacheableMetadata.php index 8e8a57816e3..33d5afbb9ee 100644 --- a/core/lib/Drupal/Core/Cache/CacheableMetadata.php +++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php @@ -132,6 +132,35 @@ class CacheableMetadata implements CacheableDependencyInterface { 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. * diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php index 81e7237ec55..a8a71d788e2 100644 --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -94,6 +94,19 @@ class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterfa 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). * diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php index 075abb58da4..9966726e507 100644 --- a/core/lib/Drupal/Core/Utility/Token.php +++ b/core/lib/Drupal/Core/Utility/Token.php @@ -8,11 +8,15 @@ namespace Drupal\Core\Utility; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageInterface; 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. @@ -104,6 +108,13 @@ class Token { */ protected $cacheTagsInvalidator; + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + /** * Constructs a new class instance. * @@ -115,12 +126,15 @@ class Token { * 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) { + 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; } /** @@ -152,19 +166,39 @@ class Token { * \Drupal\Component\Utility\Xss::filter(), * \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate * 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 * 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); if (empty($text_tokens)) { return $text; } + $bubbleable_metadata_is_passed_in = (bool) $bubbleable_metadata; + $bubbleable_metadata = $bubbleable_metadata ?: new BubbleableMetadata(); + $replacements = array(); 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'])) { $replacements += array_fill_keys($tokens, ''); } @@ -173,12 +207,20 @@ class Token { // Optionally alter the list of replacement values. if (!empty($options['callback'])) { $function = $options['callback']; - $function($replacements, $data, $options); + $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); } @@ -226,14 +268,14 @@ class Token { * 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 - * (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 + * 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: + * 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 @@ -245,6 +287,9 @@ class Token { * responsibility for running \Drupal\Component\Utility\Xss::filter(), * \Drupal\Component\Utility\SafeMarkup::checkPlain() or other appropriate * 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 * An associative array of replacement values, keyed by the original 'raw' @@ -254,9 +299,16 @@ class Token { * @see hook_tokens() * @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); - $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. $context = array( @@ -265,7 +317,7 @@ class Token { 'data' => $data, 'options' => $options, ); - $this->moduleHandler->alter('tokens', $replacements, $context); + $this->moduleHandler->alter('tokens', $replacements, $context, $bubbleable_metadata); return $replacements; } diff --git a/core/lib/Drupal/Core/Utility/token.api.php b/core/lib/Drupal/Core/Utility/token.api.php index 51133de50d0..d2d87edf003 100644 --- a/core/lib/Drupal/Core/Utility/token.api.php +++ b/core/lib/Drupal/Core/Utility/token.api.php @@ -34,22 +34,43 @@ use Drupal\user\Entity\User; * 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 * original text. - * @param $data - * (optional) An associative array of data objects to be used when generating - * replacement values, as supplied in the $data parameter to - * \Drupal\Core\Utility\Token::replace(). - * @param $options - * (optional) An associative array of options for token replacement; see + * @param array $data + * An associative array of data objects to be used when generating replacement + * values, as supplied in the $data parameter to + * \Drupal\Core\Utility\Token::replace(). + * @param array $options + * An associative array of options for token replacement; see * \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] * strings from the original text. * * @see hook_token_info() * @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(); $url_options = array('absolute' => TRUE); @@ -87,6 +108,7 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr case 'author': $account = $node->getOwner() ? $node->getOwner() : User::load(0); $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($account->label()) : $account->label(); + $bubbleable_metadata->addCacheableDependency($account); break; case 'created': @@ -96,11 +118,11 @@ function hook_tokens($type, $tokens, array $data = array(), array $options = arr } 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')) { - $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 * - 'tokens' * - 'data' * - '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() */ -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']; if (isset($options['langcode'])) { diff --git a/core/modules/comment/comment.tokens.inc b/core/modules/comment/comment.tokens.inc index 1d20b112e10..3a73f39b691 100644 --- a/core/modules/comment/comment.tokens.inc +++ b/core/modules/comment/comment.tokens.inc @@ -7,6 +7,8 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; +use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\Core\Render\BubbleableMetadata; /** * Implements hook_token_info(). @@ -105,7 +107,7 @@ function comment_token_info() { /** * 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(); $url_options = array('absolute' => TRUE); @@ -138,6 +140,11 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = case 'mail': $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; break; @@ -170,26 +177,37 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = case 'author': $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; break; case 'parent': if ($comment->hasParentComment()) { $parent = $comment->getParentComment(); + $bubbleable_metadata->addCacheableDependency($parent); $replacements[$original] = $sanitize ? Xss::filter($parent->getSubject()) : $parent->getSubject(); } break; case 'created': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($comment->getCreatedTime(), 'medium', '', NULL, $langcode); break; case 'changed': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($comment->getChangedTime(), 'medium', '', NULL, $langcode); break; case 'entity': $entity = $comment->getCommentedEntity(); + $bubbleable_metadata->addCacheableDependency($entity); $title = $entity->label(); $replacements[$original] = $sanitize ? Xss::filter($title) : $title; break; @@ -199,23 +217,23 @@ function comment_tokens($type, $tokens, array $data = array(), array $options = // Chained token relationships. if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) { $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')) { - $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')) { - $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()) { - $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()) { - $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'])) { diff --git a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php index 3c6b48a3db5..c1f4303a248 100644 --- a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php +++ b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php @@ -10,6 +10,7 @@ namespace Drupal\comment\Tests; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; use Drupal\comment\Entity\Comment; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\node\Entity\Node; /** @@ -60,6 +61,7 @@ class CommentTokenReplaceTest extends CommentTestBase { $tests['[comment:langcode]'] = SafeMarkup::checkPlain($comment->language()->getId()); $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: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:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getChangedTimeAcrossTranslations(), array('langcode' => $language_interface->getId())); $tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL; @@ -71,12 +73,48 @@ class CommentTokenReplaceTest extends CommentTestBase { $tests['[comment:author:uid]'] = $comment->getOwnerId(); $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; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $bubbleable_metadata->addCacheableDependency($this->adminUser); + $metadata_tests['[comment:mail]'] = $bubbleable_metadata; + $metadata_tests['[comment:homepage]'] = $base_bubbleable_metadata; + $metadata_tests['[comment:title]'] = $base_bubbleable_metadata; + $metadata_tests['[comment:body]'] = $base_bubbleable_metadata; + $metadata_tests['[comment:langcode]'] = $base_bubbleable_metadata; + $metadata_tests['[comment:url]'] = $base_bubbleable_metadata; + $metadata_tests['[comment:edit-url]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:created]'] = $bubbleable_metadata->addCacheTags(['rendered']); + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:created:since]'] = $bubbleable_metadata->setCacheMaxAge(0); + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:changed:since]'] = $bubbleable_metadata->setCacheMaxAge(0); + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:parent:cid]'] = $bubbleable_metadata->addCacheTags(['comment:1']); + $metadata_tests['[comment:parent:title]'] = $bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:entity]'] = $bubbleable_metadata->addCacheTags(['node:2']); + // Test node specific tokens. + $metadata_tests['[comment:entity:nid]'] = $bubbleable_metadata; + $metadata_tests['[comment:entity:title]'] = $bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[comment:author:uid]'] = $bubbleable_metadata->addCacheTags(['user:2']); + $metadata_tests['[comment:author:name]'] = $bubbleable_metadata; + // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized comment token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 2458582dcaf..a995916780b 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -6,8 +6,10 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; @@ -938,7 +940,7 @@ function file_file_predelete(File $file) { /** * Implements hook_tokens(). */ -function file_tokens($type, $tokens, array $data = array(), array $options = array()) { +function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $url_options = array('absolute' => TRUE); @@ -987,30 +989,36 @@ function file_tokens($type, $tokens, array $data = array(), array $options = arr // These tokens are default variations on the chained tokens handled below. case 'created': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($file->getCreatedTime(), 'medium', '', NULL, $langcode); break; case 'changed': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($file->getChangedTime(), 'medium', '', NULL, $langcode); break; case 'owner': - $name = $file->getOwner()->label(); + $owner = $file->getOwner(); + $bubbleable_metadata->addCacheableDependency($owner); + $name = $owner->label(); $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($name) : $name; break; } } if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) { - $replacements += $token_service->generate('date', $date_tokens, array('date' => $file->getCreatedTime()), $options); + $replacements += $token_service->generate('date', $date_tokens, array('date' => $file->getCreatedTime()), $options, $bubbleable_metadata); } if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) { - $replacements += $token_service->generate('date', $date_tokens, array('date' => $file->getChangedTime()), $options); + $replacements += $token_service->generate('date', $date_tokens, array('date' => $file->getChangedTime()), $options, $bubbleable_metadata); } if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) { - $replacements += $token_service->generate('user', $owner_tokens, array('user' => $file->getOwner()), $options); + $replacements += $token_service->generate('user', $owner_tokens, array('user' => $file->getOwner()), $options, $bubbleable_metadata); } } diff --git a/core/modules/file/src/Tests/FileTokenReplaceTest.php b/core/modules/file/src/Tests/FileTokenReplaceTest.php index f5f478a83e2..2d289c4f358 100644 --- a/core/modules/file/src/Tests/FileTokenReplaceTest.php +++ b/core/modules/file/src/Tests/FileTokenReplaceTest.php @@ -8,6 +8,7 @@ namespace Drupal\file\Tests; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\file\Entity\File; /** @@ -58,12 +59,31 @@ class FileTokenReplaceTest extends FileFieldTestBase { $tests['[file:owner]'] = SafeMarkup::checkPlain(user_format_name($this->adminUser)); $tests['[file:owner:uid]'] = $file->getOwnerId(); + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($file); + $metadata_tests = []; + $metadata_tests['[file:fid]'] = $base_bubbleable_metadata; + $metadata_tests['[file:name]'] = $base_bubbleable_metadata; + $metadata_tests['[file:path]'] = $base_bubbleable_metadata; + $metadata_tests['[file:mime]'] = $base_bubbleable_metadata; + $metadata_tests['[file:size]'] = $base_bubbleable_metadata; + $metadata_tests['[file:url]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[file:created]'] = $bubbleable_metadata->addCacheTags(['rendered']); + $metadata_tests['[file:created:short]'] = $bubbleable_metadata; + $metadata_tests['[file:changed]'] = $bubbleable_metadata; + $metadata_tests['[file:changed:short]'] = $bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[file:owner]'] = $bubbleable_metadata->addCacheTags(['user:2']); + $metadata_tests['[file:owner:uid]'] = $bubbleable_metadata; + // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_service->replace($input, array('file' => $file), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 7f8d50a8501..1bba0cc4bd6 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -6,7 +6,9 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\user\Entity\User; /** @@ -83,7 +85,7 @@ function node_token_info() { /** * Implements hook_tokens(). */ -function node_tokens($type, $tokens, array $data = array(), array $options = array()) { +function node_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $url_options = array('absolute' => TRUE); @@ -176,29 +178,34 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr // Default values for the chained tokens handled below. case 'author': $account = $node->getOwner() ? $node->getOwner() : User::load(0); + $bubbleable_metadata->addCacheableDependency($account); $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($account->label()) : $account->label(); break; case 'created': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($node->getCreatedTime(), 'medium', '', NULL, $langcode); break; case 'changed': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($node->getChangedTime(), 'medium', '', NULL, $langcode); break; } } 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')) { - $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); } if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) { - $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options); + $replacements += $token_service->generate('date', $changed_tokens, array('date' => $node->getChangedTime()), $options, $bubbleable_metadata); } } diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php index f4f7d268be2..9bfbdb44e95 100644 --- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php +++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php @@ -7,6 +7,7 @@ namespace Drupal\node\Tests; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\system\Tests\System\TokenReplaceUnitTestBase; use Drupal\Component\Utility\SafeMarkup; @@ -76,12 +77,35 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase { $tests['[node:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getCreatedTime(), array('langcode' => $this->interfaceLanguage->getId())); $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId())); + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($node); + + $metadata_tests = []; + $metadata_tests['[node:nid]'] = $base_bubbleable_metadata; + $metadata_tests['[node:vid]'] = $base_bubbleable_metadata; + $metadata_tests['[node:type]'] = $base_bubbleable_metadata; + $metadata_tests['[node:type-name]'] = $base_bubbleable_metadata; + $metadata_tests['[node:title]'] = $base_bubbleable_metadata; + $metadata_tests['[node:body]'] = $base_bubbleable_metadata; + $metadata_tests['[node:summary]'] = $base_bubbleable_metadata; + $metadata_tests['[node:langcode]'] = $base_bubbleable_metadata; + $metadata_tests['[node:url]'] = $base_bubbleable_metadata; + $metadata_tests['[node:edit-url]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[node:author]'] = $bubbleable_metadata->addCacheTags(['user:1']); + $metadata_tests['[node:author:uid]'] = $bubbleable_metadata; + $metadata_tests['[node:author:name]'] = $bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[node:created:since]'] = $bubbleable_metadata->setCacheMaxAge(0); + $metadata_tests['[node:changed:since]'] = $bubbleable_metadata; + // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php index 02999a63739..ba8438f23ca 100644 --- a/core/modules/node/src/Tests/Views/FrontPageTest.php +++ b/core/modules/node/src/Tests/Views/FrontPageTest.php @@ -260,14 +260,17 @@ class FrontPageTest extends ViewTestBase { 'config:views.view.frontpage', 'node_list', ]; + + $render_cache_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags); + $render_cache_tags = Cache::mergeTags($render_cache_tags, ['config:system.site']); $this->assertViewsCacheTags( $view, $empty_node_listing_cache_tags, $do_assert_views_caches, - Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags) + $render_cache_tags ); $expected_tags = Cache::mergeTags($empty_node_listing_cache_tags, $cache_context_tags); - $expected_tags = Cache::mergeTags($expected_tags, ['rendered', 'config:user.role.anonymous']); + $expected_tags = Cache::mergeTags($expected_tags, ['rendered', 'config:user.role.anonymous', 'config:system.site']); $this->assertPageCacheContextsAndTags( Url::fromRoute('view.frontpage.page_1'), $cache_contexts, diff --git a/core/modules/statistics/statistics.tokens.inc b/core/modules/statistics/statistics.tokens.inc index d2be86c20bb..e9d8ded4392 100644 --- a/core/modules/statistics/statistics.tokens.inc +++ b/core/modules/statistics/statistics.tokens.inc @@ -5,6 +5,8 @@ * Builds placeholder replacement tokens for node visitor statistics. */ +use Drupal\Core\Render\BubbleableMetadata; + /** * Implements hook_token_info(). */ @@ -31,7 +33,7 @@ function statistics_token_info() { /** * Implements hook_tokens(). */ -function statistics_tokens($type, $tokens, array $data = array(), array $options = array()) { +function statistics_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $replacements = array(); @@ -56,7 +58,7 @@ function statistics_tokens($type, $tokens, array $data = array(), array $options if ($created_tokens = $token_service->findWithPrefix($tokens, 'last-view')) { $statistics = statistics_get($node->id()); - $replacements += $token_service->generate('date', $created_tokens, array('date' => $statistics['timestamp']), $options); + $replacements += $token_service->generate('date', $created_tokens, array('date' => $statistics['timestamp']), $options, $bubbleable_metadata); } } diff --git a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php index f98c3735faa..b8f31e765a5 100644 --- a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php +++ b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php @@ -9,6 +9,7 @@ namespace Drupal\system\Tests\System; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; +use Drupal\Core\Render\BubbleableMetadata; /** * Generates text using placeholders for dummy content to check token @@ -111,12 +112,25 @@ class TokenReplaceUnitTest extends TokenReplaceUnitTestBase { $tests['[site:url-brief]'] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('', [], $url_options)); $tests['[site:login-url]'] = \Drupal::url('user.page', [], $url_options); + $base_bubbleable_metadata = new BubbleableMetadata(); + + $metadata_tests = []; + $metadata_tests['[site:name]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); + $metadata_tests['[site:slogan]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); + $metadata_tests['[site:mail]'] = BubbleableMetadata::createFromObject(\Drupal::config('system.site')); + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[site:url]'] = $bubbleable_metadata->addCacheContexts(['url.site']); + $metadata_tests['[site:url-brief]'] = $bubbleable_metadata; + $metadata_tests['[site:login-url]'] = $bubbleable_metadata; + // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized system site information token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. @@ -124,7 +138,7 @@ class TokenReplaceUnitTest extends TokenReplaceUnitTestBase { $tests['[site:slogan]'] = $config->get('slogan'); foreach ($tests as $input => $expected) { - $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE)); + $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Unsanitized system site information token %token replaced.', array('%token' => $input))); } @@ -133,10 +147,10 @@ class TokenReplaceUnitTest extends TokenReplaceUnitTestBase { // flag is being passed properly through the call stack and being handled // correctly by a 'known' token, [site:slogan]. $raw_tokens = array('slogan' => '[site:slogan]'); - $generated = $this->tokenService->generate('site', $raw_tokens); + $generated = $this->tokenService->generate('site', $raw_tokens, [], [], $bubbleable_metadata); $this->assertEqual($generated['[site:slogan]'], $safe_slogan, 'Token sanitized.'); - $generated = $this->tokenService->generate('site', $raw_tokens, array(), array('sanitize' => FALSE)); + $generated = $this->tokenService->generate('site', $raw_tokens, array(), array('sanitize' => FALSE), $bubbleable_metadata); $this->assertEqual($generated['[site:slogan]'], $slogan, 'Unsanitized token generated properly.'); } diff --git a/core/modules/system/src/Tests/System/TokenReplaceWebTest.php b/core/modules/system/src/Tests/System/TokenReplaceWebTest.php new file mode 100644 index 00000000000..7e9d60fe898 --- /dev/null +++ b/core/modules/system/src/Tests/System/TokenReplaceWebTest.php @@ -0,0 +1,46 @@ +drupalCreateNode(); + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + + $this->drupalGet('token-test/' . $node->id()); + $this->assertText("Tokens: {$node->id()} {$account->id()}"); + $this->assertCacheTags(['node:1', 'rendered', 'user:2']); + $this->assertCacheContexts(['languages:language_interface', 'theme', 'user']); + + $this->drupalGet('token-test-without-bubleable-metadata/' . $node->id()); + $this->assertText("Tokens: {$node->id()} {$account->id()}"); + $this->assertCacheTags(['node:1', 'rendered', 'user:2']); + $this->assertCacheContexts(['languages:language_interface', 'theme', 'user']); + } + +} diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc index 20d5ec17955..4c0e107af1a 100644 --- a/core/modules/system/system.tokens.inc +++ b/core/modules/system/system.tokens.inc @@ -9,6 +9,8 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; +use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\Core\Render\BubbleableMetadata; /** * Implements hook_token_info(). @@ -87,7 +89,7 @@ function system_token_info() { /** * Implements hook_tokens(). */ -function system_tokens($type, $tokens, array $data = array(), array $options = array()) { +function system_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $url_options = array('absolute' => TRUE); @@ -106,29 +108,44 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a foreach ($tokens as $name => $original) { switch ($name) { case 'name': - $site_name = \Drupal::config('system.site')->get('name'); + $config = \Drupal::config('system.site'); + $bubbleable_metadata->addCacheableDependency($config); + $site_name = $config->get('name'); $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($site_name) : $site_name; break; case 'slogan': - $slogan = \Drupal::config('system.site')->get('slogan'); + $config = \Drupal::config('system.site'); + $bubbleable_metadata->addCacheableDependency($config); + $slogan = $config->get('slogan'); $replacements[$original] = $sanitize ? Xss::filterAdmin($slogan) : $slogan; break; case 'mail': - $replacements[$original] = \Drupal::config('system.site')->get('mail'); + $config = \Drupal::config('system.site'); + $bubbleable_metadata->addCacheableDependency($config); + $replacements[$original] = $config->get('mail'); break; case 'url': - $replacements[$original] = \Drupal::url('', array(), $url_options); + /** @var \Drupal\Core\GeneratedUrl $result */ + $result = \Drupal::url('', array(), $url_options, TRUE); + $bubbleable_metadata->addCacheableDependency($result); + $replacements[$original] = $result->getGeneratedUrl(); break; case 'url-brief': - $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('', array(), $url_options)); + /** @var \Drupal\Core\GeneratedUrl $result */ + $result = \Drupal::url('', array(), $url_options, TRUE); + $bubbleable_metadata->addCacheableDependency($result); + $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', $result->getGeneratedUrl()); break; case 'login-url': - $replacements[$original] = \Drupal::url('user.page', [], $url_options); + /** @var \Drupal\Core\GeneratedUrl $result */ + $result = \Drupal::url('user.page', [], $url_options, TRUE); + $bubbleable_metadata->addCacheableDependency($result); + $replacements[$original] = $result->getGeneratedUrl(); break; } } @@ -137,6 +154,9 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a elseif ($type == 'date') { if (empty($data['date'])) { $date = REQUEST_TIME; + // We depend on the current request time, so the tokens are not cacheable + // at all. + $bubbleable_metadata->setCacheMaxAge(0); } else { $date = $data['date']; @@ -145,19 +165,26 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a foreach ($tokens as $name => $original) { switch ($name) { case 'short': + $date_format = DateFormat::load('short'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($date, 'short', '', NULL, $langcode); break; case 'medium': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($date, 'medium', '', NULL, $langcode); break; case 'long': + $date_format = DateFormat::load('long'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = format_date($date, 'long', '', NULL, $langcode); break; case 'since': $replacements[$original] = \Drupal::service('date.formatter')->formatTimeDiffSince($date, array('langcode' => $langcode)); + $bubbleable_metadata->setCacheMaxAge(0); break; case 'raw': diff --git a/core/modules/system/tests/modules/token_test/src/Controller/TestController.php b/core/modules/system/tests/modules/token_test/src/Controller/TestController.php new file mode 100644 index 00000000000..76469af711f --- /dev/null +++ b/core/modules/system/tests/modules/token_test/src/Controller/TestController.php @@ -0,0 +1,86 @@ +token = $token; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('token')); + } + + /** + * Provides a token replacement with a node as well as the current user. + * + * This controller passes an explicit bubbleable metadata object to + * $this->token->replace(), and applies the collected metadata to the render + * array being built. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return array + * The render array. + */ + public function tokenReplace(NodeInterface $node) { + $bubbleable_metadata = new BubbleableMetadata(); + $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], [], $bubbleable_metadata); + $bubbleable_metadata->applyTo($build); + + return $build; + } + + /** + * Provides a token replacement with a node as well as the current user. + * + * This controller is for testing the token service's fallback behavior of + * applying collected metadata to the currently active render context when an + * explicit bubbleable metadata object isn't passed in. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return array + * The render array. + */ + public function tokenReplaceWithoutPassedBubbleableMetadata(NodeInterface $node) { + $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], []); + + return $build; + } + +} diff --git a/core/modules/system/tests/modules/token_test/token_test.info.yml b/core/modules/system/tests/modules/token_test/token_test.info.yml new file mode 100644 index 00000000000..e243f95d149 --- /dev/null +++ b/core/modules/system/tests/modules/token_test/token_test.info.yml @@ -0,0 +1,8 @@ +name: Token test +type: module +core: 8.x +package: Testing +version: VERSION +dependencies: + - user + - node diff --git a/core/modules/system/tests/modules/token_test/token_test.routing.yml b/core/modules/system/tests/modules/token_test/token_test.routing.yml new file mode 100644 index 00000000000..5fc239ad491 --- /dev/null +++ b/core/modules/system/tests/modules/token_test/token_test.routing.yml @@ -0,0 +1,12 @@ +token_test.test: + path: token-test/{node} + defaults: + _controller: Drupal\token_test\Controller\TestController::tokenReplace + requirements: + _access: 'TRUE' +token_test.test_without_bubbleable_metadata: + path: token-test-without-bubleable-metadata/{node} + defaults: + _controller: Drupal\token_test\Controller\TestController::tokenReplaceWithoutPassedBubbleableMetadata + requirements: + _access: 'TRUE' diff --git a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php index 10512018500..ff7c8eff406 100644 --- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php +++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php @@ -10,6 +10,7 @@ namespace Drupal\taxonomy\Tests; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Render\BubbleableMetadata; /** * Generates text using placeholders for dummy content to check taxonomy token @@ -91,10 +92,26 @@ class TokenReplaceTest extends TaxonomyTestBase { $tests['[term:node-count]'] = 0; $tests['[term:parent:name]'] = '[term:parent:name]'; $tests['[term:vocabulary:name]'] = SafeMarkup::checkPlain($this->vocabulary->label()); + $tests['[term:vocabulary]'] = SafeMarkup::checkPlain($this->vocabulary->label()); + + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1); + + $metadata_tests = array(); + $metadata_tests['[term:tid]'] = $base_bubbleable_metadata; + $metadata_tests['[term:name]'] = $base_bubbleable_metadata; + $metadata_tests['[term:description]'] = $base_bubbleable_metadata; + $metadata_tests['[term:url]'] = $base_bubbleable_metadata; + $metadata_tests['[term:node-count]'] = $base_bubbleable_metadata; + $metadata_tests['[term:parent:name]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[term:vocabulary:name]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); + $metadata_tests['[term:vocabulary]'] = $bubbleable_metadata->addCacheTags($this->vocabulary->getCacheTags()); foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('term' => $term1), array('langcode' => $language_interface->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_service->replace($input, array('term' => $term1), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test sanitized tokens for term2. diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc index f5ba24ebd4a..824116cccf7 100644 --- a/core/modules/taxonomy/taxonomy.tokens.inc +++ b/core/modules/taxonomy/taxonomy.tokens.inc @@ -7,6 +7,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Xss; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\taxonomy\Entity\Vocabulary; /** @@ -92,7 +93,7 @@ function taxonomy_token_info() { /** * Implements hook_tokens(). */ -function taxonomy_tokens($type, $tokens, array $data = array(), array $options = array()) { +function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $replacements = array(); @@ -129,12 +130,14 @@ function taxonomy_tokens($type, $tokens, array $data = array(), array $options = case 'vocabulary': $vocabulary = Vocabulary::load($term->bundle()); + $bubbleable_metadata->addCacheableDependency($vocabulary); $replacements[$original] = SafeMarkup::checkPlain($vocabulary->label()); break; case 'parent': if ($parents = $taxonomy_storage->loadParents($term->id())) { $parent = array_pop($parents); + $bubbleable_metadata->addCacheableDependency($parent); $replacements[$original] = SafeMarkup::checkPlain($parent->getName()); } break; @@ -143,12 +146,12 @@ function taxonomy_tokens($type, $tokens, array $data = array(), array $options = if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'vocabulary')) { $vocabulary = Vocabulary::load($term->bundle()); - $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $options); + $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, array('vocabulary' => $vocabulary), $options, $bubbleable_metadata); } if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent')) && $parents = $taxonomy_storage->loadParents($term->id())) { $parent = array_pop($parents); - $replacements += $token_service->generate('term', $vocabulary_tokens, array('term' => $parent), $options); + $replacements += $token_service->generate('term', $vocabulary_tokens, array('term' => $parent), $options, $bubbleable_metadata); } } diff --git a/core/modules/user/src/Tests/UserTokenReplaceTest.php b/core/modules/user/src/Tests/UserTokenReplaceTest.php index b2dfe6da6fa..197684a125a 100644 --- a/core/modules/user/src/Tests/UserTokenReplaceTest.php +++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php @@ -8,6 +8,7 @@ namespace Drupal\user\Tests; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\User; @@ -66,15 +67,52 @@ class UserTokenReplaceTest extends WebTestBase { $tests['[user:created:short]'] = format_date($account->getCreatedTime(), 'short', '', NULL, $language_interface->getId()); $tests['[current-user:name]'] = SafeMarkup::checkPlain(user_format_name($global_account)); + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($account); + $metadata_tests = []; + $metadata_tests['[user:uid]'] = $base_bubbleable_metadata; + $metadata_tests['[user:name]'] = $base_bubbleable_metadata; + $metadata_tests['[user:mail]'] = $base_bubbleable_metadata; + $metadata_tests['[user:url]'] = $base_bubbleable_metadata; + $metadata_tests['[user:edit-url]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[user:last-login]'] = $bubbleable_metadata->addCacheTags(['rendered']); + $metadata_tests['[user:last-login:short]'] = $bubbleable_metadata; + $metadata_tests['[user:created]'] = $bubbleable_metadata; + $metadata_tests['[user:created:short]'] = $bubbleable_metadata; + $metadata_tests['[current-user:name]'] = $base_bubbleable_metadata->merge(BubbleableMetadata::createFromObject($global_account)->addCacheContexts(['user'])); + // Test to make sure that we generated something for each token. $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.'); foreach ($tests as $input => $expected) { - $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId())); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId()), $bubbleable_metadata); $this->assertEqual($output, $expected, format_string('Sanitized user token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); + } + + // Generate tokens for the anonymous user. + $anonymous_user = User::load(0); + $tests = []; + $tests['[user:uid]'] = t('not yet assigned'); + $tests['[user:name]'] = SafeMarkup::checkPlain(user_format_name($anonymous_user)); + + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($anonymous_user); + $metadata_tests = []; + $metadata_tests['[user:uid]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings')); + $metadata_tests['[user:name]'] = $bubbleable_metadata; + + foreach ($tests as $input => $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_service->replace($input, array('user' => $anonymous_user), array('langcode' => $language_interface->getId()), $bubbleable_metadata); + $this->assertEqual($output, $expected, format_string('Sanitized user token %token replaced.', array('%token' => $input))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]); } // Generate and test unsanitized tokens. + $tests = []; $tests['[user:name]'] = user_format_name($account); $tests['[user:mail]'] = $account->getEmail(); $tests['[current-user:name]'] = user_format_name($global_account); diff --git a/core/modules/user/user.tokens.inc b/core/modules/user/user.tokens.inc index 0439015e572..582c1cd2627 100644 --- a/core/modules/user/user.tokens.inc +++ b/core/modules/user/user.tokens.inc @@ -6,6 +6,8 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\user\Entity\User; /** @@ -64,7 +66,7 @@ function user_token_info() { /** * Implements hook_tokens(). */ -function user_tokens($type, $tokens, array $data = array(), array $options = array()) { +function user_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $token_service = \Drupal::token(); $url_options = array('absolute' => TRUE); @@ -80,6 +82,7 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr $replacements = array(); if ($type == 'user' && !empty($data['user'])) { + /** @var \Drupal\user\UserInterface $account */ $account = $data['user']; foreach ($tokens as $name => $original) { switch ($name) { @@ -91,6 +94,9 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr case 'name': $name = user_format_name($account); + if ($account->isAnonymous()) { + $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings')); + } $replacements[$original] = $sanitize ? SafeMarkup::checkPlain($name) : $name; break; @@ -108,10 +114,14 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr // These tokens are default variations on the chained tokens handled below. case 'last-login': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); $replacements[$original] = $account->getLastLoginTime() ? format_date($account->getLastLoginTime(), 'medium', '', NULL, $langcode) : t('never'); break; case 'created': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); // In the case of user_presave the created date may not yet be set. $replacements[$original] = $account->getCreatedTime() ? format_date($account->getCreatedTime(), 'medium', '', NULL, $langcode) : t('not yet created'); break; @@ -119,17 +129,18 @@ function user_tokens($type, $tokens, array $data = array(), array $options = arr } if ($login_tokens = $token_service->findWithPrefix($tokens, 'last-login')) { - $replacements += $token_service->generate('date', $login_tokens, array('date' => $account->getLastLoginTime()), $options); + $replacements += $token_service->generate('date', $login_tokens, array('date' => $account->getLastLoginTime()), $options, $bubbleable_metadata); } if ($registered_tokens = $token_service->findWithPrefix($tokens, 'created')) { - $replacements += $token_service->generate('date', $registered_tokens, array('date' => $account->getCreatedTime()), $options); + $replacements += $token_service->generate('date', $registered_tokens, array('date' => $account->getCreatedTime()), $options, $bubbleable_metadata); } } if ($type == 'current-user') { $account = User::load(\Drupal::currentUser()->id()); - $replacements += $token_service->generate('user', $tokens, array('user' => $account), $options); + $bubbleable_metadata->addCacheContexts(['user']); + $replacements += $token_service->generate('user', $tokens, array('user' => $account), $options, $bubbleable_metadata); } return $replacements; diff --git a/core/modules/views/src/Tests/TokenReplaceTest.php b/core/modules/views/src/Tests/TokenReplaceTest.php index ad00b7ed116..6410c90392d 100644 --- a/core/modules/views/src/Tests/TokenReplaceTest.php +++ b/core/modules/views/src/Tests/TokenReplaceTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\views\Views; /** @@ -54,9 +55,25 @@ class TokenReplaceTest extends ViewUnitTestBase { '[view:page-count]' => '1', ); + $base_bubbleable_metadata = BubbleableMetadata::createFromObject($view->storage); + $metadata_tests = []; + $metadata_tests['[view:label]'] = $base_bubbleable_metadata; + $metadata_tests['[view:description]'] = $base_bubbleable_metadata; + $metadata_tests['[view:id]'] = $base_bubbleable_metadata; + $metadata_tests['[view:title]'] = $base_bubbleable_metadata; + $metadata_tests['[view:url]'] = $base_bubbleable_metadata; + $metadata_tests['[view:total-rows]'] = $base_bubbleable_metadata; + $metadata_tests['[view:base-table]'] = $base_bubbleable_metadata; + $metadata_tests['[view:base-field]'] = $base_bubbleable_metadata; + $metadata_tests['[view:items-per-page]'] = $base_bubbleable_metadata; + $metadata_tests['[view:current-page]'] = $base_bubbleable_metadata; + $metadata_tests['[view:page-count]'] = $base_bubbleable_metadata; + foreach ($expected as $token => $expected_output) { - $output = $token_handler->replace($token, array('view' => $view)); + $bubbleable_metadata = new BubbleableMetadata(); + $output = $token_handler->replace($token, array('view' => $view), [], $bubbleable_metadata); $this->assertIdentical($output, $expected_output, format_string('Token %token replaced correctly.', array('%token' => $token))); + $this->assertEqual($bubbleable_metadata, $metadata_tests[$token]); } } diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc index 1e0540b5da6..eb766bcf636 100644 --- a/core/modules/views/views.tokens.inc +++ b/core/modules/views/views.tokens.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Render\BubbleableMetadata; /** * Implements hook_token_info(). @@ -68,9 +69,7 @@ function views_token_info() { /** * Implements hook_tokens(). */ -function views_tokens($type, $tokens, array $data = array(), array $options = array()) { - $token_service = \Drupal::token(); - +function views_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $url_options = array('absolute' => TRUE); if (isset($options['language'])) { $url_options['language'] = $options['language']; @@ -83,6 +82,8 @@ function views_tokens($type, $tokens, array $data = array(), array $options = ar /** @var \Drupal\views\ViewExecutable $view */ $view = $data['view']; + $bubbleable_metadata->addCacheableDependency($view->storage); + foreach ($tokens as $name => $original) { switch ($name) { case 'label': @@ -104,7 +105,8 @@ function views_tokens($type, $tokens, array $data = array(), array $options = ar case 'url': if ($url = $view->getUrl()) { - $replacements[$original] = $url->setOptions($url_options)->toString(); + $replacements[$original] = $url->setOptions($url_options) + ->toString(); } break; case 'base-table': @@ -129,13 +131,6 @@ function views_tokens($type, $tokens, array $data = array(), array $options = ar break; } } - - // [view:url:*] nested tokens. This only works if Token module is installed. - if ($url_tokens = $token_service->findWithPrefix($tokens, 'url')) { - if ($path = $view->getUrl()) { - $replacements += $token_service->generate('url', $url_tokens, array('path' => $url->getInternalPath()), $options); - } - } } return $replacements; diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php index f2dbd3e2842..05b51aad5e6 100644 --- a/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php @@ -42,6 +42,28 @@ class CacheableMetadataTest extends UnitTestCase { $this->assertEquals($expected, $a->merge($b)); } + /** + * @covers ::addCacheableDependency + * @dataProvider providerTestMerge + * + * This only tests at a high level, because it reuses existing logic. Detailed + * tests exist for the existing logic: + * + * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeTags() + * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeMaxAges() + * @see \Drupal\Tests\Core\Cache\CacheContextsTest + */ + public function testAddCacheableDependency(CacheableMetadata $a, CacheableMetadata $b, CacheableMetadata $expected) { + $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') + ->disableOriginalConstructor() + ->getMock(); + $container = new ContainerBuilder(); + $container->set('cache_contexts_manager', $cache_contexts_manager); + \Drupal::setContainer($container); + + $this->assertEquals($expected, $a->addCacheableDependency($b)); + } + /** * Provides test data for testMerge(). * diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index 52b6ef5033b..e695c79239f 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -648,4 +648,52 @@ class BubbleableMetadataTest extends UnitTestCase { ]; } + + /** + * @covers ::addCacheableDependency + * @dataProvider providerTestMerge + * + * This only tests at a high level, because it reuses existing logic. Detailed + * tests exist for the existing logic: + * + * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeTags() + * @see \Drupal\Tests\Core\Cache\CacheTest::testMergeMaxAges() + * @see \Drupal\Tests\Core\Cache\CacheContextsTest + */ + public function testAddCacheableDependency(BubbleableMetadata $a, $b, BubbleableMetadata $expected) { + $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') + ->disableOriginalConstructor() + ->getMock(); + $container = new ContainerBuilder(); + $container->set('cache_contexts_manager', $cache_contexts_manager); + \Drupal::setContainer($container); + + $this->assertEquals($expected, $a->addCacheableDependency($b)); + } + + /** + * Provides test data for testMerge(). + * + * @return array + */ + public function providerTestAddCachableDependency() { + return [ + // Merge in a cacheable metadata. + 'merge-cacheable-metadata' => [ + (new BubbleableMetadata())->setCacheContexts(['foo'])->setCacheTags(['foo'])->setCacheMaxAge(20), + (new CacheableMetadata())->setCacheContexts(['bar'])->setCacheTags(['bar'])->setCacheMaxAge(60), + (new BubbleableMetadata())->setCacheContexts(['foo', 'bar'])->setCacheTags(['foo', 'bar'])->setCacheMaxAge(20) + ], + 'merge-bubbleable-metadata' => [ + (new BubbleableMetadata())->setCacheContexts(['foo'])->setCacheTags(['foo'])->setCacheMaxAge(20)->setAttachments(['foo' => []]), + (new BubbleableMetadata())->setCacheContexts(['bar'])->setCacheTags(['bar'])->setCacheMaxAge(60)->setAttachments(['bar' => []]), + (new BubbleableMetadata())->setCacheContexts(['foo', 'bar'])->setCacheTags(['foo', 'bar'])->setCacheMaxAge(20)->setAttachments(['foo' => [], 'bar' => []]) + ], + 'merge-attachments-metadata' => [ + (new BubbleableMetadata())->setAttachments(['foo' => []]), + (new BubbleableMetadata())->setAttachments(['baro' => []]), + (new BubbleableMetadata())->setAttachments(['foo' => [], 'bar' => []]) + ], + ]; + } } diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php index 0a9a570fcd4..a738282a401 100644 --- a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php @@ -7,7 +7,10 @@ namespace Drupal\Tests\Core\Utility; +use Drupal\Core\Cache\Context\CacheContextsManager; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Utility\Token; use Drupal\Tests\UnitTestCase; @@ -59,6 +62,20 @@ class TokenTest extends UnitTestCase { */ protected $cacheTagsInvalidator; + /** + * The cache contexts manager. + * + * @var \Drupal\Core\Cache\Context\CacheContextsManager + */ + protected $cacheContextManager; + + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $renderer; + /** * {@inheritdoc} */ @@ -73,7 +90,17 @@ class TokenTest extends UnitTestCase { $this->cacheTagsInvalidator = $this->getMock('\Drupal\Core\Cache\CacheTagsInvalidatorInterface'); - $this->token = new Token($this->moduleHandler, $this->cache, $this->languageManager, $this->cacheTagsInvalidator); + $this->renderer = $this->getMock('Drupal\Core\Render\RendererInterface'); + + $this->token = new Token($this->moduleHandler, $this->cache, $this->languageManager, $this->cacheTagsInvalidator, $this->renderer); + + $container = new ContainerBuilder(); + $this->cacheContextManager = new CacheContextsManager($container, [ + 'current_user', + 'custom_context' + ]); + $container->set('cache_contexts_manager', $this->cacheContextManager); + \Drupal::setContainer($container); } /** @@ -122,6 +149,108 @@ class TokenTest extends UnitTestCase { $this->token->getInfo(); } + /** + * @covers ::replace + */ + public function testReplaceWithBubbleableMetadataObject() { + $this->moduleHandler->expects($this->any()) + ->method('invokeAll') + ->willReturn(['[node:title]' => 'hello world']); + + $bubbleable_metadata = new BubbleableMetadata(); + $bubbleable_metadata->setCacheContexts(['current_user']); + $bubbleable_metadata->setCacheMaxAge(12); + + $node = $this->prophesize('Drupal\node\NodeInterface'); + $node->getCacheTags()->willReturn(['node:1']); + $node->getCacheContexts()->willReturn(['custom_context']); + $node->getCacheMaxAge()->willReturn(10); + $node = $node->reveal(); + + $result = $this->token->replace('[node:title]', ['node' => $node], [], $bubbleable_metadata); + $this->assertEquals('hello world', $result); + + $this->assertEquals(['node:1'], $bubbleable_metadata->getCacheTags()); + $this->assertEquals([ + 'current_user', + 'custom_context' + ], $bubbleable_metadata->getCacheContexts()); + $this->assertEquals(10, $bubbleable_metadata->getCacheMaxAge()); + } + + /** + * @covers ::replace + */ + public function testReplaceWithHookTokensWithBubbleableMetadata() { + $this->moduleHandler->expects($this->any()) + ->method('invokeAll') + ->willReturnCallback(function ($hook_name, $args) { + $cacheable_metadata = $args[4]; + $cacheable_metadata->addCacheContexts(['custom_context']); + $cacheable_metadata->addCacheTags(['node:1']); + $cacheable_metadata->setCacheMaxAge(10); + + return ['[node:title]' => 'hello world']; + }); + + $node = $this->prophesize('Drupal\node\NodeInterface'); + $node->getCacheContexts()->willReturn([]); + $node->getCacheTags()->willReturn([]); + $node->getCacheMaxAge()->willReturn(14); + $node = $node->reveal(); + + $bubbleable_metadata = new BubbleableMetadata(); + $bubbleable_metadata->setCacheContexts(['current_user']); + $bubbleable_metadata->setCacheMaxAge(12); + + $result = $this->token->replace('[node:title]', ['node' => $node], [], $bubbleable_metadata); + $this->assertEquals('hello world', $result); + $this->assertEquals(['node:1'], $bubbleable_metadata->getCacheTags()); + $this->assertEquals([ + 'current_user', + 'custom_context' + ], $bubbleable_metadata->getCacheContexts()); + $this->assertEquals(10, $bubbleable_metadata->getCacheMaxAge()); + } + + /** + * @covers ::replace + * @covers ::replace + */ + public function testReplaceWithHookTokensAlterWithBubbleableMetadata() { + $this->moduleHandler->expects($this->any()) + ->method('invokeAll') + ->willReturn([]); + + $this->moduleHandler->expects($this->any()) + ->method('alter') + ->willReturnCallback(function ($hook_name, array &$replacements, array $context, BubbleableMetadata $bubbleable_metadata) { + $replacements['[node:title]'] = 'hello world'; + $bubbleable_metadata->addCacheContexts(['custom_context']); + $bubbleable_metadata->addCacheTags(['node:1']); + $bubbleable_metadata->setCacheMaxAge(10); + }); + + $node = $this->prophesize('Drupal\node\NodeInterface'); + $node->getCacheContexts()->willReturn([]); + $node->getCacheTags()->willReturn([]); + $node->getCacheMaxAge()->willReturn(14); + $node = $node->reveal(); + + $bubbleable_metadata = new BubbleableMetadata(); + $bubbleable_metadata->setCacheContexts(['current_user']); + $bubbleable_metadata->setCacheMaxAge(12); + + $result = $this->token->replace('[node:title]', ['node' => $node], [], $bubbleable_metadata); + $this->assertEquals('hello world', $result); + $this->assertEquals(['node:1'], $bubbleable_metadata->getCacheTags()); + $this->assertEquals([ + 'current_user', + 'custom_context' + ], $bubbleable_metadata->getCacheContexts()); + $this->assertEquals(10, $bubbleable_metadata->getCacheMaxAge()); + } + /** * @covers ::resetInfo */