Change record status: 
Project: 
Introduced in branch: 
8.0.x
Introduced in version: 
8.0.0-beta13
Description: 

Prior to 8.0.0-beta13

Prior to 8.0.0-beta13, token replacement did not take into account cacheability metadata.

For example a custom controller could include a node token such as [node:title] in its markup without the cacheability metadata for that markup/page taking into account the node that was passed, even though the resulting page really does depend on the cacheability of the node (if the node's title is modified, then the page needs to be invalidated to remain correct).

Therefore this would have lead to stale caches or even security issues.

hook_tokens() implementations

function mymodule_tokens($type, $tokens, array $data = array(), array $options = array()) {
  // ...
           case 'author':
           $account = $node->getOwner() ? $node->getOwner() : User::load(0);
           $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
           break;

     if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
       $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options);
     }
}
Calls to Token::replace()

class SomeController extends ControllerBase {

  function fooBar(NodeInterface $node) {
    $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node]);
    // This render array doesn't convey that it should only be cached until $node is updated
    // and is only relevant for the current user.
    return $build;
  }

}

After 8.0.0-beta13

Callers of \Drupal\Core\Utility\Token::replace() should pass along a new Drupal\Core\Render\BubbleableMetadata object. The Token API will set the required metadata on the passed object which can then be applied to the built render array.

(BubbleableMetadata was chosen instead of CacheableMetadata, because besides cacheability metadata, some token replacements may want to use placeholders, which requires the use of BubbleableMetadata, so placeholders can bubble up.)

If you don't pass along a BubbleableMetadata object to collect bubbleable metadata for the generated tokens, the bubbleable metadata will instead be automatically bubbled onto the render context (if the token replacement happens as part of rendering).

hook_tokens() implementations
use Drupal\Core\Render\BubbleableMetadata;

function mymodule_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {

         case 'author':
           $account = $node->getOwner() ? $node->getOwner() : User::load(0);
           $bubbleable_metadata->addCacheableDependency($account);
           $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
           break;

     if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
       $replacements += $token_service->generate('user', $author_tokens, array('user' => $node->getOwner()), $options, $bubbleable_metadata);
     }
How tokens get the right bubbleable (cacheability + attachments, which includes placeholders) metadata:
  1. The associative array of data objects $data: data objects implementing CacheableDependencyInterface or AttachmentsInterface automatically have their bubbleable metadata (again: cacheability plus attachments, which includes placeholders) added to all tokens resulting from that hook_tokens() implementation
  2. "Related objects" tokens, such as [node:author:*] tokens, call the token service's generate() method with a new $data array containing the related data objects, which also implement CacheableDependencyInterface. In the example of author tokens: $node->getOwner() returns a User entity, which also implements CacheableDependencyInterface. (See the above code sample.)
  3. Finally, for global tokens, such as [site:name], it is necessary to explicitly load the config object, and add it to $bubbleable_metadata, for example:
          switch ($name) {
            case 'name':
              $config = \Drupal::config('system.site');
              $bubbleable_metadata->addCacheableDependency($config);
              $site_name = $config->get('name');
              $replacements[$original] = $sanitize ? Html::escape($site_name) : $site_name;
              break;
    
    
Calls to Token::replace()

use Drupal\Core\Render\BubbleableMetadata;

class SomeController extends ControllerBase {

  public function tokenReplace(NodeInterface $node) {
    // Create a BubbleableMetadata object to collect cacheability metadata.
    $bubbleable_metadata = new BubbleableMetadata();

    // Pass it to Token::replace().
    $build['#markup'] = $this->token->replace('Tokens: [node:nid] [current-user:uid]', ['node' => $node], [], $bubbleable_metadata);

    // At this point, $bubbleable_metadata contains the cache tags 'node:5' and 'user:3',
    // assuming node 5 was passed and the current user is user 3.

    // This merges those 'node:5' and 'user:3' cache tags onto the $build render array.
    $bubbleable_metadata->applyTo($build);

    return $build;
  }

}
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done