Loading core/lib/Drupal/Core/Utility/Token.php +67 −14 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ use Drupal\Component\Render\HtmlEscapedText; use Drupal\Component\Render\MarkupInterface; use Drupal\Component\Render\PlainTextOutput; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; Loading Loading @@ -134,12 +135,10 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend } /** * Replaces all tokens in a given string with appropriate values. * Replaces all tokens in given markup 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. * @param string $markup * An HTML string 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 Loading Loading @@ -175,14 +174,58 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend * * @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, using * 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(). * caller is responsible for choosing the right sanitization, for example * the result can be put into #markup, in which case it would be sanitized * by Xss::filterAdmin(). * * The return value must be treated as unsafe even if the input was safe * markup. This is necessary because an attacker could craft an input * string and token value that, although each safe individually, would be * unsafe when combined by token replacement. * * @see static::replacePlain() */ public function replace($text, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { public function replace($markup, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { return $this->doReplace(TRUE, (string) $markup, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given plain text string with appropriate values. * * @param string $plain * Plain text string. * @param array $data * (optional) An array of keyed objects. See replace(). * @param array $options * (optional) A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The entered plain text with tokens replaced. */ public function replacePlain(string $plain, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL): string { return $this->doReplace(FALSE, $plain, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given string with appropriate values. * * @param bool $markup * TRUE to convert token values to markup, FALSE to convert to plain text. * @param string $text * A string containing replaceable tokens. * @param array $data * An array of keyed objects. See replace(). * @param array $options * A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The token result is the entered string with tokens replaced. */ protected function doReplace(bool $markup, string $text, array $data, array $options, BubbleableMetadata $bubbleable_metadata = NULL): string { $text_tokens = $this->scan($text); if (empty($text_tokens)) { return $text; Loading @@ -199,10 +242,20 @@ public function replace($text, array $data = [], array $options = [], Bubbleable } } // Escape the tokens, unless they are explicitly markup. // Each token value is markup if it implements MarkupInterface otherwise it // is plain text. Convert them, but only if needed. It can cause corruption // to render a string that's already plain text or to escape a string // that's already markup. foreach ($replacements as $token => $value) { if ($markup) { // Escape plain text tokens. $replacements[$token] = $value instanceof MarkupInterface ? $value : new HtmlEscapedText($value); } else { // Render markup tokens to plain text. $replacements[$token] = $value instanceof MarkupInterface ? PlainTextOutput::renderFromHtml($value) : $value; } } // Optionally alter the list of replacement values. if (!empty($options['callback'])) { Loading core/tests/Drupal/Tests/Core/Utility/TokenTest.php +25 −0 Original line number Diff line number Diff line Loading @@ -294,4 +294,29 @@ public function providerTestReplaceEscaping() { return $data; } /** * @covers ::replacePlain */ public function testReplacePlain() { $this->setupSiteTokens(); $base = 'Wow, great "[site:name]" has a slogan "[site:slogan]"'; $plain = $this->token->replacePlain($base); $this->assertEquals($plain, 'Wow, great "Your <best> buys" has a slogan "We are best"'); } /** * Sets up the token library to return site tokens. */ protected function setupSiteTokens() { // The site name is plain text, but the slogan is markup. $tokens = [ '[site:name]' => 'Your <best> buys', '[site:slogan]' => Markup::Create('We are <b>best</b>'), ]; $this->moduleHandler->expects($this->any()) ->method('invokeAll') ->willReturn($tokens); } } Loading
core/lib/Drupal/Core/Utility/Token.php +67 −14 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ use Drupal\Component\Render\HtmlEscapedText; use Drupal\Component\Render\MarkupInterface; use Drupal\Component\Render\PlainTextOutput; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; Loading Loading @@ -134,12 +135,10 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend } /** * Replaces all tokens in a given string with appropriate values. * Replaces all tokens in given markup 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. * @param string $markup * An HTML string 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 Loading Loading @@ -175,14 +174,58 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend * * @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, using * 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(). * caller is responsible for choosing the right sanitization, for example * the result can be put into #markup, in which case it would be sanitized * by Xss::filterAdmin(). * * The return value must be treated as unsafe even if the input was safe * markup. This is necessary because an attacker could craft an input * string and token value that, although each safe individually, would be * unsafe when combined by token replacement. * * @see static::replacePlain() */ public function replace($text, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { public function replace($markup, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) { return $this->doReplace(TRUE, (string) $markup, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given plain text string with appropriate values. * * @param string $plain * Plain text string. * @param array $data * (optional) An array of keyed objects. See replace(). * @param array $options * (optional) A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The entered plain text with tokens replaced. */ public function replacePlain(string $plain, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL): string { return $this->doReplace(FALSE, $plain, $data, $options, $bubbleable_metadata); } /** * Replaces all tokens in a given string with appropriate values. * * @param bool $markup * TRUE to convert token values to markup, FALSE to convert to plain text. * @param string $text * A string containing replaceable tokens. * @param array $data * An array of keyed objects. See replace(). * @param array $options * A keyed array of options. See replace(). * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata * (optional) Target for adding metadata. See replace(). * * @return string * The token result is the entered string with tokens replaced. */ protected function doReplace(bool $markup, string $text, array $data, array $options, BubbleableMetadata $bubbleable_metadata = NULL): string { $text_tokens = $this->scan($text); if (empty($text_tokens)) { return $text; Loading @@ -199,10 +242,20 @@ public function replace($text, array $data = [], array $options = [], Bubbleable } } // Escape the tokens, unless they are explicitly markup. // Each token value is markup if it implements MarkupInterface otherwise it // is plain text. Convert them, but only if needed. It can cause corruption // to render a string that's already plain text or to escape a string // that's already markup. foreach ($replacements as $token => $value) { if ($markup) { // Escape plain text tokens. $replacements[$token] = $value instanceof MarkupInterface ? $value : new HtmlEscapedText($value); } else { // Render markup tokens to plain text. $replacements[$token] = $value instanceof MarkupInterface ? PlainTextOutput::renderFromHtml($value) : $value; } } // Optionally alter the list of replacement values. if (!empty($options['callback'])) { Loading
core/tests/Drupal/Tests/Core/Utility/TokenTest.php +25 −0 Original line number Diff line number Diff line Loading @@ -294,4 +294,29 @@ public function providerTestReplaceEscaping() { return $data; } /** * @covers ::replacePlain */ public function testReplacePlain() { $this->setupSiteTokens(); $base = 'Wow, great "[site:name]" has a slogan "[site:slogan]"'; $plain = $this->token->replacePlain($base); $this->assertEquals($plain, 'Wow, great "Your <best> buys" has a slogan "We are best"'); } /** * Sets up the token library to return site tokens. */ protected function setupSiteTokens() { // The site name is plain text, but the slogan is markup. $tokens = [ '[site:name]' => 'Your <best> buys', '[site:slogan]' => Markup::Create('We are <b>best</b>'), ]; $this->moduleHandler->expects($this->any()) ->method('invokeAll') ->willReturn($tokens); } }