Issue #2550945 by alexpott, joelpittet, xjm, Wim Leers, stefan.r: Add Html::escape()

8.0.x
Nathaniel Catchpole 2015-08-17 08:34:08 +01:00
parent 9594553dee
commit a5bfd122e1
13 changed files with 121 additions and 17 deletions

View File

@ -338,14 +338,54 @@ EOD;
* "&lt;", not "<"). Be careful when using this function, as it will revert
* previous sanitization efforts (&lt;script&gt; will become <script>).
*
* This method is not the opposite of Html::escape(). For example, this method
* will convert "&eacute;" to "é", whereas Html::escape() will not convert "é"
* to "&eacute;".
*
* @param string $text
* The text to decode entities in.
*
* @return string
* The input $text, with all HTML entities decoded once.
*
* @see html_entity_decode()
* @see \Drupal\Component\Utility\Html::escape()
*/
public static function decodeEntities($text) {
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 &amp;
* - " (double quote) becomes &quot;
* - ' (single quote) becomes &#039;
* - < (less than) becomes &lt;
* - > (greater than) becomes &gt;
* Special characters that have already been escaped will be double-escaped
* (for example, "&lt;" becomes "&amp;lt;").
*
* This method is not the opposite of Html::decodeEntities(). For example,
* this method will not encode "é" to "&eacute;", whereas
* Html::decodeEntities() will convert all HTML entities to UTF-8 bytes,
* including "&eacute;" and "&lt;" 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');
}
}

View File

@ -223,7 +223,7 @@ class SafeMarkup {
* @see drupal_validate_utf8()
*/
public static function checkPlain($text) {
$string = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$string = Html::escape($text);
static::$safeStrings[$string]['html'] = TRUE;
return $string;
}

View File

@ -7,6 +7,7 @@
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\SafeString;
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
// ensure this cannot be abused.
$escaped_tag = htmlspecialchars($element['#tag'], ENT_QUOTES, 'UTF-8');
$escaped_tag = Html::escape($element['#tag']);
$markup = '<' . $escaped_tag . $attributes;
// Construct a void element.
if (in_array($element['#tag'], self::$voidElements)) {

View File

@ -40,7 +40,9 @@ use Drupal\Component\Utility\SafeStringInterface;
* @endcode
*
* 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 {

View File

@ -7,6 +7,8 @@
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.
*
@ -74,7 +76,7 @@ class AttributeArray extends AttributeValueBase implements \ArrayAccess, \Iterat
public function __toString() {
// Filter out any empty values before printing.
$this->value = array_unique(array_filter($this->value));
return htmlspecialchars(implode(' ', $this->value), ENT_QUOTES, 'UTF-8');
return Html::escape(implode(' ', $this->value));
}
/**

View File

@ -7,6 +7,8 @@
namespace Drupal\Core\Template;
use Drupal\Component\Utility\Html;
/**
* A class that defines a type of boolean HTML attribute.
*
@ -40,7 +42,7 @@ class AttributeBoolean extends AttributeValueBase {
* Implements the magic __toString() method.
*/
public function __toString() {
return $this->value === FALSE ? '' : htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
return $this->value === FALSE ? '' : Html::escape($this->name);
}
}

View File

@ -7,6 +7,8 @@
namespace Drupal\Core\Template;
use Drupal\Component\Utility\Html;
/**
* A class that represents most standard HTML attributes.
*
@ -28,7 +30,7 @@ class AttributeString extends AttributeValueBase {
* Implements the magic __toString() method.
*/
public function __toString() {
return htmlspecialchars($this->value, ENT_QUOTES, 'UTF-8');
return Html::escape($this->value);
}
}

View File

@ -6,6 +6,7 @@
*/
namespace Drupal\Core\Template;
use Drupal\Component\Utility\Html;
/**
* Defines the base class for an attribute type.
@ -55,7 +56,7 @@ abstract class AttributeValueBase {
public function render() {
$value = (string) $this;
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 . '"';
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\aggregator\Tests;
use Drupal\aggregator\Entity\Feed;
use Drupal\Component\Utility\Html;
use Drupal\simpletest\WebTestBase;
use Drupal\aggregator\FeedInterface;
@ -243,7 +244,7 @@ abstract class AggregatorTestBase extends WebTestBase {
public function getValidOpml(array $feeds) {
// Properly escape URLs so that XML parsers don't choke on them.
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.

View File

@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Menu;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
@ -30,8 +31,8 @@ class LocalActionTest extends WebTestBase {
// Ensure that both menu and route based actions are shown.
$this->assertLocalAction([
[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'), 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 jungle!')</script>")],
[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_action3'), 'My YAML discovery action'],
[Url::fromRoute('menu_test.local_action5'), 'Title override'],

View File

@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Menu;
use Drupal\Component\Utility\Html;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
@ -78,9 +79,9 @@ class LocalTasksTest extends WebTestBase {
]);
// 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);
$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);
// 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', []],
]);
$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);
// Ensure the view tab is active.

View File

@ -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('&lt;script&gt;', '<script>'),
array('&amp;lt;script&amp;gt;', '&lt;script&gt;'),
array('&amp;#34;', '&#34;'),
array('&quot;', '"'),
array('&amp;quot;', '&quot;'),
array('&#039;', "'"),
array('&amp;#039;', '&#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&eacute;</em>";
$escaped = Html::escape($string);
$this->assertSame('&lt;em&gt;répét&amp;eacute;&lt;/em&gt;', $escaped);
$decoded = Html::decodeEntities($escaped);
$this->assertSame('<em>répét&eacute;</em>', $decoded);
$decoded = Html::decodeEntities($decoded);
$this->assertSame('<em>répété</em>', $decoded);
$escaped = Html::escape($decoded);
$this->assertSame('&lt;em&gt;répété&lt;/em&gt;', $escaped);
}
}

View File

@ -5,6 +5,7 @@
* Handles integration of Twig templates with the Drupal theme system.
*/
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\SafeString;
use Drupal\Core\Extension\Extension;
@ -75,7 +76,7 @@ function twig_render_template($template_file, array $variables) {
}
if ($twig_service->isDebug()) {
$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
// suggestions are shown first.
if (!empty($variables['theme_hook_suggestions'])) {
@ -109,10 +110,10 @@ function twig_render_template($template_file, array $variables) {
$prefix = ($template == $current_template) ? 'x' : '*';
$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_suffix'] .= "\n<!-- END OUTPUT from '" . htmlspecialchars($template_file, ENT_QUOTES, 'UTF-8') . "' -->\n\n";
$output['debug_info'] .= "\n<!-- BEGIN OUTPUT from '" . Html::escape($template_file) . "' -->\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.
return SafeString::create(implode('', $output));