Commit 24ed79e1 authored by catch's avatar catch

Issue #2559971 by stefan.r, alexpott, lauriii, nlisgo, plach, dawehner: Make...

Issue #2559971 by stefan.r, alexpott, lauriii, nlisgo, plach, dawehner: Make SafeMarkup::format() return a safe string object to remove reliance on a static, unpredictable safe list
parent 03ac04f3
<?php
/**
* @file
* Contains Drupal\Component\Utility\FormattableString.
*/
namespace Drupal\Component\Utility;
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* When cast to a string it replaces variable placeholders in the string with
* the arguments passed in during construction and escapes the values so they
* can be safely displayed as HTML. It should be used on any unknown text that
* is intended to be printed to an HTML page (especially text that may have come
* from untrusted users, since in that case it prevents cross-site scripting and
* other security problems).
*
* This class is not intended for passing arbitrary user input into any HTML
* attribute value, as only URL attributes such as "src" and "href" are
* supported (using ":variable"). Never use this method on unsafe HTML
* attributes such as "on*" and "style" and take care when using this with
* unsupported attributes such as "title" or "alt" as this can lead to
* unexpected output.
*
* In most cases, you should use TranslatableString or PluralTranslatableString
* rather than this object, since they will translate the text (on
* non-English-only sites) in addition to formatting it.
*
* @ingroup sanitization
*
* @see \Drupal\Core\StringTranslation\TranslatableString
* @see \Drupal\Core\StringTranslation\PluralTranslatableString
*/
class FormattableString implements SafeStringInterface {
use PlaceholderTrait;
/**
* The arguments to replace placeholders with.
*
* @var array
*/
protected $arguments = [];
/**
* Constructs a new class instance.
*
* @param string $string
* A string containing placeholders. The string itself will not be escaped,
* any unsafe content must be in $args and inserted via placeholders.
* @param array $args
* An array with placeholder replacements, keyed by placeholder. See
* \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
* additional information about placeholders.
*
* @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
*/
public function __construct($string, array $arguments) {
$this->string = (string) $string;
$this->arguments = $arguments;
}
/**
* {@inheritdoc}
*/
public function __toString() {
return static::placeholderFormat($this->string, $this->arguments);
}
/**
* Returns the string length.
*
* @return int
* The length of the string.
*/
public function count() {
return Unicode::strlen($this->string);
}
/**
* Returns a representation of the object for use in JSON serialization.
*
* @return string
* The safe string content.
*/
public function jsonSerialize() {
return $this->__toString();
}
}
......@@ -15,18 +15,62 @@ trait PlaceholderTrait {
/**
* Formats a string by replacing variable placeholders.
*
* This trait is not intended for passing arbitrary user input into any HTML
* attribute value, as only URL attributes such as "src" and "href" are
* supported (using ":variable"). Never use this method on unsafe HTML
* attributes such as "on*" and "style" and take care when using this with
* unsupported attributes such as "title" or "alt" as this can lead to
* unexpected and unsafe output.
*
* @param string $string
* A string containing placeholders.
* @param array $args
* An associative array of replacements to make.
* An associative array of replacements to make. Occurrences in $string of
* any key in $args are replaced with the corresponding value, after
* optional sanitization and formatting. The type of sanitization and
* formatting depends on the first character of the key:
* - @variable: Escaped to HTML using Html::escape() unless the value is
* already HTML-safe. Use this as the default choice for anything
* displayed on a page on the site, but not within HTML attributes.
* - %variable: Escaped to HTML just like @variable, but also wrapped in
* <em> tags, which makes the following HTML code:
* @code
* <em class="placeholder">text output here.</em>
* @endcode
* As with @variable, do not use this within HTML attributes.
* - :variable: Escaped to HTML using Html::escape() and filtered for
* dangerous protocols using UrlHelper::stripDangerousProtocols(). Use
* this when passing in a URL, such as when using the "src" or "href"
* attributes, ensuring the value is always wrapped in quotes:
* - Secure: <a href=":variable">@variable</a>
* - Insecure: <a href=:variable>@variable</a>
* When ":variable" comes from arbitrary user input, the result is secure,
* but not guaranteed to be a valid URL (which means the resulting output
* could fail HTML validation). To guarantee a valid URL, use
* Url::fromUri($user_input)->toString() (which either throws an exception
* or returns a well-formed URL) before passing the result into a
* ":variable" placeholder.
* - !variable: Inserted as is, with no sanitization or formatting. Only
* use this when the resulting string is being generated for one of:
* - Non-HTML usage, such as a plain-text email.
* - Non-direct HTML output, such as a plain-text variable that will be
* printed as an HTML attribute value and therefore formatted with
* self::checkPlain() as part of that.
* - Some other special reason for suppressing sanitization.
* @param bool &$safe
* A boolean indicating whether the string is safe or not (optional).
*
* @return string
* The string with the placeholders replaced.
*
* @see \Drupal\Component\Utility\SafeMarkup::format()
* @see \Drupal\Core\StringTranslation\TranslatableString::render()
* @ingroup sanitization
*
* @see \Drupal\Component\Utility\FormattableString
* @see \Drupal\Core\StringTranslation\TranslatableString
* @see \Drupal\Core\StringTranslation\PluralTranslatableString
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
* @see \Drupal\Core\Url::fromUri()
*/
protected static function placeholderFormat($string, array $args, &$safe = TRUE) {
// Transform arguments before inserting them.
......
......@@ -162,79 +162,41 @@ public static function checkPlain($text) {
/**
* Formats a string for HTML display by replacing variable placeholders.
*
* This method replaces variable placeholders in a string with the requested
* values and escapes the values so they can be safely displayed as HTML. It
* should be used on any unknown text that is intended to be printed to an
* HTML page (especially text that may have come from untrusted users, since
* in that case it prevents cross-site scripting and other security problems).
*
* This method is not intended for passing arbitrary user input into any
* HTML attribute value, as only URL attributes such as "src" and "href" are
* supported (using ":variable"). Never use this method on unsafe HTML
* attributes such as "on*" and "style" and take care when using this with
* unsupported attributes such as "title" or "alt" as this can lead to
* unexpected output.
*
* In most cases, you should use t() rather than calling this function
* directly, since it will translate the text (on non-English-only sites) in
* addition to formatting it.
*
* @param string $string
* A string containing placeholders. The string itself is not escaped, any
* unsafe content must be in $args and inserted via placeholders.
* A string containing placeholders. The string itself will not be escaped,
* any unsafe content must be in $args and inserted via placeholders.
* @param array $args
* An associative array of replacements to make. Occurrences in $string of
* any key in $args are replaced with the corresponding value, after
* optional sanitization and formatting. The type of sanitization and
* formatting depends on the first character of the key:
* - @variable: Escaped to HTML using Html::escape() unless the value is
* already HTML-safe. Use this as the default choice for anything
* displayed on a page on the site, but not within HTML attributes.
* - %variable: Escaped to HTML just like @variable, but also wrapped in
* <em> tags, which makes the following HTML code:
* @code
* <em class="placeholder">text output here.</em>
* @endcode
* As with @variable, do not use this within HTML attributes.
* - :variable: Escaped to HTML using Html::escape() and filtered for
* dangerous protocols using UrlHelper::stripDangerousProtocols(). Use
* this when passing in a URL, such as when using the "src" or "href"
* attributes, ensuring the value is always wrapped in quotes:
* - Secure: <a href=":variable">@variable</a>
* - Insecure: <a href=:variable>@variable</a>
* When ":variable" comes from arbitrary user input, the result is secure,
* but not guaranteed to be a valid URL (which means the resulting output
* could fail HTML validation). To guarantee a valid URL, use
* Url::fromUri($user_input)->toString() (which either throws an exception
* or returns a well-formed URL) before passing the result into a
* ":variable" placeholder.
* - !variable: Inserted as is, with no sanitization or formatting. Only
* use this when the resulting string is being generated for one of:
* - Non-HTML usage, such as a plain-text email.
* - Non-direct HTML output, such as a plain-text variable that will be
* printed as an HTML attribute value and therefore formatted with
* self::checkPlain() as part of that.
* - Some other special reason for suppressing sanitization.
* An array with placeholder replacements, keyed by placeholder. See
* \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
* additional information about placeholders.
*
* @return string
* The formatted string, which is marked as safe unless sanitization of an
* unsafe argument was suppressed (see above).
* @return string|\Drupal\Component\Utility\SafeStringInterface
* The formatted string, which is an instance of SafeStringInterface unless
* sanitization of an unsafe argument was suppressed (see above).
*
* @ingroup sanitization
*
* @see t()
* @see \Drupal\Component\Utility\Html::escape()
* @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
* @see \Drupal\Core\Url::fromUri()
* @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
* @see \Drupal\Component\Utility\FormattableString
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
* Use \Drupal\Component\Utility\FormattableString.
*/
public static function format($string, array $args) {
// If the string has arguments that start with '!' we consider it unsafe
// and return a string instead of an object for backward compatibility
// purposes.
// @todo https://www.drupal.org/node/2571695 remove this temporary
// workaround.
$safe = TRUE;
$output = static::placeholderFormat($string, $args, $safe);
if ($safe) {
static::$safeStrings[$output]['html'] = TRUE;
foreach ($args as $key => $value) {
if ($key[0] == '!' && !static::isSafe($value)) {
$safe = FALSE;
}
}
return $output;
$safe_string = new FormattableString($string, $args);
return $safe ? $safe_string : (string) $safe_string;
}
}
......@@ -63,18 +63,18 @@ class PluralTranslatableString extends TranslatableString {
* ease translation. Use @count in place of the item count, as in
* "@count new comments".
* @param array $args
* (optional) An associative array of replacements to make after
* translation. Instances of any key in this array are replaced with the
* corresponding value. Based on the first character of the key, the value
* is escaped and/or themed. See
* \Drupal\Component\Utility\SafeMarkup::format(). Note that you do not need
* to include @count in this array; this replacement is done automatically
* (optional) An array with placeholder replacements, keyed by placeholder.
* See \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
* additional information about placeholders. Note that you do not need to
* include @count in this array; this replacement is done automatically
* for the plural cases.
* @param array $options
* (optional) An associative array of additional options. See t() for
* allowed keys.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* (optional) The string translation service.
*
* @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
*/
public function __construct($count, $singular, $plural, array $args = [], array $options = [], TranslationInterface $string_translation = NULL) {
$this->count = $count;
......
......@@ -73,10 +73,14 @@ class TranslatableString implements SafeStringInterface {
* The string that is to be translated.
* @param array $arguments
* (optional) An array with placeholder replacements, keyed by placeholder.
* See \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat() for
* additional information about placeholders.
* @param array $options
* (optional) An array of additional options.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* (optional) The string translation service.
*
* @see \Drupal\Component\Utility\PlaceholderTrait::placeholderFormat()
*/
public function __construct($string, array $arguments = array(), array $options = array(), TranslationInterface $string_translation = NULL) {
$this->string = $string;
......
......@@ -111,11 +111,15 @@ protected function createAttributeValue($name, $value) {
}
// An array value or 'class' attribute name are forced to always be an
// AttributeArray value for consistency.
if (is_array($value) || $name == 'class') {
if ($name == 'class' && !is_array($value)) {
// Cast the value to string in case it implements SafeStringInterface.
$value = [(string) $value];
}
if (is_array($value)) {
// Cast the value to an array if the value was passed in as a string.
// @todo Decide to fix all the broken instances of class as a string
// in core or cast them.
$value = new AttributeArray($name, (array) $value);
$value = new AttributeArray($name, $value);
}
elseif (is_bool($value)) {
$value = new AttributeBoolean($name, $value);
......
......@@ -135,7 +135,7 @@ public function challengeException(Request $request, \Exception $previous) {
$challenge = SafeMarkup::format('Basic realm="@realm"', array(
'@realm' => !empty($site_name) ? $site_name : 'Access restricted',
));
return new UnauthorizedHttpException($challenge, 'No authentication credentials provided.', $previous);
return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Utility\FormattableStringTest.
*/
namespace Drupal\Tests\Component\Utility;
use Drupal\Component\Utility\FormattableString;
use Drupal\Tests\UnitTestCase;
/**
* Tests the TranslatableString class.
*
* @coversDefaultClass \Drupal\Component\Utility\FormattableString
* @group utility
*/
class FormattableStringTest extends UnitTestCase {
/**
* @covers ::__toString
* @covers ::jsonSerialize
*/
public function testToString() {
$string = 'Can I please have a @replacement';
$formattable_string = new FormattableString($string, ['@replacement' => 'kitten']);
$text = (string) $formattable_string;
$this->assertEquals('Can I please have a kitten', $text);
$text = $formattable_string->jsonSerialize();
$this->assertEquals('Can I please have a kitten', $text);
}
/**
* @covers ::count
*/
public function testCount() {
$string = 'Can I please have a @replacement';
$formattable_string = new FormattableString($string, ['@replacement' => 'kitten']);
$this->assertEquals(strlen($string), $formattable_string->count());
}
}
......@@ -427,9 +427,9 @@ public function testSetCacheAuthUser() {
* @covers ::setCache
*/
public function testSetCacheWithSafeStrings() {
// A call to SafeMarkup::format() is appropriate in this test as a way to
// add a string to the safe list in the simplest way possible.
SafeMarkup::format('@value', ['@value' => 'a_safe_string']);
SafeMarkup::setMultiple([
'a_safe_string' => ['html' => TRUE],
]);
$form_build_id = 'the_form_build_id';
$form = [
'#form_id' => 'the_form_id'
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Template\AttributeArray;
use Drupal\Core\Template\AttributeString;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\SafeStringInterface;
/**
* @coversDefaultClass \Drupal\Core\Template\Attribute
......@@ -30,6 +31,18 @@ public function testConstructor() {
$attribute = new Attribute(['selected' => TRUE, 'checked' => FALSE]);
$this->assertTrue($attribute['selected']->value());
$this->assertFalse($attribute['checked']->value());
// Test that non-array values with name "class" are cast to array.
$attribute = new Attribute(array('class' => 'example-class'));
$this->assertTrue(isset($attribute['class']));
$this->assertEquals(new AttributeArray('class', array('example-class')), $attribute['class']);
// Test that safe string objects work correctly.
$safe_string = $this->prophesize(SafeStringInterface::class);
$safe_string->__toString()->willReturn('example-class');
$attribute = new Attribute(array('class' => $safe_string->reveal()));
$this->assertTrue(isset($attribute['class']));
$this->assertEquals(new AttributeArray('class', array('example-class')), $attribute['class']);
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment