summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2015-10-01 11:45:11 (GMT)
committerAlex Pott2015-10-01 11:45:11 (GMT)
commitde0bbdd9522680f4a7705bc2efbef338881730ce (patch)
tree8ad7b5c38b80e2486b502b33b83091ddf28722fa
parent542a1663cfc802cc3ea004c80921d5f7b907a204 (diff)
Revert "Issue #2567257 by dawehner, stefan.r, effulgentsia, pwolanin, catch, Xano, mr.baileys, Wim Leers, k4v, Dave Reid, chx, googletorp, plach, lauriii, Berdir, webchick, alexpott, stefan.r: hook_tokens() $sanitize option incompatible with Html sanitisation requirements"
This reverts commit 542a1663cfc802cc3ea004c80921d5f7b907a204.
-rw-r--r--core/lib/Drupal/Core/Utility/Token.php37
-rw-r--r--core/lib/Drupal/Core/Utility/token.api.php11
-rw-r--r--core/modules/action/src/Plugin/Action/EmailAction.php3
-rw-r--r--core/modules/action/src/Plugin/Action/MessageAction.php27
-rw-r--r--core/modules/comment/comment.tokens.inc24
-rw-r--r--core/modules/comment/src/Tests/CommentTokenReplaceTest.php43
-rw-r--r--core/modules/file/file.module12
-rw-r--r--core/modules/file/src/Plugin/Field/FieldType/FileItem.php11
-rw-r--r--core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php4
-rw-r--r--core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php4
-rw-r--r--core/modules/node/node.tokens.inc20
-rw-r--r--core/modules/node/src/Tests/NodeTokenReplaceTest.php39
-rw-r--r--core/modules/system/src/Tests/System/TokenReplaceUnitTest.php25
-rw-r--r--core/modules/system/system.module3
-rw-r--r--core/modules/system/system.tokens.inc13
-rw-r--r--core/modules/taxonomy/src/Tests/TokenReplaceTest.php38
-rw-r--r--core/modules/taxonomy/taxonomy.tokens.inc18
-rw-r--r--core/modules/tour/src/Plugin/tour/tip/TipPluginText.php3
-rw-r--r--core/modules/user/src/Tests/UserTokenReplaceTest.php48
-rw-r--r--core/modules/user/user.module7
-rw-r--r--core/modules/user/user.tokens.inc7
-rw-r--r--core/modules/views/src/Plugin/views/area/Entity.php4
-rw-r--r--core/modules/views/views.tokens.inc9
-rw-r--r--core/tests/Drupal/Tests/Core/Utility/TokenTest.php37
24 files changed, 243 insertions, 204 deletions
diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php
index d1033b4..f1f80cc 100644
--- a/core/lib/Drupal/Core/Utility/Token.php
+++ b/core/lib/Drupal/Core/Utility/Token.php
@@ -7,8 +7,6 @@
namespace Drupal\Core\Utility;
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheBackendInterface;
@@ -143,9 +141,7 @@ class Token {
* Replaces all tokens in a given string with appropriate values.
*
* @param string $text
- * An HTML string containing replaceable tokens. The caller is responsible
- * for calling \Drupal\Component\Utility\Html::escape() in case the $text
- * was plain text.
+ * A string potentially containing replaceable tokens.
* @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
@@ -158,9 +154,18 @@ class Token {
* - langcode: A language code to be used when generating locale-sensitive
* tokens.
* - callback: A callback function that will be used to post-process the
- * array of token replacements after they are generated.
+ * array of token replacements after they are generated. For example, a
+ * module using tokens in a text-only email might provide a callback to
+ * strip HTML entities from token values before they are inserted into the
+ * final text.
* - clear: A boolean flag indicating that tokens should be removed from the
* final text if no replacement value can be generated.
+ * - sanitize: A boolean flag indicating that tokens should be sanitized for
+ * display to a web browser. Defaults to TRUE. Developers who set this
+ * option to FALSE assume responsibility for running
+ * \Drupal\Component\Utility\Xss::filter(),
+ * \Drupal\Component\Utility\Html::escape() 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.
@@ -180,13 +185,7 @@ class Token {
* Renderer's currently active render context.
*
* @return string
- * The token result is the entered HTML text with tokens replaced. The
- * caller is responsible for choosing the right escaping / sanitization. If
- * the result is intended to be used as plain text, the usage of
- * PlainTextOutput::renderFromHtml() is recommended. If the result is just
- * printed as part of a template relying on Twig autoescaping is possible,
- * otherwise for example the result can be put into #markup, in which case
- * it would be sanitized by Xss::filterAdmin().
+ * Text with tokens replaced.
*/
public function replace($text, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata = NULL) {
$text_tokens = $this->scan($text);
@@ -205,11 +204,6 @@ class Token {
}
}
- // Escape the tokens, unless they are explicitly markup.
- foreach ($replacements as $token => $value) {
- $replacements[$token] = SafeMarkup::isSafe($value) ? $value : Html::escape($value);
- }
-
// Optionally alter the list of replacement values.
if (!empty($options['callback'])) {
$function = $options['callback'];
@@ -288,6 +282,11 @@ class Token {
* array of token replacements after they are generated. Can be used when
* modules require special formatting of token text, for example URL
* encoding or truncation to a specific length.
+ * - sanitize: A boolean flag indicating that tokens should be sanitized for
+ * display to a web browser. Developers who set this option to FALSE assume
+ * responsibility for running \Drupal\Component\Utility\Xss::filter(),
+ * \Drupal\Component\Utility\Html::escape() 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.
@@ -301,6 +300,8 @@ class Token {
* @see hook_tokens_alter()
*/
public function generate($type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+ $options += array('sanitize' => TRUE);
+
foreach ($data as $object) {
if ($object instanceof CacheableDependencyInterface || $object instanceof AttachmentsInterface) {
$bubbleable_metadata->addCacheableDependency($object);
diff --git a/core/lib/Drupal/Core/Utility/token.api.php b/core/lib/Drupal/Core/Utility/token.api.php
index 4bc619f..57a1e6d 100644
--- a/core/lib/Drupal/Core/Utility/token.api.php
+++ b/core/lib/Drupal/Core/Utility/token.api.php
@@ -5,6 +5,7 @@
* Hooks related to the Token system.
*/
+use Drupal\Component\Utility\Html;
use Drupal\user\Entity\User;
/**
@@ -64,9 +65,7 @@ use Drupal\user\Entity\User;
*
* @return array
* An associative array of replacement values, keyed by the raw [type:token]
- * strings from the original text. The returned values must be either plain
- * text strings, or an object implementing SafeStringInterface if they are
- * HTML-formatted.
+ * strings from the original text.
*
* @see hook_token_info()
* @see hook_tokens_alter()
@@ -82,6 +81,8 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
else {
$langcode = NULL;
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'node' && !empty($data['node'])) {
@@ -96,7 +97,7 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
break;
case 'title':
- $replacements[$original] = $node->getTitle();
+ $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle();
break;
case 'edit-url':
@@ -106,7 +107,7 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
// Default values for the chained tokens handled below.
case 'author':
$account = $node->getOwner() ? $node->getOwner() : User::load(0);
- $replacements[$original] = $account->label();
+ $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
$bubbleable_metadata->addCacheableDependency($account);
break;
diff --git a/core/modules/action/src/Plugin/Action/EmailAction.php b/core/modules/action/src/Plugin/Action/EmailAction.php
index 4c68e1d..252428c 100644
--- a/core/modules/action/src/Plugin/Action/EmailAction.php
+++ b/core/modules/action/src/Plugin/Action/EmailAction.php
@@ -7,7 +7,6 @@
namespace Drupal\action\Plugin\Action;
-use Drupal\Component\Utility\PlainTextOutput;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\EntityManagerInterface;
@@ -128,7 +127,7 @@ class EmailAction extends ConfigurableActionBase implements ContainerFactoryPlug
$this->configuration['node'] = $entity;
}
- $recipient = PlainTextOutput::renderFromHtml($this->token->replace($this->configuration['recipient'], $this->configuration));
+ $recipient = $this->token->replace($this->configuration['recipient'], $this->configuration);
// If the recipient is a registered user with a language preference, use
// the recipient's preferred language. Otherwise, use the system default
diff --git a/core/modules/action/src/Plugin/Action/MessageAction.php b/core/modules/action/src/Plugin/Action/MessageAction.php
index ba41b16..3da24ef 100644
--- a/core/modules/action/src/Plugin/Action/MessageAction.php
+++ b/core/modules/action/src/Plugin/Action/MessageAction.php
@@ -7,11 +7,11 @@
namespace Drupal\action\Plugin\Action;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -33,32 +33,19 @@ class MessageAction extends ConfigurableActionBase implements ContainerFactoryPl
protected $token;
/**
- * The renderer.
- *
- * @var \Drupal\Core\Render\RendererInterface
- */
- protected $renderer;
-
- /**
* Constructs a MessageAction object.
- *
- * @param \Drupal\Core\Utility\Token $token
- * The token replacement service.
- * @param \Drupal\Core\Render\RendererInterface $renderer
- * The renderer.
*/
- public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token, RendererInterface $renderer) {
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->token = $token;
- $this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'), $container->get('renderer'));
+ return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'));
}
/**
@@ -68,12 +55,8 @@ class MessageAction extends ConfigurableActionBase implements ContainerFactoryPl
if (empty($this->configuration['node'])) {
$this->configuration['node'] = $entity;
}
- $message = $this->token->replace($this->configuration['message'], $this->configuration);
- $build = [
- '#markup' => $message,
- ];
-
- drupal_set_message($this->renderer->renderPlain($build));
+ $message = $this->token->replace(Xss::filterAdmin($this->configuration['message']), $this->configuration);
+ drupal_set_message($message);
}
/**
diff --git a/core/modules/comment/comment.tokens.inc b/core/modules/comment/comment.tokens.inc
index e2d606f..1bd179e 100644
--- a/core/modules/comment/comment.tokens.inc
+++ b/core/modules/comment/comment.tokens.inc
@@ -5,7 +5,9 @@
* Builds placeholder replacement tokens for comment-related data.
*/
+use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Render\BubbleableMetadata;
@@ -117,6 +119,8 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
else {
$langcode = NULL;
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'comment' && !empty($data['comment'])) {
@@ -132,7 +136,7 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
// Poster identity information for comments.
case 'hostname':
- $replacements[$original] = $comment->getHostname();
+ $replacements[$original] = $sanitize ? Html::escape($comment->getHostname()) : $comment->getHostname();
break;
case 'mail':
@@ -142,25 +146,23 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
- $replacements[$original] = $mail;
+ $replacements[$original] = $sanitize ? Html::escape($mail) : $mail;
break;
case 'homepage':
- $replacements[$original] = UrlHelper::stripDangerousProtocols($comment->getHomepage());
+ $replacements[$original] = $sanitize ? UrlHelper::filterBadProtocol($comment->getHomepage()) : $comment->getHomepage();
break;
case 'title':
- $replacements[$original] = $comment->getSubject();
+ $replacements[$original] = $sanitize ? Html::escape($comment->getSubject()) : $comment->getSubject();
break;
case 'body':
- // "processed" returns a \Drupal\Component\Utility\SafeStringInterface
- // via check_markup().
- $replacements[$original] = $comment->comment_body->processed;
+ $replacements[$original] = $sanitize ? $comment->comment_body->processed : $comment->comment_body->value;
break;
case 'langcode':
- $replacements[$original] = $comment->language()->getId();
+ $replacements[$original] = $sanitize ? Html::escape($comment->language()->getId()) : $comment->language()->getId();
break;
// Comment related URLs.
@@ -181,14 +183,14 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
if ($comment->getOwnerId()) {
$bubbleable_metadata->addCacheableDependency($comment->getOwner());
}
- $replacements[$original] = $name;
+ $replacements[$original] = $sanitize ? Html::escape($name) : $name;
break;
case 'parent':
if ($comment->hasParentComment()) {
$parent = $comment->getParentComment();
$bubbleable_metadata->addCacheableDependency($parent);
- $replacements[$original] = $parent->getSubject();
+ $replacements[$original] = $sanitize ? Html::escape($parent->getSubject()) : $parent->getSubject();
}
break;
@@ -208,7 +210,7 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
$entity = $comment->getCommentedEntity();
$bubbleable_metadata->addCacheableDependency($entity);
$title = $entity->label();
- $replacements[$original] = $title;
+ $replacements[$original] = $sanitize ? Html::escape($title) : $title;
break;
}
}
diff --git a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
index 13c415e..cf96998 100644
--- a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
+++ b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
@@ -7,7 +7,6 @@
namespace Drupal\comment\Tests;
-use Drupal\Component\Utility\FormattableString;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
@@ -33,16 +32,13 @@ class CommentTokenReplaceTest extends CommentTestBase {
'language' => $language_interface,
);
- // Change the title of the admin user.
- $this->adminUser->name->value = 'This is a title with some special & > " stuff.';
- $this->adminUser->save();
$this->drupalLogin($this->adminUser);
// Set comment variables.
$this->setCommentSubject(TRUE);
// Create a node and a comment.
- $node = $this->drupalCreateNode(['type' => 'article', 'title' => '<script>alert("123")</script>']);
+ $node = $this->drupalCreateNode(array('type' => 'article'));
$parent_comment = $this->postComment($node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
// Post a reply to the comment.
@@ -54,29 +50,29 @@ class CommentTokenReplaceTest extends CommentTestBase {
// Add HTML to ensure that sanitation of some fields tested directly.
$comment->setSubject('<blink>Blinking Comment</blink>');
- // Generate and test tokens.
+ // Generate and test sanitized tokens.
$tests = array();
$tests['[comment:cid]'] = $comment->id();
- $tests['[comment:hostname]'] = $comment->getHostname();
+ $tests['[comment:hostname]'] = Html::escape($comment->getHostname());
$tests['[comment:author]'] = Html::escape($comment->getAuthorName());
- $tests['[comment:mail]'] = $this->adminUser->getEmail();
+ $tests['[comment:mail]'] = Html::escape($this->adminUser->getEmail());
$tests['[comment:homepage]'] = UrlHelper::filterBadProtocol($comment->getHomepage());
$tests['[comment:title]'] = Html::escape($comment->getSubject());
$tests['[comment:body]'] = $comment->comment_body->processed;
- $tests['[comment:langcode]'] = $comment->language()->getId();
+ $tests['[comment:langcode]'] = Html::escape($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;
- $tests['[comment:parent:title]'] = $parent_comment->getSubject();
+ $tests['[comment:parent:title]'] = Html::escape($parent_comment->getSubject());
$tests['[comment:entity]'] = Html::escape($node->getTitle());
// Test node specific tokens.
$tests['[comment:entity:nid]'] = $comment->getCommentedEntityId();
$tests['[comment:entity:title]'] = Html::escape($node->getTitle());
$tests['[comment:author:uid]'] = $comment->getOwnerId();
- $tests['[comment:author:name]'] = Html::escape($this->adminUser->getDisplayName());
+ $tests['[comment:author:name]'] = Html::escape($this->adminUser->getUsername());
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($comment);
$metadata_tests = [];
@@ -118,16 +114,35 @@ class CommentTokenReplaceTest extends CommentTestBase {
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()), $bubbleable_metadata);
- $this->assertEqual($output, $expected, new FormattableString('Comment token %token replaced.', ['%token' => $input]));
+ $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.
+ $tests['[comment:hostname]'] = $comment->getHostname();
+ $tests['[comment:author]'] = $comment->getAuthorName();
+ $tests['[comment:mail]'] = $this->adminUser->getEmail();
+ $tests['[comment:homepage]'] = $comment->getHomepage();
+ $tests['[comment:title]'] = $comment->getSubject();
+ $tests['[comment:body]'] = $comment->comment_body->value;
+ $tests['[comment:langcode]'] = $comment->language()->getId();
+ $tests['[comment:parent:title]'] = $parent_comment->getSubject();
+ $tests['[comment:entity]'] = $node->getTitle();
+ $tests['[comment:author:name]'] = $this->adminUser->getUsername();
+
+ foreach ($tests as $input => $expected) {
+ $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized comment token %token replaced.', array('%token' => $input)));
+ }
+
// Test anonymous comment author.
- $author_name = 'This is a random & " > string';
+ $author_name = $this->randomString();
$comment->setOwnerId(0)->setAuthorName($author_name);
$input = '[comment:author]';
$output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()));
- $this->assertEqual($output, Html::escape($author_name), format_string('Comment author token %token replaced.', array('%token' => $input)));
+ $this->assertEqual($output, Html::escape($author_name), format_string('Sanitized comment author token %token replaced.', array('%token' => $input)));
+ $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $author_name, format_string('Unsanitized comment author token %token replaced.', array('%token' => $input)));
// Load node so comment_count gets computed.
$node = Node::load($node->id());
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index ec8556f..3c677ea 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -5,6 +5,7 @@
* Defines a "managed_file" Form API field and a "file" field for Field module.
*/
+use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
@@ -950,6 +951,7 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
else {
$langcode = NULL;
}
+ $sanitize = !empty($options['sanitize']);
$replacements = array();
@@ -966,15 +968,15 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
// Essential file data
case 'name':
- $replacements[$original] = $file->getFilename();
+ $replacements[$original] = $sanitize ? Html::escape($file->getFilename()) : $file->getFilename();
break;
case 'path':
- $replacements[$original] = $file->getFileUri();
+ $replacements[$original] = $sanitize ? Html::escape($file->getFileUri()) : $file->getFileUri();
break;
case 'mime':
- $replacements[$original] = $file->getMimeType();
+ $replacements[$original] = $sanitize ? Html::escape($file->getMimeType()) : $file->getMimeType();
break;
case 'size':
@@ -982,7 +984,7 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
break;
case 'url':
- $replacements[$original] = file_create_url($file->getFileUri());
+ $replacements[$original] = $sanitize ? Html::escape(file_create_url($file->getFileUri())) : file_create_url($file->getFileUri());
break;
// These tokens are default variations on the chained tokens handled below.
@@ -1002,7 +1004,7 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
$owner = $file->getOwner();
$bubbleable_metadata->addCacheableDependency($owner);
$name = $owner->label();
- $replacements[$original] = $name;
+ $replacements[$original] = $sanitize ? Html::escape($name) : $name;
break;
}
}
diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
index 9174b0f..afff2a3 100644
--- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
+++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
@@ -8,7 +8,6 @@
namespace Drupal\file\Plugin\Field\FieldType;
use Drupal\Component\Utility\Bytes;
-use Drupal\Component\Utility\PlainTextOutput;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -261,7 +260,7 @@ class FileItem extends EntityReferenceItem {
* An array of token objects to pass to token_replace().
*
* @return string
- * An unsanitized file directory URI with tokens replaced.
+ * A file directory URI with tokens replaced.
*
* @see token_replace()
*/
@@ -269,13 +268,9 @@ class FileItem extends EntityReferenceItem {
$settings = $this->getSettings();
$destination = trim($settings['file_directory'], '/');
- // Replace tokens. As the tokens might contain HTML we convert it to plain
- // text.
- $destination = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($destination, $data));
+ // Replace tokens.
+ $destination = \Drupal::token()->replace($destination, $data);
- // @todo Is any valid URI always safe output? If not, handle invalid URIs
- // here, and certainly do not return them, see
- // https://www.drupal.org/node/2578193.
return $settings['uri_scheme'] . '://' . $destination;
}
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index 0bfabb5..d978058 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -188,7 +188,9 @@ class LinkFormatter extends FormatterBase implements ContainerFactoryPluginInter
// If the title field value is available, use it for the link text.
if (empty($settings['url_only']) && !empty($item->title)) {
- $link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
+ // Unsanitized token replacement here because the entire link title
+ // gets auto-escaped during link generation.
+ $link_title = \Drupal::token()->replace($item->title, array($entity->getEntityTypeId() => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
}
// Trim the link text to the desired length.
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
index c6cf212..5e3f458 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
@@ -54,7 +54,9 @@ class LinkSeparateFormatter extends LinkFormatter {
// If the link text field value is available, use it for the text.
if (empty($settings['url_only']) && !empty($item->title)) {
- $link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
+ // Unsanitized token replacement here because the entire link title
+ // gets auto-escaped during link generation.
+ $link_title = \Drupal::token()->replace($item->title, array($entity->getEntityTypeId() => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
}
// The link_separate formatter has two titles; the link text (as in the
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index dbe8da2..3294044 100644
--- a/core/modules/node/node.tokens.inc
+++ b/core/modules/node/node.tokens.inc
@@ -5,6 +5,7 @@
* Builds placeholder replacement tokens for node-related data.
*/
+use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
@@ -95,6 +96,8 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
else {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'node' && !empty($data['node'])) {
@@ -113,16 +116,16 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
break;
case 'type':
- $replacements[$original] = $node->getType();
+ $replacements[$original] = $sanitize ? Html::escape($node->getType()) : $node->getType();
break;
case 'type-name':
$type_name = node_get_type_label($node);
- $replacements[$original] = $type_name;
+ $replacements[$original] = $sanitize ? Html::escape($type_name) : $type_name;
break;
case 'title':
- $replacements[$original] = $node->getTitle();
+ $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle();
break;
case 'body':
@@ -130,13 +133,14 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
$translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens'));
if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
$item = $items[0];
+ $field_definition = \Drupal::entityManager()->getFieldDefinitions('node', $node->bundle())['body'];
// If the summary was requested and is not empty, use it.
if ($name == 'summary' && !empty($item->summary)) {
- $output = $item->summary_processed;
+ $output = $sanitize ? $item->summary_processed : $item->summary;
}
// Attempt to provide a suitable version of the 'body' field.
else {
- $output = $item->processed;
+ $output = $sanitize ? $item->processed : $item->value;
// A summary was requested.
if ($name == 'summary') {
// Generate an optionally trimmed summary of the body field.
@@ -155,14 +159,12 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
$output = text_summary($output, $item->format, $length);
}
}
- // "processed" returns a
- // \Drupal\Component\Utility\SafeStringInterface via check_markup().
$replacements[$original] = $output;
}
break;
case 'langcode':
- $replacements[$original] = $node->language()->getId();
+ $replacements[$original] = $sanitize ? Html::escape($node->language()->getId()) : $node->language()->getId();
break;
case 'url':
@@ -177,7 +179,7 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
case 'author':
$account = $node->getOwner() ? $node->getOwner() : User::load(0);
$bubbleable_metadata->addCacheableDependency($account);
- $replacements[$original] = $account->label();
+ $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
break;
case 'created':
diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
index a99637f..3ec91d9 100644
--- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
@@ -7,7 +7,6 @@
namespace Drupal\node\Tests;
-use Drupal\Component\Utility\FormattableString;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\system\Tests\System\TokenReplaceUnitTestBase;
@@ -56,11 +55,11 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
'tnid' => 0,
'uid' => $account->id(),
'title' => '<blink>Blinking Text</blink>',
- 'body' => [['value' => 'Regular NODE body for the test.', 'summary' => 'Fancy NODE summary.', 'format' => 'plain_text']],
+ 'body' => array(array('value' => $this->randomMachineName(32), 'summary' => $this->randomMachineName(16), 'format' => 'plain_text')),
));
$node->save();
- // Generate and test tokens.
+ // Generate and test sanitized tokens.
$tests = array();
$tests['[node:nid]'] = $node->id();
$tests['[node:vid]'] = $node->getRevisionId();
@@ -69,12 +68,12 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
$tests['[node:title]'] = Html::escape($node->getTitle());
$tests['[node:body]'] = $node->body->processed;
$tests['[node:summary]'] = $node->body->summary_processed;
- $tests['[node:langcode]'] = $node->language()->getId();
+ $tests['[node:langcode]'] = Html::escape($node->language()->getId());
$tests['[node:url]'] = $node->url('canonical', $url_options);
$tests['[node:edit-url]'] = $node->url('edit-form', $url_options);
- $tests['[node:author]'] = $account->getUsername();
+ $tests['[node:author]'] = Html::escape($account->getUsername());
$tests['[node:author:uid]'] = $node->getOwnerId();
- $tests['[node:author:name]'] = $account->getUsername();
+ $tests['[node:author:name]'] = Html::escape($account->getUsername());
$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()));
@@ -105,20 +104,32 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
foreach ($tests as $input => $expected) {
$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('Node token %token replaced.', ['%token' => $input]));
+ $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.
+ $tests['[node:title]'] = $node->getTitle();
+ $tests['[node:body]'] = $node->body->value;
+ $tests['[node:summary]'] = $node->body->summary;
+ $tests['[node:langcode]'] = $node->language()->getId();
+ $tests['[node:author:name]'] = $account->getUsername();
+
+ foreach ($tests as $input => $expected) {
+ $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced.', array('%token' => $input)));
+ }
+
// Repeat for a node without a summary.
$node = entity_create('node', array(
'type' => 'article',
'uid' => $account->id(),
'title' => '<blink>Blinking Text</blink>',
- 'body' => [['value' => 'A string that looks random like TR5c2I', 'format' => 'plain_text']],
+ 'body' => array(array('value' => $this->randomMachineName(32), 'format' => 'plain_text')),
));
$node->save();
- // Generate and test token - use full body as expected value.
+ // Generate and test sanitized token - use full body as expected value.
$tests = array();
$tests['[node:summary]'] = $node->body->processed;
@@ -127,7 +138,15 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
foreach ($tests as $input => $expected) {
$output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage));
- $this->assertEqual($output, $expected, new FormattableString('Node token %token replaced for node without a summary.', ['%token' => $input]));
+ $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced for node without a summary.', array('%token' => $input)));
+ }
+
+ // Generate and test unsanitized tokens.
+ $tests['[node:summary]'] = $node->body->value;
+
+ foreach ($tests as $input => $expected) {
+ $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage, 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced for node without a summary.', array('%token' => $input)));
}
}
diff --git a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
index c7af02d..33f867a 100644
--- a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
+++ b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
@@ -7,7 +7,6 @@
namespace Drupal\system\Tests\System;
-use Drupal\Component\Utility\FormattableString;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Render\BubbleableMetadata;
@@ -104,7 +103,7 @@ class TokenReplaceUnitTest extends TokenReplaceUnitTestBase {
->save();
- // Generate and test tokens.
+ // Generate and test sanitized tokens.
$tests = array();
$tests['[site:name]'] = Html::escape($config->get('name'));
$tests['[site:slogan]'] = $safe_slogan;
@@ -130,9 +129,29 @@ class TokenReplaceUnitTest extends TokenReplaceUnitTestBase {
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata);
- $this->assertEqual($output, $expected, new FormattableString('System site information token %token replaced.', ['%token' => $input]));
+ $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.
+ $tests['[site:name]'] = $config->get('name');
+ $tests['[site:slogan]'] = $config->get('slogan');
+
+ foreach ($tests as $input => $expected) {
+ $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)));
+ }
+
+ // Check that the results of Token::generate are sanitized properly. This
+ // does NOT test the cleanliness of every token -- just that the $sanitize
+ // 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, [], [], $bubbleable_metadata);
+ $this->assertEqual($generated['[site:slogan]'], $safe_slogan, 'Token sanitized.');
+
+ $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/system.module b/core/modules/system/system.module
index e21c2c8..a8477d3 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -5,7 +5,6 @@
* Configuration system that lets administrators modify the workings of the site.
*/
-use Drupal\Component\Utility\PlainTextOutput;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Cache\Cache;
@@ -1291,7 +1290,7 @@ function system_mail($key, &$message, $params) {
$context = $params['context'];
- $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'], $context));
+ $subject = $token_service->replace($context['subject'], $context);
$body = $token_service->replace($context['message'], $context);
$message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc
index e436e42..a11f55d 100644
--- a/core/modules/system/system.tokens.inc
+++ b/core/modules/system/system.tokens.inc
@@ -7,6 +7,8 @@
* This file handles tokens for the global 'site' and 'date' tokens.
*/
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Render\BubbleableMetadata;
@@ -98,6 +100,8 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
else {
$langcode = NULL;
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'site') {
@@ -107,17 +111,14 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
$config = \Drupal::config('system.site');
$bubbleable_metadata->addCacheableDependency($config);
$site_name = $config->get('name');
- $replacements[$original] = $site_name;
+ $replacements[$original] = $sanitize ? Html::escape($site_name) : $site_name;
break;
case 'slogan':
$config = \Drupal::config('system.site');
$bubbleable_metadata->addCacheableDependency($config);
$slogan = $config->get('slogan');
- $build = [
- '#markup' => $slogan,
- ];
- $replacements[$original] = \Drupal::service('renderer')->renderPlain($build);
+ $replacements[$original] = $sanitize ? Xss::filterAdmin($slogan) : $slogan;
break;
case 'mail':
@@ -177,7 +178,7 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
break;
case 'raw':
- $replacements[$original] = $date;
+ $replacements[$original] = $sanitize ? Html::escape($date) : $date;
break;
}
}
diff --git a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
index 1e00a8e..4fcb0f1 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -7,6 +7,8 @@
namespace Drupal\taxonomy\Tests;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Render\BubbleableMetadata;
@@ -84,13 +86,13 @@ class TokenReplaceTest extends TaxonomyTestBase {
// Generate and test sanitized tokens for term1.
$tests = array();
$tests['[term:tid]'] = $term1->id();
- $tests['[term:name]'] = $term1->getName();
+ $tests['[term:name]'] = Html::escape($term1->getName());
$tests['[term:description]'] = $term1->description->processed;
$tests['[term:url]'] = $term1->url('canonical', array('absolute' => TRUE));
$tests['[term:node-count]'] = 0;
$tests['[term:parent:name]'] = '[term:parent:name]';
- $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
- $tests['[term:vocabulary]'] = $this->vocabulary->label();
+ $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
+ $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label());
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1);
@@ -115,14 +117,14 @@ class TokenReplaceTest extends TaxonomyTestBase {
// Generate and test sanitized tokens for term2.
$tests = array();
$tests['[term:tid]'] = $term2->id();
- $tests['[term:name]'] = $term2->getName();
+ $tests['[term:name]'] = Html::escape($term2->getName());
$tests['[term:description]'] = $term2->description->processed;
$tests['[term:url]'] = $term2->url('canonical', array('absolute' => TRUE));
$tests['[term:node-count]'] = 1;
- $tests['[term:parent:name]'] = $term1->getName();
+ $tests['[term:parent:name]'] = Html::escape($term1->getName());
$tests['[term:parent:url]'] = $term1->url('canonical', array('absolute' => TRUE));
$tests['[term:parent:parent:name]'] = '[term:parent:parent:name]';
- $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
+ $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
// Test to make sure that we generated something for each token.
$this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
@@ -132,11 +134,22 @@ class TokenReplaceTest extends TaxonomyTestBase {
$this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input)));
}
+ // Generate and test unsanitized tokens.
+ $tests['[term:name]'] = $term2->getName();
+ $tests['[term:description]'] = $term2->getDescription();
+ $tests['[term:parent:name]'] = $term1->getName();
+ $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
+
+ foreach ($tests as $input => $expected) {
+ $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input)));
+ }
+
// Generate and test sanitized tokens.
$tests = array();
$tests['[vocabulary:vid]'] = $this->vocabulary->id();
- $tests['[vocabulary:name]'] = $this->vocabulary->label();
- $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
+ $tests['[vocabulary:name]'] = Html::escape($this->vocabulary->label());
+ $tests['[vocabulary:description]'] = Xss::filter($this->vocabulary->getDescription());
$tests['[vocabulary:node-count]'] = 1;
$tests['[vocabulary:term-count]'] = 2;
@@ -147,6 +160,15 @@ class TokenReplaceTest extends TaxonomyTestBase {
$output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId()));
$this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
}
+
+ // Generate and test unsanitized tokens.
+ $tests['[vocabulary:name]'] = $this->vocabulary->label();
+ $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
+
+ foreach ($tests as $input => $expected) {
+ $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
+ }
}
}
diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc
index e255d77..daf690b 100644
--- a/core/modules/taxonomy/taxonomy.tokens.inc
+++ b/core/modules/taxonomy/taxonomy.tokens.inc
@@ -5,6 +5,8 @@
* Builds placeholder replacement tokens for taxonomy terms and vocabularies.
*/
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\taxonomy\Entity\Vocabulary;
@@ -95,6 +97,7 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
$token_service = \Drupal::token();
$replacements = array();
+ $sanitize = !empty($options['sanitize']);
$taxonomy_storage = \Drupal::entityManager()->getStorage('taxonomy_term');
if ($type == 'term' && !empty($data['term'])) {
$term = $data['term'];
@@ -106,13 +109,11 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
break;
case 'name':
- $replacements[$original] = $term->getName();
+ $replacements[$original] = $sanitize ? Html::escape($term->getName()) : $term->getName();
break;
case 'description':
- // "processed" returns a \Drupal\Component\Utility\SafeStringInterface
- // via check_markup().
- $replacements[$original] = $term->description->processed;
+ $replacements[$original] = $sanitize ? $term->description->processed : $term->getDescription();
break;
case 'url':
@@ -130,14 +131,14 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
case 'vocabulary':
$vocabulary = Vocabulary::load($term->bundle());
$bubbleable_metadata->addCacheableDependency($vocabulary);
- $replacements[$original] = $vocabulary->label();
+ $replacements[$original] = Html::escape($vocabulary->label());
break;
case 'parent':
if ($parents = $taxonomy_storage->loadParents($term->id())) {
$parent = array_pop($parents);
$bubbleable_metadata->addCacheableDependency($parent);
- $replacements[$original] = $parent->getName();
+ $replacements[$original] = Html::escape($parent->getName());
}
break;
}
@@ -164,12 +165,11 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
break;
case 'name':
- $replacements[$original] = $vocabulary->label();
+ $replacements[$original] = $sanitize ? Html::escape($vocabulary->label()) : $vocabulary->label();
break;
case 'description':
- $build = ['#markup' => $vocabulary->getDescription()];
- $replacements[$original] = \Drupal::service('renderer')->renderPlain($build);
+ $replacements[$original] = $sanitize ? Xss::filter($vocabulary->getDescription()) : $vocabulary->getDescription();
break;
case 'term-count':
diff --git a/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php b/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
index ac73cee..bedb4bd 100644
--- a/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
+++ b/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
@@ -8,6 +8,7 @@
namespace Drupal\tour\Plugin\tour\tip;
use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Xss;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Utility\Token;
use Drupal\tour\TipPluginBase;
@@ -120,7 +121,7 @@ class TipPluginText extends TipPluginBase implements ContainerFactoryPluginInter
*/
public function getOutput() {
$output = '<h2 class="tour-tip-label" id="tour-tip-' . $this->getAriaId() . '-label">' . Html::escape($this->getLabel()) . '</h2>';
- $output .= '<p class="tour-tip-body" id="tour-tip-' . $this->getAriaId() . '-contents">' . $this->token->replace($this->getBody()) . '</p>';
+ $output .= '<p class="tour-tip-body" id="tour-tip-' . $this->getAriaId() . '-contents">' . Xss::filterAdmin($this->token->replace($this->getBody())) . '</p>';
return array('#markup' => $output);
}
diff --git a/core/modules/user/src/Tests/UserTokenReplaceTest.php b/core/modules/user/src/Tests/UserTokenReplaceTest.php
index fd2f032..ad5d296 100644
--- a/core/modules/user/src/Tests/UserTokenReplaceTest.php
+++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php
@@ -7,7 +7,7 @@
namespace Drupal\user\Tests;
-use Drupal\Component\Utility\FormattableString;
+use Drupal\Component\Utility\Html;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
@@ -48,7 +48,6 @@ class UserTokenReplaceTest extends WebTestBase {
);
\Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE);
- \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
// Create two users and log them in one after another.
$user1 = $this->drupalCreateUser(array());
@@ -60,21 +59,21 @@ class UserTokenReplaceTest extends WebTestBase {
$account = User::load($user1->id());
$global_account = User::load(\Drupal::currentUser()->id());
- // Generate and test tokens.
+ // Generate and test sanitized tokens.
$tests = array();
$tests['[user:uid]'] = $account->id();
- $tests['[user:name]'] = $account->getAccountName();
- $tests['[user:account-name]'] = $account->getAccountName();
+ $tests['[user:name]'] = Html::escape($account->getAccountName());
+ $tests['[user:account-name]'] = Html::escape($account->getAccountName());
$tests['[user:display-name]'] = $account->getDisplayName();
- $tests['[user:mail]'] = $account->getEmail();
+ $tests['[user:mail]'] = Html::escape($account->getEmail());
$tests['[user:url]'] = $account->url('canonical', $url_options);
$tests['[user:edit-url]'] = $account->url('edit-form', $url_options);
$tests['[user:last-login]'] = format_date($account->getLastLoginTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[user:last-login:short]'] = format_date($account->getLastLoginTime(), 'short', '', NULL, $language_interface->getId());
$tests['[user:created]'] = format_date($account->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[user:created:short]'] = format_date($account->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
- $tests['[current-user:name]'] = $global_account->getAccountName();
- $tests['[current-user:account-name]'] = $global_account->getAccountName();
+ $tests['[current-user:name]'] = Html::escape($global_account->getAccountName());
+ $tests['[current-user:account-name]'] = Html::escape($global_account->getAccountName());
$tests['[current-user:display-name]'] = $global_account->getDisplayName();
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($account);
@@ -106,8 +105,8 @@ class UserTokenReplaceTest extends WebTestBase {
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
- $output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId()], $bubbleable_metadata);
- $this->assertEqual($output, $expected, new FormattableString('User token %token replaced.', ['%token' => $input]));
+ $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]);
}
@@ -131,6 +130,21 @@ class UserTokenReplaceTest extends WebTestBase {
$this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
}
+ // Generate and test unsanitized tokens.
+ $tests = [];
+ $tests['[user:name]'] = $account->getAccountName();
+ $tests['[user:account-name]'] = $account->getAccountName();
+ $tests['[user:display-name]'] = $account->getDisplayName();
+ $tests['[user:mail]'] = $account->getEmail();
+ $tests['[current-user:account-name]'] = $global_account->getAccountname();
+ $tests['[current-user:name]'] = $global_account->getAccountName();
+ $tests['[current-user:display-name]'] = $global_account->getDisplayName();
+
+ foreach ($tests as $input => $expected) {
+ $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized user token %token replaced.', array('%token' => $input)));
+ }
+
// Generate login and cancel link.
$tests = array();
$tests['[user:one-time-login-url]'] = user_pass_reset_url($account);
@@ -139,7 +153,7 @@ class UserTokenReplaceTest extends WebTestBase {
// Generate tokens with interface language.
$link = \Drupal::url('user.page', [], array('absolute' => TRUE));
foreach ($tests as $input => $expected) {
- $output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId(), 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
+ $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId(), 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
$this->assertTrue(strpos($output, $link) === 0, 'Generated URL is in interface language.');
}
@@ -148,7 +162,7 @@ class UserTokenReplaceTest extends WebTestBase {
$account->save();
$link = \Drupal::url('user.page', [], array('language' => \Drupal::languageManager()->getLanguage($account->getPreferredLangcode()), 'absolute' => TRUE));
foreach ($tests as $input => $expected) {
- $output = $token_service->replace($input, ['user' => $account], ['callback' => 'user_mail_tokens', 'clear' => TRUE]);
+ $output = $token_service->replace($input, array('user' => $account), array('callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
$this->assertTrue(strpos($output, $link) === 0, "Generated URL is in the user's preferred language.");
}
@@ -156,17 +170,9 @@ class UserTokenReplaceTest extends WebTestBase {
$link = \Drupal::url('user.page', [], array('language' => \Drupal::languageManager()->getLanguage('de'), 'absolute' => TRUE));
foreach ($tests as $input => $expected) {
foreach (array($user1, $user2) as $account) {
- $output = $token_service->replace($input, ['user' => $account], ['langcode' => 'de', 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
+ $output = $token_service->replace($input, array('user' => $account), array('langcode' => 'de', 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
$this->assertTrue(strpos($output, $link) === 0, "Generated URL in in the requested language.");
}
}
-
- // Generate user display name tokens when safe markup is returned.
- // @see user_hooks_test_user_format_name_alter()
- \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
- $input = '[user:display-name] [current-user:display-name]';
- $expected = "<em>{$user1->id()}</em> <em>{$user2->id()}</em>";
- $output = $token_service->replace($input, ['user' => $user1]);
- $this->assertEqual($output, $expected, new FormattableString('User token %token does not escape safe markup.', ['%token' => 'display-name']));
}
}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index a7f752a..084b991 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1,7 +1,6 @@
<?php
use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\PlainTextOutput;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Cache\Cache;
@@ -917,8 +916,10 @@ function user_mail($key, &$message, $params) {
$language_manager->setConfigOverrideLanguage($language);
$mail_config = \Drupal::config('user.mail');
- $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
- $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options));
+ // We do not sanitize the token replacement, since the output of this
+ // replacement is intended for an email message, not a web browser.
+ $token_options = array('langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE);
+ $message['subject'] .= $token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options);
$message['body'][] = $token_service->replace($mail_config->get($key . '.body'), $variables, $token_options);
$language_manager->setConfigOverrideLanguage($original_language);
diff --git a/core/modules/user/user.tokens.inc b/core/modules/user/user.tokens.inc
index 7f456f6..087724e 100644
--- a/core/modules/user/user.tokens.inc
+++ b/core/modules/user/user.tokens.inc
@@ -5,6 +5,7 @@
* Builds placeholder replacement tokens for user-related data.
*/
+use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\Entity\DateFormat;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\user\Entity\User;
@@ -84,6 +85,8 @@ function user_tokens($type, $tokens, array $data, array $options, BubbleableMeta
else {
$langcode = NULL;
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'user' && !empty($data['user'])) {
@@ -107,14 +110,14 @@ function user_tokens($type, $tokens, array $data, array $options, BubbleableMeta
case 'name':
case 'account-name':
$display_name = $account->getAccountName();
- $replacements[$original] = $display_name;
+ $replacements[$original] = $sanitize ? Html::escape($display_name) : $display_name;
if ($account->isAnonymous()) {
$bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
}
break;
case 'mail':
- $replacements[$original] = $account->getEmail();
+ $replacements[$original] = $sanitize ? Html::escape($account->getEmail()) : $account->getEmail();
break;
case 'url':
diff --git a/core/modules/views/src/Plugin/views/area/Entity.php b/core/modules/views/src/Plugin/views/area/Entity.php
index ed17232..3234f88 100644
--- a/core/modules/views/src/Plugin/views/area/Entity.php
+++ b/core/modules/views/src/Plugin/views/area/Entity.php
@@ -162,9 +162,7 @@ class Entity extends TokenizeAreaPluginBase {
// @todo Use a method to check for tokens in
// https://www.drupal.org/node/2396607.
if (strpos($this->options['target'], '{{') !== FALSE) {
- // We cast as we need the integer/string value provided by the
- // ::tokenizeValue() call.
- $target_id = (string) $this->tokenizeValue($this->options['target']);
+ $target_id = $this->tokenizeValue($this->options['target']);
if ($entity = $this->entityManager->getStorage($this->entityType)->load($target_id)) {
$target_entity = $entity;
}
diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc
index 6d9de95..33d162b 100644
--- a/core/modules/views/views.tokens.inc
+++ b/core/modules/views/views.tokens.inc
@@ -5,6 +5,7 @@
* Token integration for the views module.
*/
+use Drupal\Component\Utility\Html;
use Drupal\Core\Render\BubbleableMetadata;
/**
@@ -73,6 +74,8 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
if (isset($options['language'])) {
$url_options['language'] = $options['language'];
}
+ $sanitize = !empty($options['sanitize']);
+
$replacements = array();
if ($type == 'view' && !empty($data['view'])) {
@@ -84,11 +87,11 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
foreach ($tokens as $name => $original) {
switch ($name) {
case 'label':
- $replacements[$original] = $view->storage->label();
+ $replacements[$original] = $sanitize ? Html::escape($view->storage->label()) : $view->storage->label();
break;
case 'description':
- $replacements[$original] = $view->storage->get('description');
+ $replacements[$original] = $sanitize ? Html::escape($view->storage->get('description')) : $view->storage->get('description');
break;
case 'id':
@@ -97,7 +100,7 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
case 'title':
$title = $view->getTitle();
- $replacements[$original] = $title;
+ $replacements[$original] = $sanitize ? Html::escape($title) : $title;
break;
case 'url':
diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
index e902134..a738282 100644
--- a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
@@ -7,12 +7,10 @@
namespace Drupal\Tests\Core\Utility;
-use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\Core\Render\SafeString;
use Drupal\Core\Utility\Token;
use Drupal\Tests\UnitTestCase;
@@ -264,39 +262,4 @@ class TokenTest extends UnitTestCase {
$this->token->resetInfo();
}
- /**
- * @covers ::replace
- * @dataProvider providerTestReplaceEscaping
- */
- public function testReplaceEscaping($string, array $tokens, $expected) {
- $this->moduleHandler->expects($this->any())
- ->method('invokeAll')
- ->willReturnCallback(function ($type, $args) {
- return $args[2]['tokens'];
- });
-
- $result = $this->token->replace($string, ['tokens' => $tokens]);
- $this->assertInternalType('string', $result);
- $this->assertEquals($expected, $result);
- }
-
- public function providerTestReplaceEscaping() {
- $data = [];
-
- // No tokens. The first argument to Token::replace() should not be escaped.
- $data['no-tokens'] = ['muh', [], 'muh'];
- $data['html-in-string'] = ['<h1>Giraffe</h1>', [], '<h1>Giraffe</h1>'];
- $data['html-in-string-quote'] = ['<h1>Giraffe"</h1>', [], '<h1>Giraffe"</h1>'];
-
- $data['simple-placeholder-with-plain-text'] = ['<h1>[token:meh]</h1>', ['[token:meh]' => 'Giraffe"'], '<h1>' . Html::escape('Giraffe"') . '</h1>'];
-
- $data['simple-placeholder-with-safe-html'] = [
- '<h1>[token:meh]</h1>',
- ['[token:meh]' => SafeString::create('<em>Emphasized</em>')],
- '<h1><em>Emphasized</em></h1>',
- ];
-
- return $data;
- }
-
}