Issue #2550945 by alexpott, joelpittet, xjm, Wim Leers, stefan.r: Add Html::escape()
parent
9594553dee
commit
a5bfd122e1
|
@ -338,14 +338,54 @@ EOD;
|
||||||
* "<", not "<"). Be careful when using this function, as it will revert
|
* "<", not "<"). Be careful when using this function, as it will revert
|
||||||
* previous sanitization efforts (<script> will become <script>).
|
* previous sanitization efforts (<script> will become <script>).
|
||||||
*
|
*
|
||||||
|
* This method is not the opposite of Html::escape(). For example, this method
|
||||||
|
* will convert "é" to "é", whereas Html::escape() will not convert "é"
|
||||||
|
* to "é".
|
||||||
|
*
|
||||||
* @param string $text
|
* @param string $text
|
||||||
* The text to decode entities in.
|
* The text to decode entities in.
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
* The input $text, with all HTML entities decoded once.
|
* The input $text, with all HTML entities decoded once.
|
||||||
|
*
|
||||||
|
* @see html_entity_decode()
|
||||||
|
* @see \Drupal\Component\Utility\Html::escape()
|
||||||
*/
|
*/
|
||||||
public static function decodeEntities($text) {
|
public static function decodeEntities($text) {
|
||||||
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes text by converting special characters to HTML entities.
|
||||||
|
*
|
||||||
|
* This method escapes HTML for sanitization purposes by replacing the
|
||||||
|
* following special characters with their HTML entity equivalents:
|
||||||
|
* - & (ampersand) becomes &
|
||||||
|
* - " (double quote) becomes "
|
||||||
|
* - ' (single quote) becomes '
|
||||||
|
* - < (less than) becomes <
|
||||||
|
* - > (greater than) becomes >
|
||||||
|
* Special characters that have already been escaped will be double-escaped
|
||||||
|
* (for example, "<" becomes "&lt;").
|
||||||
|
*
|
||||||
|
* This method is not the opposite of Html::decodeEntities(). For example,
|
||||||
|
* this method will not encode "é" to "é", whereas
|
||||||
|
* Html::decodeEntities() will convert all HTML entities to UTF-8 bytes,
|
||||||
|
* including "é" and "<" to "é" and "<".
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* The input text.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The text with all HTML special characters converted.
|
||||||
|
*
|
||||||
|
* @see htmlspecialchars()
|
||||||
|
* @see \Drupal\Component\Utility\Html::decodeEntities()
|
||||||
|
*
|
||||||
|
* @ingroup sanitization
|
||||||
|
*/
|
||||||
|
public static function escape($text) {
|
||||||
|
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,7 @@ class SafeMarkup {
|
||||||
* @see drupal_validate_utf8()
|
* @see drupal_validate_utf8()
|
||||||
*/
|
*/
|
||||||
public static function checkPlain($text) {
|
public static function checkPlain($text) {
|
||||||
$string = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
$string = Html::escape($text);
|
||||||
static::$safeStrings[$string]['html'] = TRUE;
|
static::$safeStrings[$string]['html'] = TRUE;
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Render\Element;
|
namespace Drupal\Core\Render\Element;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Component\Utility\SafeMarkup;
|
use Drupal\Component\Utility\SafeMarkup;
|
||||||
use Drupal\Core\Render\SafeString;
|
use Drupal\Core\Render\SafeString;
|
||||||
use Drupal\Component\Utility\Xss;
|
use Drupal\Component\Utility\Xss;
|
||||||
|
@ -78,7 +79,7 @@ class HtmlTag extends RenderElement {
|
||||||
|
|
||||||
// An HTML tag should not contain any special characters. Escape them to
|
// An HTML tag should not contain any special characters. Escape them to
|
||||||
// ensure this cannot be abused.
|
// ensure this cannot be abused.
|
||||||
$escaped_tag = htmlspecialchars($element['#tag'], ENT_QUOTES, 'UTF-8');
|
$escaped_tag = Html::escape($element['#tag']);
|
||||||
$markup = '<' . $escaped_tag . $attributes;
|
$markup = '<' . $escaped_tag . $attributes;
|
||||||
// Construct a void element.
|
// Construct a void element.
|
||||||
if (in_array($element['#tag'], self::$voidElements)) {
|
if (in_array($element['#tag'], self::$voidElements)) {
|
||||||
|
|
|
@ -40,7 +40,9 @@ use Drupal\Component\Utility\SafeStringInterface;
|
||||||
* @endcode
|
* @endcode
|
||||||
*
|
*
|
||||||
* The attribute keys and values are automatically sanitized for output with
|
* The attribute keys and values are automatically sanitized for output with
|
||||||
* htmlspecialchars() and the entire attribute string is marked safe for output.
|
* Html::escape() and the entire attribute string is marked safe for output.
|
||||||
|
*
|
||||||
|
* @see \Drupal\Component\Utility\Html::escape()
|
||||||
*/
|
*/
|
||||||
class Attribute implements \ArrayAccess, \IteratorAggregate, SafeStringInterface {
|
class Attribute implements \ArrayAccess, \IteratorAggregate, SafeStringInterface {
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Template;
|
namespace Drupal\Core\Template;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that defines a type of Attribute that can be added to as an array.
|
* A class that defines a type of Attribute that can be added to as an array.
|
||||||
*
|
*
|
||||||
|
@ -74,7 +76,7 @@ class AttributeArray extends AttributeValueBase implements \ArrayAccess, \Iterat
|
||||||
public function __toString() {
|
public function __toString() {
|
||||||
// Filter out any empty values before printing.
|
// Filter out any empty values before printing.
|
||||||
$this->value = array_unique(array_filter($this->value));
|
$this->value = array_unique(array_filter($this->value));
|
||||||
return htmlspecialchars(implode(' ', $this->value), ENT_QUOTES, 'UTF-8');
|
return Html::escape(implode(' ', $this->value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Template;
|
namespace Drupal\Core\Template;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that defines a type of boolean HTML attribute.
|
* A class that defines a type of boolean HTML attribute.
|
||||||
*
|
*
|
||||||
|
@ -40,7 +42,7 @@ class AttributeBoolean extends AttributeValueBase {
|
||||||
* Implements the magic __toString() method.
|
* Implements the magic __toString() method.
|
||||||
*/
|
*/
|
||||||
public function __toString() {
|
public function __toString() {
|
||||||
return $this->value === FALSE ? '' : htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
return $this->value === FALSE ? '' : Html::escape($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
namespace Drupal\Core\Template;
|
namespace Drupal\Core\Template;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that represents most standard HTML attributes.
|
* A class that represents most standard HTML attributes.
|
||||||
*
|
*
|
||||||
|
@ -28,7 +30,7 @@ class AttributeString extends AttributeValueBase {
|
||||||
* Implements the magic __toString() method.
|
* Implements the magic __toString() method.
|
||||||
*/
|
*/
|
||||||
public function __toString() {
|
public function __toString() {
|
||||||
return htmlspecialchars($this->value, ENT_QUOTES, 'UTF-8');
|
return Html::escape($this->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Drupal\Core\Template;
|
namespace Drupal\Core\Template;
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the base class for an attribute type.
|
* Defines the base class for an attribute type.
|
||||||
|
@ -55,7 +56,7 @@ abstract class AttributeValueBase {
|
||||||
public function render() {
|
public function render() {
|
||||||
$value = (string) $this;
|
$value = (string) $this;
|
||||||
if (isset($this->value) && static::RENDER_EMPTY_ATTRIBUTE || !empty($value)) {
|
if (isset($this->value) && static::RENDER_EMPTY_ATTRIBUTE || !empty($value)) {
|
||||||
return htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8') . '="' . $value . '"';
|
return Html::escape($this->name) . '="' . $value . '"';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
namespace Drupal\aggregator\Tests;
|
namespace Drupal\aggregator\Tests;
|
||||||
|
|
||||||
use Drupal\aggregator\Entity\Feed;
|
use Drupal\aggregator\Entity\Feed;
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\simpletest\WebTestBase;
|
use Drupal\simpletest\WebTestBase;
|
||||||
use Drupal\aggregator\FeedInterface;
|
use Drupal\aggregator\FeedInterface;
|
||||||
|
|
||||||
|
@ -243,7 +244,7 @@ abstract class AggregatorTestBase extends WebTestBase {
|
||||||
public function getValidOpml(array $feeds) {
|
public function getValidOpml(array $feeds) {
|
||||||
// Properly escape URLs so that XML parsers don't choke on them.
|
// Properly escape URLs so that XML parsers don't choke on them.
|
||||||
foreach ($feeds as &$feed) {
|
foreach ($feeds as &$feed) {
|
||||||
$feed['url[0][value]'] = htmlspecialchars($feed['url[0][value]']);
|
$feed['url[0][value]'] = Html::escape($feed['url[0][value]']);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Does not have an XML declaration, must pass the parser.
|
* Does not have an XML declaration, must pass the parser.
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
namespace Drupal\system\Tests\Menu;
|
namespace Drupal\system\Tests\Menu;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
use Drupal\simpletest\WebTestBase;
|
use Drupal\simpletest\WebTestBase;
|
||||||
|
|
||||||
|
@ -30,8 +31,8 @@ class LocalActionTest extends WebTestBase {
|
||||||
// Ensure that both menu and route based actions are shown.
|
// Ensure that both menu and route based actions are shown.
|
||||||
$this->assertLocalAction([
|
$this->assertLocalAction([
|
||||||
[Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
|
[Url::fromRoute('menu_test.local_action4'), 'My dynamic-title action'],
|
||||||
[Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
|
[Url::fromRoute('menu_test.local_action4'), Html::escape("<script>alert('Welcome to the jungle!')</script>")],
|
||||||
[Url::fromRoute('menu_test.local_action4'), htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')],
|
[Url::fromRoute('menu_test.local_action4'), Html::escape("<script>alert('Welcome to the derived jungle!')</script>")],
|
||||||
[Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'],
|
[Url::fromRoute('menu_test.local_action2'), 'My hook_menu action'],
|
||||||
[Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'],
|
[Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'],
|
||||||
[Url::fromRoute('menu_test.local_action5'), 'Title override'],
|
[Url::fromRoute('menu_test.local_action5'), 'Title override'],
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
namespace Drupal\system\Tests\Menu;
|
namespace Drupal\system\Tests\Menu;
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
use Drupal\simpletest\WebTestBase;
|
use Drupal\simpletest\WebTestBase;
|
||||||
|
|
||||||
|
@ -78,9 +79,9 @@ class LocalTasksTest extends WebTestBase {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Verify that script tags are escaped on output.
|
// Verify that script tags are escaped on output.
|
||||||
$title = htmlspecialchars("Task 1 <script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$title = Html::escape("Task 1 <script>alert('Welcome to the jungle!')</script>");
|
||||||
$this->assertLocalTaskAppers($title);
|
$this->assertLocalTaskAppers($title);
|
||||||
$title = htmlspecialchars("<script>alert('Welcome to the derived jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$title = Html::escape("<script>alert('Welcome to the derived jungle!')</script>");
|
||||||
$this->assertLocalTaskAppers($title);
|
$this->assertLocalTaskAppers($title);
|
||||||
|
|
||||||
// Verify that local tasks appear as defined in the router.
|
// Verify that local tasks appear as defined in the router.
|
||||||
|
@ -92,7 +93,7 @@ class LocalTasksTest extends WebTestBase {
|
||||||
['menu_test.local_task_test_tasks_settings_dynamic', []],
|
['menu_test.local_task_test_tasks_settings_dynamic', []],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$title = htmlspecialchars("<script>alert('Welcome to the jungle!')</script>", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$title = Html::escape("<script>alert('Welcome to the jungle!')</script>");
|
||||||
$this->assertLocalTaskAppers($title);
|
$this->assertLocalTaskAppers($title);
|
||||||
|
|
||||||
// Ensure the view tab is active.
|
// Ensure the view tab is active.
|
||||||
|
|
|
@ -259,4 +259,54 @@ class HtmlTest extends UnitTestCase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests Html::escape().
|
||||||
|
*
|
||||||
|
* @dataProvider providerEscape
|
||||||
|
* @covers ::escape
|
||||||
|
*/
|
||||||
|
public function testEscape($expected, $text) {
|
||||||
|
$this->assertEquals($expected, Html::escape($text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for testEscape().
|
||||||
|
*
|
||||||
|
* @see testCheckPlain()
|
||||||
|
*/
|
||||||
|
public function providerEscape() {
|
||||||
|
return array(
|
||||||
|
array('Drupal', 'Drupal'),
|
||||||
|
array('<script>', '<script>'),
|
||||||
|
array('&lt;script&gt;', '<script>'),
|
||||||
|
array('&#34;', '"'),
|
||||||
|
array('"', '"'),
|
||||||
|
array('&quot;', '"'),
|
||||||
|
array(''', "'"),
|
||||||
|
array('&#039;', '''),
|
||||||
|
array('©', '©'),
|
||||||
|
array('→', '→'),
|
||||||
|
array('➼', '➼'),
|
||||||
|
array('€', '€'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests relationship between escaping and decoding HTML entities.
|
||||||
|
*
|
||||||
|
* @covers ::decodeEntities
|
||||||
|
* @covers ::escape
|
||||||
|
*/
|
||||||
|
public function testDecodeEntitiesAndEscape() {
|
||||||
|
$string = "<em>répété</em>";
|
||||||
|
$escaped = Html::escape($string);
|
||||||
|
$this->assertSame('<em>répét&eacute;</em>', $escaped);
|
||||||
|
$decoded = Html::decodeEntities($escaped);
|
||||||
|
$this->assertSame('<em>répété</em>', $decoded);
|
||||||
|
$decoded = Html::decodeEntities($decoded);
|
||||||
|
$this->assertSame('<em>répété</em>', $decoded);
|
||||||
|
$escaped = Html::escape($decoded);
|
||||||
|
$this->assertSame('<em>répété</em>', $escaped);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* Handles integration of Twig templates with the Drupal theme system.
|
* Handles integration of Twig templates with the Drupal theme system.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Drupal\Component\Utility\Html;
|
||||||
use Drupal\Component\Utility\SafeMarkup;
|
use Drupal\Component\Utility\SafeMarkup;
|
||||||
use Drupal\Core\Render\SafeString;
|
use Drupal\Core\Render\SafeString;
|
||||||
use Drupal\Core\Extension\Extension;
|
use Drupal\Core\Extension\Extension;
|
||||||
|
@ -75,7 +76,7 @@ function twig_render_template($template_file, array $variables) {
|
||||||
}
|
}
|
||||||
if ($twig_service->isDebug()) {
|
if ($twig_service->isDebug()) {
|
||||||
$output['debug_prefix'] .= "\n\n<!-- THEME DEBUG -->";
|
$output['debug_prefix'] .= "\n\n<!-- THEME DEBUG -->";
|
||||||
$output['debug_prefix'] .= "\n<!-- THEME HOOK: '" . htmlspecialchars($variables['theme_hook_original'], ENT_QUOTES, 'UTF-8') . "' -->";
|
$output['debug_prefix'] .= "\n<!-- THEME HOOK: '" . Html::escape($variables['theme_hook_original']) . "' -->";
|
||||||
// If there are theme suggestions, reverse the array so more specific
|
// If there are theme suggestions, reverse the array so more specific
|
||||||
// suggestions are shown first.
|
// suggestions are shown first.
|
||||||
if (!empty($variables['theme_hook_suggestions'])) {
|
if (!empty($variables['theme_hook_suggestions'])) {
|
||||||
|
@ -109,10 +110,10 @@ function twig_render_template($template_file, array $variables) {
|
||||||
$prefix = ($template == $current_template) ? 'x' : '*';
|
$prefix = ($template == $current_template) ? 'x' : '*';
|
||||||
$suggestion = $prefix . ' ' . $template;
|
$suggestion = $prefix . ' ' . $template;
|
||||||
}
|
}
|
||||||
$output['debug_info'] .= "\n<!-- FILE NAME SUGGESTIONS:\n " . htmlspecialchars(implode("\n ", $suggestions), ENT_QUOTES, 'UTF-8') . "\n-->";
|
$output['debug_info'] .= "\n<!-- FILE NAME SUGGESTIONS:\n " . Html::escape(implode("\n ", $suggestions)) . "\n-->";
|
||||||
}
|
}
|
||||||
$output['debug_info'] .= "\n<!-- BEGIN OUTPUT from '" . htmlspecialchars($template_file, ENT_QUOTES, 'UTF-8') . "' -->\n";
|
$output['debug_info'] .= "\n<!-- BEGIN OUTPUT from '" . Html::escape($template_file) . "' -->\n";
|
||||||
$output['debug_suffix'] .= "\n<!-- END OUTPUT from '" . htmlspecialchars($template_file, ENT_QUOTES, 'UTF-8') . "' -->\n\n";
|
$output['debug_suffix'] .= "\n<!-- END OUTPUT from '" . Html::escape($template_file) . "' -->\n\n";
|
||||||
}
|
}
|
||||||
// This output has already been rendered and is therefore considered safe.
|
// This output has already been rendered and is therefore considered safe.
|
||||||
return SafeString::create(implode('', $output));
|
return SafeString::create(implode('', $output));
|
||||||
|
|
Loading…
Reference in New Issue