From 1a0cdcd32c051fd653d40ee88579511c36c4b4b8 Mon Sep 17 00:00:00 2001
From: effulgentsia <alex.bronstein@acquia.com>
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 045a02e35b88..b83231423810 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 8e8a57816e37..33d5afbb9eeb 100644
--- a/core/lib/Drupal/Core/Cache/CacheableMetadata.php
+++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php
@@ -132,6 +132,35 @@ public function setCacheMaxAge($max_age) {
     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 81e7237ec555..a8a71d788e24 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -94,6 +94,19 @@ public static function createFromObject($object) {
     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 075abb58da45..9966726e5079 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 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend
    *     \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 @@ public function replace($text, array $data = array(), array $options = array())
     // 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 @@ public function scan($text) {
    *   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 @@ public function scan($text) {
    *     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 @@ public function scan($text) {
    * @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 @@ public function generate($type, array $tokens, array $data = array(), array $opt
       '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 51133de50d06..d2d87edf0037 100644
--- a/core/lib/Drupal/Core/Utility/token.api.php
+++ b/core/lib/Drupal/Core/Utility/token.api.php
@@ -34,22 +34,43 @@
  *   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 1d20b112e100..3a73f39b6912 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 3c6b48a3db5c..c1f4303a2487 100644
--- a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
+++ b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
@@ -10,6 +10,7 @@
 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 @@ function testCommentTokenReplacement() {
     $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 @@ function testCommentTokenReplacement() {
     $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 2458582dcafd..a995916780bb 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 f5f478a83e2b..2d289c4f3581 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 @@ function testFileTokenReplacement() {
     $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 7f8d50a85017..1bba0cc4bd65 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 f4f7d268be2d..9bfbdb44e955 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 @@ function testNodeTokenReplacement() {
     $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 02999a637395..ba8438f23ca4 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -260,14 +260,17 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches) {
       '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 d2be86c20bb0..e9d8ded43926 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 f98c3735faa4..b8f31e765a56 100644
--- a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
+++ b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
@@ -9,6 +9,7 @@
 
 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 @@ public function testSystemSiteTokenReplacement() {
     $tests['[site:url-brief]'] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('<front>', [], $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 @@ public function testSystemSiteTokenReplacement() {
     $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 @@ public function testSystemSiteTokenReplacement() {
     // 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 000000000000..7e9d60fe8988
--- /dev/null
+++ b/core/modules/system/src/Tests/System/TokenReplaceWebTest.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\System\TokenReplaceWebTest.
+ */
+
+namespace Drupal\system\Tests\System;
+
+use Drupal\simpletest\WebTestBase;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
+
+/**
+ * Tests the token system integration.
+ *
+ * @group system
+ */
+class TokenReplaceWebTest extends WebTestBase {
+
+  use AssertPageCacheContextsAndTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['token_test', 'filter', 'node'];
+
+  /**
+   * Tests a token replacement on an actual website.
+   */
+  public function testTokens() {
+    $node = $this->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 20d5ec17955b..4c0e107af1a0 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('<front>', array(), $url_options);
+          /** @var \Drupal\Core\GeneratedUrl $result */
+          $result = \Drupal::url('<front>', array(), $url_options, TRUE);
+          $bubbleable_metadata->addCacheableDependency($result);
+          $replacements[$original] = $result->getGeneratedUrl();
           break;
 
         case 'url-brief':
-          $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('<front>', array(), $url_options));
+          /** @var \Drupal\Core\GeneratedUrl $result */
+          $result = \Drupal::url('<front>', 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 000000000000..76469af711fc
--- /dev/null
+++ b/core/modules/system/tests/modules/token_test/src/Controller/TestController.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\token_test\Controller\TestController.
+ */
+
+namespace Drupal\token_test\Controller;
+
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Utility\Token;
+use Drupal\node\NodeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a test controller for token replacement.
+ */
+class TestController extends ControllerBase {
+
+  /**
+   * The token replacement system.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * Constructs a new TestController instance.
+   *
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token replacement system.
+   */
+  public function __construct(Token $token) {
+    $this->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 000000000000..e243f95d1491
--- /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 000000000000..5fc239ad4918
--- /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 10512018500e..ff7c8eff4065 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -10,6 +10,7 @@
 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 @@ function testTaxonomyTokenReplacement() {
     $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 f5ba24ebd4a5..824116cccf7e 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 b2dfe6da6fa1..197684a125a8 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 @@ function testUserTokenReplacement() {
     $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 0439015e572d..582c1cd26270 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 ad00b7ed1164..6410c90392d6 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 @@ function testTokenReplacement() {
       '[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 1e0540b5da60..eb766bcf6369 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 f2dbd3e28422..05b51aad5e62 100644
--- a/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Cache/CacheableMetadataTest.php
@@ -42,6 +42,28 @@ public function testMerge(CacheableMetadata $a, CacheableMetadata $b, CacheableM
     $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 52b6ef5033b5..e695c79239fa 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -648,4 +648,52 @@ public function providerTestMergeAttachmentsHttpHeaderMerging() {
     ];
   }
 
+
+  /**
+   * @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 0a9a570fcd4e..a738282a401f 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 @@ protected function setUp() {
 
     $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 @@ public function testGetInfo() {
     $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
    */
-- 
GitLab