diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 7ab2059..9d482cc 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -387,7 +387,7 @@ function ajax_pre_render_element($element) { // uniquely, in which case a match on value is also needed. // @see _form_button_was_clicked() if (!empty($element['#is_button']) && empty($element['#has_garbage_value'])) { - $settings['submit']['_triggering_element_value'] = $element['#value']; + $settings['submit']['_triggering_element_value'] = (string) $element['#value']; } } diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 3d41fcb..bf25d25 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -16,6 +16,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Site\Settings; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Utility\Title; use Drupal\Core\Utility\Error; use Symfony\Component\ClassLoader\ApcClassLoader; @@ -1180,6 +1181,9 @@ function watchdog($type, $message, array $variables = array(), $severity = WATCH */ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) { if ($message) { + if ($message instanceof SafeMarkup) { + $message = chr(0) . $message; + } if (!isset($_SESSION['messages'][$type])) { $_SESSION['messages'][$type] = array(); } @@ -1223,6 +1227,13 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) */ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { if ($messages = drupal_set_message()) { + foreach ($messages as $message_type => $messages_typed) { + foreach ($messages_typed as $key => $message) { + if (!$message instanceof SafeMarkup && $message[0] == chr(0)) { + $messages[$message_type][$key] = new SafeMarkup(substr($message, 1)); + } + } + } if ($type) { if ($clear_queue) { unset($_SESSION['messages'][$type]); diff --git a/core/includes/common.inc b/core/includes/common.inc index 5572e54..c2b1687 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -29,6 +29,7 @@ use Drupal\Core\EventSubscriber\HtmlViewSubscriber; use Drupal\Core\Routing\GeneratorNotInitializedException; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Render\Element; use Drupal\Core\Session\AnonymousUserSession; @@ -886,8 +887,7 @@ function l($text, $path, array $options = array()) { // Sanitize the link text if necessary. $text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); - - return '' . $text . ''; + return new SafeMarkup('' . $text . ''); } /** @@ -1440,6 +1440,9 @@ function drupal_html_class($class) { // static instead of drupal_static(). static $classes = array(); + // @todo Needs safe makrup preservation or should be cast to string before + // arg is passed to this function? + $class = (string) $class; if (!isset($classes[$class])) { $classes[$class] = drupal_clean_css_identifier(drupal_strtolower($class)); } @@ -2829,6 +2832,7 @@ function drupal_pre_render_conditional_comments($elements) { * - meta: To provide meta information, such as a page refresh. * - link: To refer to stylesheets and other contextual information. * - script: To load JavaScript. + * This is not HTML escaped, do not pass in user input. * - #attributes: (optional) An array of HTML attributes to apply to the * tag. * - #value: (optional) A string containing tag content, such as inline @@ -2841,7 +2845,9 @@ function drupal_pre_render_conditional_comments($elements) { function drupal_pre_render_html_tag($element) { $attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : ''; if (!isset($element['#value'])) { - $markup = '<' . $element['#tag'] . $attributes . " />\n"; + // Attributes are safe and we are assuming people don't use this function + // and second they don't pass unsafe variables to #tag. + $markup = new SafeMarkup('<' . $element['#tag'] . $attributes . " />\n"); } else { $markup = '<' . $element['#tag'] . $attributes . '>'; @@ -2853,6 +2859,8 @@ function drupal_pre_render_html_tag($element) { $markup .= $element['#value_suffix']; } $markup .= '\n"; + // @TODO Creating safe markup, avoid if possible! + $markup = new SafeMarkup($markup); } if (!empty($element['#noscript'])) { $element['#markup'] = ''; @@ -3261,7 +3269,7 @@ function drupal_render_page($page) { * @param bool $is_recursive_call * Whether this is a recursive call or not, for internal use. * - * @return string + * @return \Drupal\Core\Template\SafeMarkup * The rendered HTML. * * @see element_info() @@ -3286,6 +3294,7 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { $cached_element = drupal_render_cache_get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; + $elements['#markup'] = new SafeMarkup($elements['#markup']); // Only when we're not in a recursive drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking the // render cache in case of nested elements with #cache set. @@ -3336,6 +3345,33 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { $elements['#children'] = ''; } + // Assume still safe unless the element or it's __toString return value aren't + // instances of Twig_Markup. + $get_string = function ($element) { + if (is_object($element) && method_exists($element, '__toString')) { + $markup = $element->__toString(); + } + else { + $markup = $element; + } + // This is necessary because $element can be a RenderWrapper. + if ($markup && !$element instanceof SafeMarkup && !$markup instanceof SafeMarkup) { + $markup = String::checkPlain($markup); + } + return $markup; + }; + + // @TODO: remove wrapping of #markup. + if (isset($elements['#markup'])) { + if (is_object($elements['#markup']) && method_exists($elements['#markup'], '__toString')) { + $markup = $elements['#markup']->__toString(); + } + else { + $markup = $elements['#markup']; + } + $elements['#markup'] = new SafeMarkup($markup); + } + // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); @@ -3357,8 +3393,9 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { // process as drupal_render_children() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { - $elements['#children'] .= drupal_render($elements[$key], TRUE); + $elements['#children'] .= $get_string(drupal_render($elements[$key], TRUE)); } + $elements['#children'] = new SafeMarkup($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a @@ -3369,7 +3406,7 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { - $elements['#children'] = $elements['#markup'] . $elements['#children']; + $elements['#children'] = new SafeMarkup($get_string($elements['#markup']) . $get_string($elements['#children'])); } // Let the theme functions in #theme_wrappers add markup around the rendered @@ -3453,6 +3490,7 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { } $elements['#printed'] = TRUE; + $elements['#markup'] = new SafeMarkup($elements['#markup']); return $elements['#markup']; } @@ -3466,7 +3504,7 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { * can be passed in to save another run of * \Drupal\Core\Render\Element::children(). * - * @return string + * @return \Drupal\Core\Template\SafeMarkup * The rendered HTML of all children of the element. * @see drupal_render() @@ -3475,13 +3513,13 @@ function drupal_render_children(&$element, $children_keys = NULL) { if ($children_keys === NULL) { $children_keys = Element::children($element); } - $output = ''; + $markups = array(); foreach ($children_keys as $key) { if (!empty($element[$key])) { - $output .= drupal_render($element[$key]); + $markups[] = drupal_render($element[$key]); } } - return $output; + return SafeMarkup::implode('', $markups); } /** diff --git a/core/includes/form.inc b/core/includes/form.inc index 42206a4..9464fe9 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -15,6 +15,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Utility\Color; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -983,7 +984,7 @@ function form_select_options($element, $choices = NULL) { $options .= form_select_options($element, $choice); $options .= ''; } - elseif (is_object($choice)) { + elseif (is_object($choice) && isset($choice->option)) { $options .= form_select_options($element, $choice->option); } else { @@ -998,7 +999,7 @@ function form_select_options($element, $choices = NULL) { $options .= ''; } } - return $options; + return new SafeMarkup($options); } /** @@ -1659,7 +1660,7 @@ function theme_tableselect($variables) { foreach ($element['#header'] as $fieldname => $title) { // A row cell can span over multiple headers, which means less row cells // than headers could be present. - if (isset($element['#options'][$key][$fieldname])) { + if (isset($element['#options'][$key][$fieldname]) && is_array($element['#options'][$key][$fieldname])) { // A header can span over multiple cells and in this case the cells // are passed in an array. The order of this array determines the // order in which they are added. @@ -1976,7 +1977,7 @@ function form_process_machine_name($element, &$form_state) { $element['#machine_name'] += array( 'source' => array('label'), 'target' => '#' . $element['#id'], - 'label' => t('Machine name'), + 'label' => (string) t('Machine name'), 'replace_pattern' => '[^a-z0-9_]+', 'replace' => '_', 'standalone' => FALSE, @@ -2009,13 +2010,13 @@ function form_process_machine_name($element, &$form_state) { $element['#machine_name']['suffix'] = '#' . $suffix_id; if ($element['#machine_name']['standalone']) { - $element['#suffix'] .= '  '; + $element['#suffix'] = SafeMarkup::concat($element['#suffix'], new SafeMarkup('  ')); } else { // Append a field suffix to the source form element, which will contain // the live preview of the machine name. $source += array('#field_suffix' => ''); - $source['#field_suffix'] .= '  '; + $source['#field_suffix'] = SafeMarkup::concat($source['#field_suffix'], new SafeMarkup('  ')); $parents = array_merge($element['#machine_name']['source'], array('#field_suffix')); NestedArray::setValue($form_state['complete_form'], $parents, $source['#field_suffix']); @@ -2348,6 +2349,9 @@ function template_preprocess_input(&$variables) { function theme_input($variables) { $element = $variables['element']; $attributes = $variables['attributes']; + if (!$attributes instanceof Attribute) { + throw new \LogicException('Attributes are expected to be an instance of attributes'); + } return '' . drupal_render_children($element); } @@ -2976,8 +2980,6 @@ function theme_form_element_label($variables) { return ''; } - $title = Xss::filterAdmin($element['#title']); - $attributes = array(); // Style the label as class option to display inline with the element. if ($element['#title_display'] == 'after') { @@ -3008,6 +3010,8 @@ function theme_form_element_label($variables) { $attributes['class'][] = 'form-required'; } + $title = Xss::filterAdmin($element['#title']); + return '' . $title . ''; } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 2b06429..0c81cb8 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -21,6 +21,7 @@ use Drupal\Core\Page\MetaElement; use Drupal\Core\Template\Attribute; use Drupal\Core\Template\RenderWrapper; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Theme\ThemeSettings; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Render\Element; @@ -579,7 +580,7 @@ function _theme($hook, $variables = array()) { $output = ''; if (isset($info['function'])) { if (function_exists($info['function'])) { - $output = $info['function']($variables); + $output = new SafeMarkup($info['function']($variables)); } } else { @@ -650,7 +651,7 @@ function _theme($hook, $variables = array()) { // restore path_to_theme() $theme_path = $temp; - return (string) $output; + return $output; } /** @@ -2053,7 +2054,10 @@ function template_preprocess_html(&$variables) { // Wrap function calls in an object so they can be called when printed. $variables['head'] = new RenderWrapper(function() use ($page) { - return implode("\n", $page->getMetaElements()) . implode("\n", $page->getLinkElements()); + // HeadElement itself is safe and MetaElement and LinkElement both extend + // it so each element are safe. The result of concatenating a lot of them + // together is also safe. + return new SafeMarkup(implode("\n", $page->getMetaElements()) . implode("\n", $page->getLinkElements())); }); $variables['styles'] = new RenderWrapper('drupal_get_css'); $variables['scripts'] = new RenderWrapper('drupal_get_js'); diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 8a19a49..09957df 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -7,6 +7,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Site\Settings; +use Drupal\Core\Template\SafeMarkup; /** * Sets up the theming system for maintenance page. @@ -110,6 +111,8 @@ function _drupal_maintenance_theme() { * @param $variables * An associative array containing: * - items: An associative array of maintenance tasks. + * It's the caller's responsibility to ensure this array's items contains no + * XSS holes. * - active: The key for the currently active maintenance task. * * @ingroup themeable @@ -178,6 +181,7 @@ function theme_authorize_report($variables) { } $output .= ''; } + return $output; } @@ -187,6 +191,8 @@ function theme_authorize_report($variables) { * @param $variables * An associative array containing: * - message: The log message. + * It's the caller's responsibility to ensure this string contains no + * XSS holes. * - success: A boolean indicating failure or success. * * @ingroup themeable diff --git a/core/lib/Drupal/Component/Utility/String.php b/core/lib/Drupal/Component/Utility/String.php index 9796a5f..7af8d67 100644 --- a/core/lib/Drupal/Component/Utility/String.php +++ b/core/lib/Drupal/Component/Utility/String.php @@ -7,6 +7,8 @@ namespace Drupal\Component\Utility; +use Drupal\Core\Template\SafeMarkup; + /** * Provides helpers to operate on strings. * @@ -22,7 +24,7 @@ class String { * @param string $text * The text to be checked or processed. * - * @return string + * @return \Drupal\Core\Template\SafeMarkup * An HTML safe version of $text, or an empty string if $text is not * valid UTF-8. * @@ -31,7 +33,7 @@ class String { * @see drupal_validate_utf8() */ public static function checkPlain($text) { - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + return new SafeMarkup(htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8')); } /** @@ -65,7 +67,8 @@ public static function decodeEntities($text) { * addition to formatting it. * * @param $string - * A string containing placeholders. + * A string containing placeholders. The string itself is not escaped, any + * unsafe content must be in $args and inserted via placeholders. * @param $args * An associative array of replacements to make. Occurrences in $string of * any key in $args are replaced with the corresponding value, after @@ -108,7 +111,7 @@ public static function format($string, array $args = array()) { // Pass-through. } } - return strtr($string, $args); + return new SafeMarkup(strtr($string, $args)); } /** @@ -123,7 +126,8 @@ public static function format($string, array $args = array()) { * The formatted text (html). */ public static function placeholder($text) { - return '' . static::checkPlain($text) . ''; + return new SafeMarkup('' . static::checkPlain($text) . ''); } + } diff --git a/core/lib/Drupal/Component/Utility/Xss.php b/core/lib/Drupal/Component/Utility/Xss.php index dc49913..dd0df76 100644 --- a/core/lib/Drupal/Component/Utility/Xss.php +++ b/core/lib/Drupal/Component/Utility/Xss.php @@ -7,6 +7,8 @@ namespace Drupal\Component\Utility; +use Drupal\Core\Template\SafeMarkup; + /** * Provides helper to filter for cross-site scripting. * @@ -90,7 +92,7 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', ' $splitter = function ($matches) use ($html_tags, $mode) { return static::split($matches[1], $html_tags, $mode); }; - return preg_replace_callback('% + return new SafeMarkup(preg_replace_callback('% ( <(?=[^a-zA-Z!/]) # a lone < | # or @@ -99,7 +101,7 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', ' <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string | # or > # just a > - )%x', $splitter, $string); + )%x', $splitter, $string)); } /** diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php index eac0b62..b2f87df 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Ajax; use Drupal\Core\Page\HtmlFragment; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\HttpFoundation\Response; /** @@ -36,9 +37,9 @@ public function render($content) { $content = $content->getContent(); } // Most controllers return a render array, but some return a string. - if (!is_array($content)) { + if (is_string($content) || $content instanceof SafeMarkup) { $content = array( - '#markup' => $content, + '#markup' => (string) $content, ); } @@ -74,11 +75,9 @@ public function render($content) { /** * Wraps drupal_render(). - * - * @todo: Remove as part of https://drupal.org/node/2182149 */ protected function drupalRender(&$elements, $is_recursive_call = FALSE) { - return drupal_render($elements, $is_recursive_call); + return (string) drupal_render($elements, $is_recursive_call); } /** diff --git a/core/lib/Drupal/Core/Ajax/InsertCommand.php b/core/lib/Drupal/Core/Ajax/InsertCommand.php index 9871ef2..e46eac0 100644 --- a/core/lib/Drupal/Core/Ajax/InsertCommand.php +++ b/core/lib/Drupal/Core/Ajax/InsertCommand.php @@ -57,7 +57,7 @@ class InsertCommand implements CommandInterface { */ public function __construct($selector, $html, array $settings = NULL) { $this->selector = $selector; - $this->html = $html; + $this->html = (string) $html; $this->settings = $settings; } diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php index 4a5fd1d..475e1e4 100644 --- a/core/lib/Drupal/Core/Config/StorableConfigBase.php +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Config\Schema\Ignore; use Drupal\Core\Config\Schema\SchemaIncompleteException; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\TypedData\Type\FloatInterface; use Drupal\Core\TypedData\Type\IntegerInterface; @@ -169,6 +170,10 @@ protected function validateValue($key, $value) { * Exception on unsupported/undefined data type deducted. */ protected function castValue($key, $value) { + if ($value instanceof SafeMarkup) { + $value = (string) $value; + } + $element = FALSE; try { $element = $this->getSchemaWrapper()->get($key); diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index fe23fb6..99784f7 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -101,7 +101,7 @@ public static function registerTwig(ContainerBuilder $container) { 'cache' => drupal_installation_attempted() ? FALSE : Settings::get('twig_cache', TRUE), // @todo Remove in followup issue // @see http://drupal.org/node/1712444. - 'autoescape' => FALSE, + 'autoescape' => TRUE, 'debug' => Settings::get('twig_debug', FALSE), 'auto_reload' => Settings::get('twig_auto_reload', NULL), )) diff --git a/core/lib/Drupal/Core/Database/Install/Tasks.php b/core/lib/Drupal/Core/Database/Install/Tasks.php index c4d88d2..6d0dfe7 100644 --- a/core/lib/Drupal/Core/Database/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Install/Tasks.php @@ -92,14 +92,14 @@ protected function hasPdoDriver() { * Assert test as failed. */ protected function fail($message) { - $this->results[$message] = FALSE; + $this->results[(string) $message] = FALSE; } /** * Assert test as a pass. */ protected function pass($message) { - $this->results[$message] = TRUE; + $this->results[(string) $message] = TRUE; } /** diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index dbe2c78..8354bea 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -626,6 +626,8 @@ public function getName($theme) { if (!isset($themes[$theme])) { throw new \InvalidArgumentException(String::format('Requested the name of a non-existing theme @theme', array('@theme' => $theme))); } + // @todo For testing please remove before commit. + return \Drupal::config('system.site')->get('name'); return String::checkPlain($themes[$theme]->info['name']); } diff --git a/core/lib/Drupal/Core/Form/OptGroup.php b/core/lib/Drupal/Core/Form/OptGroup.php index f94d62d..21e9855 100644 --- a/core/lib/Drupal/Core/Form/OptGroup.php +++ b/core/lib/Drupal/Core/Form/OptGroup.php @@ -43,7 +43,7 @@ public static function flattenOptions(array $array) { */ protected static function doFlattenOptions(array $array, array &$options) { foreach ($array as $key => $value) { - if (is_object($value)) { + if (is_object($value) && isset($value->option)) { static::doFlattenOptions($value->option, $options); } elseif (is_array($value)) { diff --git a/core/lib/Drupal/Core/Page/HeadElement.php b/core/lib/Drupal/Core/Page/HeadElement.php index 85055ad..70cf2b7 100644 --- a/core/lib/Drupal/Core/Page/HeadElement.php +++ b/core/lib/Drupal/Core/Page/HeadElement.php @@ -8,11 +8,12 @@ namespace Drupal\Core\Page; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; /** * This class represents an HTML element that appears in the HEAD tag. */ -class HeadElement { +class HeadElement extends SafeMarkup { /** * An array of attributes for this element. diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php index bd1b3b3..c34c4c0 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\StringTranslation\Translator\TranslatorInterface; +use Drupal\Core\Template\SafeMarkup; /** * Defines a chained translation implementation combining multiple translators. @@ -140,7 +141,7 @@ public function translate($string, array $args = array(), array $options = array $string = $translation === FALSE ? $string : $translation; if (empty($args)) { - return $string; + return new SafeMarkup($string); } else { return String::format($string, $args); @@ -160,7 +161,7 @@ public function formatPlural($count, $singular, $plural, array $args = array(), $translated_array = explode(LOCALE_PLURAL_DELIMITER, $translated_strings); if ($count == 1) { - return $translated_array[0]; + return new SafeMarkup($translated_array[0]); } // Get the plural index through the gettext formula. @@ -168,20 +169,21 @@ public function formatPlural($count, $singular, $plural, array $args = array(), $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1; if ($index == 0) { // Singular form. - return $translated_array[0]; + $return = $translated_array[0]; } else { if (isset($translated_array[$index])) { // N-th plural form. - return $translated_array[$index]; + $return = $translated_array[$index]; } else { // If the index cannot be computed or there's no translation, use // the second plural form as a fallback (which allows for most flexiblity // with the replaceable @count value). - return $translated_array[1]; + $return = $translated_array[1]; } } + return new SafeMarkup($return); } /** diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index ead5d05..7432b6d 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -31,7 +31,7 @@ * // Produces * @endcode */ -class Attribute implements \ArrayAccess, \IteratorAggregate { +class Attribute extends \Twig_Markup implements \ArrayAccess, \IteratorAggregate { /** * Stores the attribute data. @@ -86,9 +86,12 @@ protected function createAttributeValue($name, $value) { elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); } - elseif (!is_object($value)) { + elseif (!is_object($value) || $value instanceof SafeMarkup) { $value = new AttributeString($name, $value); } + elseif (!method_exists($value, 'render')) { + throw new \Exception('boo!'); + } return $value; } diff --git a/core/lib/Drupal/Core/Template/AttributeArray.php b/core/lib/Drupal/Core/Template/AttributeArray.php index 95e0ef3..4d8145f 100644 --- a/core/lib/Drupal/Core/Template/AttributeArray.php +++ b/core/lib/Drupal/Core/Template/AttributeArray.php @@ -66,7 +66,7 @@ public function offsetExists($offset) { * Implements the magic __toString() method. */ public function __toString() { - return String::checkPlain(implode(' ', $this->value)); + return (string) String::checkPlain(implode(' ', $this->value)); } /** diff --git a/core/lib/Drupal/Core/Template/AttributeBoolean.php b/core/lib/Drupal/Core/Template/AttributeBoolean.php index 4e9ea67..b5a0957 100644 --- a/core/lib/Drupal/Core/Template/AttributeBoolean.php +++ b/core/lib/Drupal/Core/Template/AttributeBoolean.php @@ -42,7 +42,7 @@ public function render() { * Implements the magic __toString() method. */ public function __toString() { - return $this->value === FALSE ? '' : String::checkPlain($this->name); + return $this->value === FALSE ? '' : (string) String::checkPlain($this->name); } } diff --git a/core/lib/Drupal/Core/Template/AttributeString.php b/core/lib/Drupal/Core/Template/AttributeString.php index 07211be..e1c69dc 100644 --- a/core/lib/Drupal/Core/Template/AttributeString.php +++ b/core/lib/Drupal/Core/Template/AttributeString.php @@ -30,7 +30,7 @@ class AttributeString extends AttributeValueBase { * Implements the magic __toString() method. */ public function __toString() { - return String::checkPlain($this->value); + return (string) String::checkPlain($this->value); } } diff --git a/core/lib/Drupal/Core/Template/SafeMarkup.php b/core/lib/Drupal/Core/Template/SafeMarkup.php new file mode 100644 index 0000000..58d85c4 --- /dev/null +++ b/core/lib/Drupal/Core/Template/SafeMarkup.php @@ -0,0 +1,65 @@ + $string) { + if (!$string instanceof SafeMarkup) { + $array[$key] = String::checkPlain($string); + } + } + return new SafeMarkup(implode($delimiter, $array)); + } + + public static function strReplace($search, $replace, $subject, &$count = NULL) { + $safe = $subject instanceof SafeMarkup; + $replacement = str_replace($search, $replace, $subject, $count); + return $safe ? new SafeMarkup($replacement) : $replacement; + } + + /** + * Renders the markup. + * + * @return string + * The results of the callback function. + */ + public function render() { + return $this->__toString(); + } + +} diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php index b547d95..231f3aa 100644 --- a/core/lib/Drupal/Core/Utility/LinkGenerator.php +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -11,8 +11,9 @@ use Drupal\Component\Utility\String; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Path\AliasManagerInterface; -use Drupal\Core\Template\Attribute; use Drupal\Core\Routing\UrlGeneratorInterface; +use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Url; /** @@ -122,8 +123,7 @@ public function generateFromUrl($text, Url $url) { // Sanitize the link text if necessary. $text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']); - - return '' . $text . ''; + return new SafeMarkup('' . $text . ''); } /** diff --git a/core/modules/action/action.views_execution.inc b/core/modules/action/action.views_execution.inc index 32bc883..66d18bf 100644 --- a/core/modules/action/action.views_execution.inc +++ b/core/modules/action/action.views_execution.inc @@ -19,6 +19,6 @@ function action_views_form_substitutions() { '#attributes' => array('class' => array('action-table-select-all')), ); return array( - $select_all_placeholder => drupal_render($select_all), + (string) $select_all_placeholder => drupal_render($select_all), ); } diff --git a/core/modules/block/src/Tests/BlockInterfaceTest.php b/core/modules/block/src/Tests/BlockInterfaceTest.php index 0b71146..67f0faa 100644 --- a/core/modules/block/src/Tests/BlockInterfaceTest.php +++ b/core/modules/block/src/Tests/BlockInterfaceTest.php @@ -7,6 +7,9 @@ namespace Drupal\block\Tests; +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Render\Element; +use Drupal\Core\Template\SafeMarkup; use Drupal\simpletest\DrupalUnitTestBase; use Drupal\block\BlockInterface; @@ -82,14 +85,14 @@ public function testBlockInterface() { ), 'label' => array( '#type' => 'textfield', - '#title' => 'Title', + '#title' => t('Title'), '#maxlength' => 255, '#default_value' => 'Custom Display Message', '#required' => TRUE, ), 'label_display' => array( '#type' => 'checkbox', - '#title' => 'Display title', + '#title' => t('Display title'), '#default_value' => TRUE, '#return_value' => 'visible', ), @@ -124,7 +127,7 @@ public function testBlockInterface() { ); $form_state = array(); // Ensure there are no form elements that do not belong to the plugin. - $this->assertIdentical($display_block->buildConfigurationForm(array(), $form_state), $expected_form, 'Only the expected form elements were present.'); + $this->assertIdenticalArray($display_block->buildConfigurationForm(array(), $form_state), $expected_form, 'Only the expected form elements were present.'); $expected_build = array( '#children' => 'My custom display message.', @@ -136,4 +139,5 @@ public function testBlockInterface() { // testing BlockBase's implementation, not the interface itself. $this->assertIdentical($display_block->getMachineNameSuggestion(), 'displaymessage', 'The plugin returned the expected machine name suggestion.'); } + } diff --git a/core/modules/contextual/src/ContextualController.php b/core/modules/contextual/src/ContextualController.php index aeee4a6..fcc47de 100644 --- a/core/modules/contextual/src/ContextualController.php +++ b/core/modules/contextual/src/ContextualController.php @@ -44,7 +44,10 @@ public function render(Request $request) { '#type' => 'contextual_links', '#contextual_links' => _contextual_id_to_links($id), ); - $rendered[$id] = drupal_render($element); + // Cast any returned safe markup to string so that it is created + // as correct JSON. + // @todo Maybe check safeness and checkplain the results? + $rendered[$id] = (string) drupal_render($element); } return new JsonResponse($rendered); diff --git a/core/modules/editor/tests/src/EditorXssFilter/StandardTest.php b/core/modules/editor/tests/src/EditorXssFilter/StandardTest.php index 7927eef..cc3a884 100644 --- a/core/modules/editor/tests/src/EditorXssFilter/StandardTest.php +++ b/core/modules/editor/tests/src/EditorXssFilter/StandardTest.php @@ -551,7 +551,7 @@ public function providerTestFilterXss() { */ public function testFilterXss($input, $expected_output) { $output = call_user_func($this->editorXssFilterClass . '::filterXss', $input, $this->format); - $this->assertSame($expected_output, $output); + $this->assertSame($expected_output, (string) $output); } } diff --git a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php index e5ad209..9a5b732 100644 --- a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php +++ b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php @@ -90,7 +90,7 @@ public function getSettableOptions(AccountInterface $account = NULL) { $return = array(); foreach ($options as $bundle => $entity_ids) { $bundle_label = String::checkPlain($bundles[$bundle]['label']); - $return[$bundle_label] = $entity_ids; + $return[(string) $bundle_label] = $entity_ids; } return count($return) == 1 ? reset($return) : $return; diff --git a/core/modules/field/field.module b/core/modules/field/field.module index fc6ec9e..507897d 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -9,6 +9,7 @@ use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Extension\Extension; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\HttpFoundation\Request; /* @@ -266,7 +267,7 @@ function field_entity_bundle_delete($entity_type, $bundle) { * UTF-8. */ function field_filter_xss($string) { - return Html::normalize(Xss::filter($string, _field_filter_xss_allowed_tags())); + return new SafeMarkup(Html::normalize(Xss::filter($string, _field_filter_xss_allowed_tags()))); } /** diff --git a/core/modules/field/src/Plugin/views/field/Field.php b/core/modules/field/src/Plugin/views/field/Field.php index ec794fd..3737791 100644 --- a/core/modules/field/src/Plugin/views/field/Field.php +++ b/core/modules/field/src/Plugin/views/field/Field.php @@ -7,12 +7,14 @@ namespace Drupal\field\Plugin\views\field; +use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Field\FieldDefinition; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterPluginManager; @@ -685,11 +687,11 @@ public function submitGroupByForm(&$form, &$form_state) { protected function renderItems($items) { if (!empty($items)) { if (!$this->options['group_rows']) { - return implode('', $items); + return SafeMarkup::implode('', $items); } if ($this->options['multi_type'] == 'separator') { - return implode(Xss::filterAdmin($this->options['separator']), $items); + return new SafeMarkup(implode(Xss::filterAdmin($this->options['separator']), $items)); } else { $item_list = array( diff --git a/core/modules/field_ui/src/DisplayOverviewBase.php b/core/modules/field_ui/src/DisplayOverviewBase.php index 2a6f46f..29ea754 100644 --- a/core/modules/field_ui/src/DisplayOverviewBase.php +++ b/core/modules/field_ui/src/DisplayOverviewBase.php @@ -13,6 +13,7 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -398,7 +399,7 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, Ent if (!empty($summary)) { $field_row['settings_summary'] = array( - '#markup' => '
' . implode('
', $summary) . '
', + '#markup' => '
' . SafeMarkup::implode('
', $summary) . '
', '#cell_attributes' => array('class' => array('field-plugin-summary-cell')), ); } diff --git a/core/modules/field_ui/src/FieldConfigListBuilder.php b/core/modules/field_ui/src/FieldConfigListBuilder.php index 0da1bf9..91f8d45 100644 --- a/core/modules/field_ui/src/FieldConfigListBuilder.php +++ b/core/modules/field_ui/src/FieldConfigListBuilder.php @@ -7,11 +7,13 @@ namespace Drupal\field_ui; +use Drupal\Component\Utility\String; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -117,7 +119,7 @@ public function buildRow(EntityInterface $field) { $usage[] = $this->bundles[$field->entity_type][$bundle]['label']; } } - $row['data']['usage'] = implode(', ', $usage); + $row['data']['usage'] = SafeMarkup::implode(', ', $usage); return $row; } diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index f9aa9f9..11b071b 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -8,6 +8,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Template\SafeMarkup; /** * Returns HTML for an individual file upload widget. @@ -125,7 +126,7 @@ function theme_file_widget_multiple($variables) { $row[] = $display; } $row[] = $weight; - $row[] = $operations; + $row[] = new SafeMarkup($operations); $rows[] = array( 'data' => $row, 'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'), diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 47b72ef..4c41b78 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Template\SafeMarkup; use Drupal\file\Entity\File; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\Unicode; @@ -943,10 +944,10 @@ function file_save_upload($form_field_name, $validators = array(), $destination '#theme' => 'item_list', '#items' => $errors, ); - $message .= drupal_render($item_list); + $message = SafeMarkup::concat($message, drupal_render($item_list)); } else { - $message .= ' ' . array_pop($errors); + $message = SafeMarkup::concat($message, ' ', array_pop($errors)); } drupal_set_message($message, 'error'); $files[$i] = FALSE; diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index f25fe79..b2f2c28 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -13,6 +13,7 @@ use Drupal\Core\Render\Element; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Drupal\filter\FilterFormatInterface; use Drupal\filter\Plugin\FilterInterface; use Symfony\Component\HttpFoundation\Request; @@ -414,7 +415,7 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE, \Drupal::cache('filter')->set($cache_id, $text, Cache::PERMANENT, array('filter_format' => $format->id())); } - return $text; + return new SafeMarkup($text); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php index 08ad4c8..22980fa 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php +++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Xss; +use Drupal\Core\Template\SafeMarkup; use Drupal\filter\Plugin\FilterBase; /** @@ -76,7 +77,7 @@ public function process($text, $langcode, $cache, $cache_id) { // caption. $filter_caption = array( '#theme' => 'filter_caption', - '#node' => $node->C14N(), + '#node' => new SafeMarkup($node->C14N()), '#tag' => $node->tagName, '#caption' => $caption, '#align' => $align, diff --git a/core/modules/filter/templates/filter-guidelines.html.twig b/core/modules/filter/templates/filter-guidelines.html.twig index 88a3b47..ecf9b94 100644 --- a/core/modules/filter/templates/filter-guidelines.html.twig +++ b/core/modules/filter/templates/filter-guidelines.html.twig @@ -20,6 +20,6 @@ */ #} -

{{ format.name|escape }}

+

{{ format.name }}

{{ tips }} diff --git a/core/modules/node/node.install b/core/modules/node/node.install index d0d6d1c..2bed32a 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -7,6 +7,7 @@ use Drupal\Component\Uuid\Uuid; use Drupal\Core\Language\Language; +use Drupal\Core\Template\SafeMarkup; /** * Implements hook_requirements(). @@ -29,7 +30,9 @@ function node_requirements($phase) { $requirements['node_access'] = array( 'title' => t('Node Access Permissions'), 'value' => $value, - 'description' => $description . ' ' . l(t('Rebuild permissions'), 'admin/reports/status/rebuild'), + // The results of t() is safe and so is the results of l(). Preserving + // safe object. + 'description' => new SafeMarkup($description . ' ' . l(t('Rebuild permissions'), 'admin/reports/status/rebuild')), ); } return $requirements; diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index 53c182c..fd7341d 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -7,6 +7,7 @@ use Drupal\breakpoint\Entity\Breakpoint; use \Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\HttpFoundation\Request; /** @@ -190,7 +191,6 @@ function theme_responsive_image($variables) { } $sources = array(); - $output = array(); // Fallback image, output as source with media query. $sources[] = array( @@ -234,6 +234,7 @@ function theme_responsive_image($variables) { } if (!empty($sources)) { + $output = array(); $output[] = ''; // Add source tags to the output. @@ -253,7 +254,7 @@ function theme_responsive_image($variables) { } $output[] = ''; - return implode("\n", $output); + return new SafeMarkup(implode("\n", $output)); } } diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index bec96c4..dd03660 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -526,7 +526,7 @@ function simpletest_test_get_all($module = NULL) { } } - $groups[$info['group']][$class] = $info; + $groups[(string) $info['group']][$class] = $info; } } diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php index 299f305..2786a3a 100644 --- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php +++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Form\FormBase; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -155,7 +156,8 @@ public function buildForm(array $form, array &$form_state, $test_id = NULL) { $rows = array(); foreach ($assertions as $assertion) { $row = array(); - $row[] = $assertion->message; + // @todo Need to preserve safe markup, not create it. + $row[] = new SafeMarkup($assertion->message); $row[] = $assertion->message_group; $row[] = drupal_basename($assertion->file); $row[] = $assertion->line; diff --git a/core/modules/simpletest/src/Form/SimpletestTestForm.php b/core/modules/simpletest/src/Form/SimpletestTestForm.php index e3876a5..466ea67 100644 --- a/core/modules/simpletest/src/Form/SimpletestTestForm.php +++ b/core/modules/simpletest/src/Form/SimpletestTestForm.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\String; use Drupal\Core\Form\FormBase; +use Drupal\Core\Template\SafeMarkup; /** * List tests arranged in groups that can be selected and run. @@ -87,8 +88,8 @@ public function buildForm(array $form, array &$form_state) { 'data' => array( 'simpleTest' => array( 'images' => array( - drupal_render($image_collapsed), - drupal_render($image_extended), + (string) drupal_render($image_collapsed), + (string) drupal_render($image_extended), ), ), ), diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index 023cf8e..7a7d281 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -277,7 +277,7 @@ protected function assert($status, $message = '', $group = 'Other', array $calle 'test_id' => $this->testId, 'test_class' => get_class($this), 'status' => $status, - 'message' => $message, + 'message' => (string) $message, 'message_group' => $group, 'function' => $caller['function'], 'line' => $caller['line'], @@ -646,6 +646,41 @@ protected function assertIdenticalObject($object1, $object2, $message = '', $gro return $this->assertTrue($identical, $message, $group); } + /** + * Asserts two array are identical. + * + * Use this helper if the arrays contain objects. + * + * @param array $array1 + * The first array to check. + * @param array $array2 + * The second array to check. + * @param $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\String::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + */ + protected function assertIdenticalArray(array $array1, array $array2, $message = '', $group = 'Other') { + $this->assertEqual(count($array1), count($array2)); + foreach (array_keys($array1) as $key) { + if (is_array($array1[$key])) { + $this->assertIdenticalArray($array1[$key], $array2[$key]); + } + elseif (is_object($array1[$key])) { + $this->assertIdenticalObject($array1[$key], $array2[$key]); + } + else { + $this->assertIdentical($array1[$key], $array2[$key]); + } + } + } + /** * Asserts that no errors have been logged to the PHP error.log thus far. * diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index ec50d4d..f2a229a 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -23,6 +23,7 @@ use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\block\Entity\Block; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\CssSelector\CssSelector; @@ -1629,6 +1630,9 @@ protected function drupalGetAJAX($path, array $options = array(), array $headers */ protected function drupalPostForm($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) { $submit_matches = FALSE; + if ($submit instanceof SafeMarkup) { + $submit = (string) $submit; + } $ajax = is_array($submit); if (isset($path)) { $this->drupalGet($path, $options); @@ -2285,7 +2289,7 @@ protected function buildXPathQuery($xpath, array $args = array()) { // XPath 1.0 doesn't support a way to escape single or double quotes in a // string literal. We split double quotes out of the string, and encode // them separately. - if (is_string($value)) { + if (is_string($value) || $value instanceof SafeMarkup) { // Explode the text at the quote characters. $parts = explode('"', $value); @@ -2776,7 +2780,7 @@ protected function assertRaw($raw, $message = '', $group = 'Other') { if (!$message) { $message = String::format('Raw "@raw" found', array('@raw' => $raw)); } - return $this->assert(strpos($this->drupalGetContent(), $raw) !== FALSE, $message, $group); + return $this->assert(strpos($this->drupalGetContent(), (string) $raw) !== FALSE, $message, $group); } /** @@ -2803,7 +2807,7 @@ protected function assertNoRaw($raw, $message = '', $group = 'Other') { if (!$message) { $message = String::format('Raw "@raw" not found', array('@raw' => $raw)); } - return $this->assert(strpos($this->drupalGetContent(), $raw) === FALSE, $message, $group); + return $this->assert(strpos($this->drupalGetContent(), (string) $raw) === FALSE, $message, $group); } /** @@ -2887,7 +2891,7 @@ protected function assertTextHelper($text, $message = '', $group, $not_exists) { if (!$message) { $message = !$not_exists ? String::format('"@text" found', array('@text' => $text)) : String::format('"@text" not found', array('@text' => $text)); } - return $this->assert($not_exists == (strpos($this->plainTextContent, $text) === FALSE), $message, $group); + return $this->assert($not_exists == (strpos($this->plainTextContent, (string) $text) === FALSE), $message, $group); } /** @@ -3701,6 +3705,7 @@ protected function assertMail($name, $value = '', $message = '', $group = 'E-mai protected function assertMailString($field_name, $string, $email_depth, $message = '', $group = 'Other') { $mails = $this->drupalGetMails(); $string_found = FALSE; + $string = (string) $string; for ($i = count($mails) -1; $i >= count($mails) - $email_depth && $i >= 0; $i--) { $mail = $mails[$i]; // Normalize whitespace, as we don't know what the mail system might have diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index d061495..180be27 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -17,6 +17,7 @@ use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Render\Element; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Access\AccessManager; @@ -162,7 +163,7 @@ public function buildForm(array $form, array &$form_state) { '#open' => TRUE, '#theme' => 'system_modules_details', '#header' => array( - array('data' => '' . $this->t('Installed') . '', 'class' => array('checkbox')), + array('data' => new SafeMarkup('' . $this->t('Installed') . ''), 'class' => array('checkbox')), array('data' => $this->t('Name'), 'class' => array('name')), array('data' => $this->t('Description'), 'class' => array('description', RESPONSIVE_PRIORITY_LOW)), ), diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php index 7438e48..b272148 100644 --- a/core/modules/system/src/Form/ModulesUninstallForm.php +++ b/core/modules/system/src/Form/ModulesUninstallForm.php @@ -10,6 +10,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; +use Drupal\Core\Template\SafeMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php index 8edbb07..1458a0f 100644 --- a/core/modules/system/src/Tests/Common/RenderTest.php +++ b/core/modules/system/src/Tests/Common/RenderTest.php @@ -277,7 +277,8 @@ function testDrupalRenderBasics() { ); foreach($types as $type) { - $this->assertIdentical(drupal_render($type['value']), $type['expected'], '"' . $type['name'] . '" input rendered correctly by drupal_render().'); + $value = drupal_render($type['value']); + $this->assertIdentical((string) $value, $type['expected'], '"' . $type['name'] . '" input rendered correctly by drupal_render().'); } } @@ -394,14 +395,14 @@ function testDrupalRenderThemeArguments() { '#theme' => 'common_test_foo', ); // Test that defaults work. - $this->assertEqual(drupal_render($element), 'foobar', 'Defaults work'); + $this->assertEqual((string) drupal_render($element), 'foobar', 'Defaults work'); $element = array( '#theme' => 'common_test_foo', '#foo' => $this->randomName(), '#bar' => $this->randomName(), ); // Tests that passing arguments to the theme function works. - $this->assertEqual(drupal_render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works'); + $this->assertEqual((string) drupal_render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works'); } /** @@ -470,7 +471,7 @@ function testDrupalRenderPostRenderCache() { $element = $test_element; $element['#markup'] = '

#cache disabled

'; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.'); @@ -486,7 +487,7 @@ function testDrupalRenderPostRenderCache() { $element['#cache'] = array('cid' => 'post_render_cache_test_GET'); $element['#markup'] = '

#cache enabled, GET

'; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -509,7 +510,7 @@ function testDrupalRenderPostRenderCache() { $element['#cache'] = array('cid' => 'post_render_cache_test_GET'); $element['#markup'] = '

#cache enabled, GET

'; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -526,7 +527,7 @@ function testDrupalRenderPostRenderCache() { $element['#cache'] = array('cid' => 'post_render_cache_test_POST'); $element['#markup'] = '

#cache enabled, POST

'; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -589,7 +590,7 @@ function testDrupalRenderChildrenPostRenderCache() { ); $element = $test_element; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -635,7 +636,7 @@ function testDrupalRenderChildrenPostRenderCache() { drupal_static_reset('_drupal_add_js'); $element = $test_element; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.'); @@ -647,7 +648,7 @@ function testDrupalRenderChildrenPostRenderCache() { unset($test_element['#cache']); $element = $test_element; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); $expected_settings = $context_1 + $context_2 + $context_3; @@ -667,7 +668,7 @@ function testDrupalRenderChildrenPostRenderCache() { $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent'); $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child'); $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], '

overridden

', '#markup is overridden.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -742,7 +743,7 @@ function testDrupalRenderChildrenPostRenderCache() { $element = $test_element; $element['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_parent'); $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['foo'], 'bar', 'Original JavaScript setting is added to the page.'); @@ -754,7 +755,7 @@ function testDrupalRenderChildrenPostRenderCache() { $element['child']['#cache']['keys'] = array('simpletest', 'drupal_render', 'children_post_render_cache', 'nested_cache_child'); $element = $element['child']; $output = drupal_render($element); - $this->assertIdentical($output, '

overridden

', 'Output is overridden.'); + $this->assertIdentical((string) $output, '

overridden

', 'Output is overridden.'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $settings = $this->parseDrupalSettings(drupal_get_js()); $expected_settings = $context_2 + $context_3; @@ -790,7 +791,7 @@ function testDrupalRenderRenderCachePlaceholder() { drupal_static_reset('_drupal_add_js'); $element = $test_element; $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.'); @@ -803,7 +804,7 @@ function testDrupalRenderRenderCachePlaceholder() { $element = $test_element; $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET'); $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -840,7 +841,7 @@ function testDrupalRenderRenderCachePlaceholder() { $element = $test_element; $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET'); $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -880,7 +881,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { drupal_static_reset('_drupal_add_js'); $element = $container; $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.'); @@ -899,7 +900,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $element['#children'] = drupal_render($child, TRUE); // Eventually, drupal_render() gets called on the root element. $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); @@ -996,7 +997,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $child = &$element['test_element']; $element['#children'] = drupal_render($child, TRUE); $output = drupal_render($element); - $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); + $this->assertIdentical((string) $output, $expected_output, 'Placeholder was replaced in output'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 9ba1a1c..67664e9 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -10,6 +10,7 @@ use Drupal\Core\Extension\Extension; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; /** * Recursively check compatibility. @@ -231,7 +232,7 @@ function theme_system_modules_details($variables) { // Add the module label and expand/collapse functionalty. $col2 = ''; - $row[] = array('class' => array('module'), 'data' => $col2); + $row[] = array('class' => array('module'), 'data' => new SafeMarkup($col2)); // Add the description, along with any modules it requires. $description = ''; @@ -259,9 +260,9 @@ function theme_system_modules_details($variables) { } $details = array( '#type' => 'details', - '#title' => ' ' . drupal_render($module['description']) . '', + '#title' => new SafeMarkup(' ' . drupal_render($module['description']) . ''), '#attributes' => array('id' => $module['enable']['#id'] . '-description'), - '#description' => $description, + '#description' => new SafeMarkup($description), ); $col4 = drupal_render($details); $row[] = array('class' => array('description', 'expand'), 'data' => $col4); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 6161819..dfff949 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -11,6 +11,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\Template\SafeMarkup; /** * Implements hook_requirements(). @@ -57,7 +58,8 @@ function system_requirements($phase) { if (function_exists('phpinfo')) { $requirements['php'] = array( 'title' => t('PHP'), - 'value' => ($phase == 'runtime') ? $phpversion .' ('. l(t('more information'), 'admin/reports/status/php') .')' : $phpversion, + // $phpversion is safe and output of l() is safe, so this value is safe. + 'value' => new SafeMarkup(($phase == 'runtime') ? $phpversion . ' (' . l(t('more information'), 'admin/reports/status/php') . ')' : $phpversion), ); } else { @@ -319,7 +321,8 @@ function system_requirements($phase) { 'title' => t('Cron maintenance tasks'), 'severity' => $severity, 'value' => $summary, - 'description' => $description + // @todo Needs to preserve safe markup. + 'description' => new SafeMarkup($description), ); } if ($phase != 'install') { diff --git a/core/modules/system/templates/block--system-branding-block.html.twig b/core/modules/system/templates/block--system-branding-block.html.twig index 2a12c7a..4cf0f1a 100644 --- a/core/modules/system/templates/block--system-branding-block.html.twig +++ b/core/modules/system/templates/block--system-branding-block.html.twig @@ -23,7 +23,7 @@ {% endif %} {% if site_name %} {% endif %} {% if site_slogan %} diff --git a/core/modules/system/templates/datetime.html.twig b/core/modules/system/templates/datetime.html.twig index 25ef788..183b834 100644 --- a/core/modules/system/templates/datetime.html.twig +++ b/core/modules/system/templates/datetime.html.twig @@ -25,5 +25,4 @@ * @see http://www.w3.org/TR/html5-author/the-time-element.html#attr-time-datetime */ #} -{# @todo Revisit once http://drupal.org/node/1825952 is resolved. #} -{{ html ? text|raw : text|escape }} +{{ html ? text|raw : text }} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 4b28c81..8936c27 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -6,6 +6,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AnonymousUserSession; use \Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Template\SafeMarkup; use Drupal\Core\Url; use Drupal\file\Entity\File; use Drupal\user\Entity\Role; @@ -692,7 +693,7 @@ function theme_username($variables) { // We have a link path, so we should generate a link using l(). // Additional classes may be added as array elements like // $variables['link_options']['attributes']['class'][] = 'myclass'; - $output = l($variables['name'] . $variables['extra'], $variables['link_path'], $variables['link_options']); + $output = l(new SafeMarkup($variables['name'] . $variables['extra']), $variables['link_path'], $variables['link_options']); } else { // Modules may have added important attributes so they must be included diff --git a/core/modules/views/src/Plugin/views/HandlerBase.php b/core/modules/views/src/Plugin/views/HandlerBase.php index cbff847..da8ddb1 100644 --- a/core/modules/views/src/Plugin/views/HandlerBase.php +++ b/core/modules/views/src/Plugin/views/HandlerBase.php @@ -236,7 +236,7 @@ public function getField($field = NULL) { * @param $type * The type of sanitization needed. If not provided, String::checkPlain() is used. * - * @return string + * @return \Drupal\Core\Template\SafeMarkup * Returns the safe value. */ public function sanitizeValue($value, $type = NULL) { diff --git a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php index 6f0add5..9af6b70 100644 --- a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php +++ b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php @@ -51,13 +51,13 @@ public function tokenForm(&$form, &$form_state) { // Get a list of the available fields and arguments for token replacement. $options = array(); foreach ($this->view->display_handler->getHandlers('field') as $field => $handler) { - $options[t('Fields')]["[$field]"] = $handler->adminLabel(); + $options[(string) t('Fields')]["[$field]"] = $handler->adminLabel(); } $count = 0; // This lets us prepare the key as we want it printed. foreach ($this->view->display_handler->getHandlers('argument') as $handler) { - $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); - $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); } if (!empty($options)) { diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index b960d5b..c40a3c7 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -1732,8 +1732,8 @@ public function buildOptionsForm(&$form, &$form_state) { $options = array(); $count = 0; // This lets us prepare the key as we want it printed. foreach ($this->view->display_handler->getHandlers('argument') as $handler) { - $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); - $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); } // Default text. diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 5eb5765..b5581ea 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; +use Drupal\Core\Template\SafeMarkup; use Drupal\views\Plugin\views\HandlerBase; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ResultRow; @@ -850,18 +851,18 @@ public function buildOptionsForm(&$form, &$form_state) { // Setup the tokens for fields. $previous = $this->getPreviousFieldLabels(); foreach ($previous as $id => $label) { - $options[t('Fields')]["[$id]"] = $label; + $options[(string) t('Fields')]["[$id]"] = $label; } // Add the field to the list of options. - $options[t('Fields')]["[{$this->options['id']}]"] = $this->label(); + $options[(string) t('Fields')]["[{$this->options['id']}]"] = $this->label(); $count = 0; // This lets us prepare the key as we want it printed. foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { - $options[t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); - $options[t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['%' . ++$count] = t('@argument title', array('@argument' => $handler->adminLabel())); + $options[(string) t('Arguments')]['!' . $count] = t('@argument input', array('@argument' => $handler->adminLabel())); } - $this->documentSelfTokens($options[t('Fields')]); + $this->documentSelfTokens($options[(string) t('Fields')]); // Default text. $output = '

' . t('You must add some additional fields to this display before using this field. These fields may be marked as Exclude from display if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.') . '

'; @@ -1172,6 +1173,10 @@ public function advancedRender(ResultRow $values) { $this->last_render = $this->renderText($alter); } } + // @TODO: this is very dicey! + if ($this->last_render && is_string($this->last_render)) { + $this->last_render = new SafeMarkup($this->last_render); + } return $this->last_render; } diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 902b543..42b8d0b 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -1159,8 +1159,7 @@ protected function prepareFilterSelectOptions(&$options) { $this->prepareFilterSelectOptions($options[$value]); } // FAPI has some special value to allow hierarchy. - // @see _form_options_flatten - elseif (is_object($label)) { + elseif (is_object($label) && isset($options[$value]->option)) { $this->prepareFilterSelectOptions($options[$value]->option); } else { diff --git a/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php b/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php index 7a85bbc..673869b 100644 --- a/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php +++ b/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php @@ -130,10 +130,10 @@ public function buildOptionsForm(&$form, &$form_state) { // TODO: check the field is the correct sort? // or let users hang themselves at this stage and check later? if ($view->type == 'Default') { - $views[t('Default Views')][$view->storage->id()] = $view->storage->id(); + $views[(string) t('Default Views')][$view->storage->id()] = $view->storage->id(); } else { - $views[t('Existing Views')][$view->storage->id()] = $view->storage->id(); + $views[(string) t('Existing Views')][$view->storage->id()] = $view->storage->id(); } } } diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 216bc93..5a93d2e 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -14,6 +14,7 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Language\Language; use Drupal\Core\Render\Element; +use Drupal\Core\Template\SafeMarkup; use Drupal\views\Plugin\Derivative\ViewsLocalTask; use Drupal\Core\Template\AttributeArray; use Drupal\views\ViewExecutable; @@ -903,7 +904,7 @@ function views_pre_render_views_form_views_form($element) { } // Apply substitutions to the rendered output. - $element['output']['#markup'] = str_replace($search, $replace, $element['output']['#markup']); + $element['output']['#markup'] = SafeMarkup::strReplace($search, $replace, $element['output']['#markup']); // Sort, render and add remaining form fields. $children = Element::children($element, TRUE); diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 86e3e85..28155ea 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Language\Language; use Drupal\Core\Template\Attribute; +use Drupal\Core\Template\SafeMarkup; use Drupal\views\Form\ViewsForm; use Drupal\views\ViewExecutable; @@ -525,6 +526,7 @@ function template_preprocess_views_view_table(&$variables) { // Render the header labels. if ($field == $column && empty($fields[$field]->options['exclude'])) { + $safe = TRUE; $label = String::checkPlain(!empty($fields[$field]) ? $fields[$field]->label() : ''); if (empty($options['info'][$field]['sortable']) || !$fields[$field]->clickSortable()) { $variables['header'][$field]['content'] = $label; @@ -542,7 +544,10 @@ function template_preprocess_views_view_table(&$variables) { '#theme' => 'tablesort_indicator', '#style' => $initial, ); - $label .= drupal_render($tablesort_indicator); + $markup = drupal_render($tablesort_indicator); + // $label is safe. + $safe = $markup instanceof SafeMarkup; + $label .= $markup; } $query['order'] = $field; @@ -552,7 +557,7 @@ function template_preprocess_views_view_table(&$variables) { 'attributes' => array('title' => $title), 'query' => $query, ); - $variables['header'][$field]['content'] = l($label, current_path(), $link_options); + $variables['header'][$field]['content'] = l($safe ? new SafeMarkup($label) : $label, current_path(), $link_options); } // Set up the header label class. @@ -633,7 +638,7 @@ function template_preprocess_views_view_table(&$variables) { $field_output = $handler->getField($num, $field); $element_type = $fields[$field]->elementType(TRUE, TRUE); if ($element_type) { - $field_output = '<' . $element_type . '>' . $field_output . ''; + $field_output = SafeMarkup::concat(new SafeMarkup('<' . $element_type . '>'), $field_output, new SafeMarkup('')); } // Only bother with separators and stuff if the field shows up. @@ -641,13 +646,13 @@ function template_preprocess_views_view_table(&$variables) { // Place the field into the column, along with an optional separator. if (!empty($column_reference['content'])) { if (!empty($options['info'][$column]['separator'])) { - $column_reference['content'] .= Xss::filterAdmin($options['info'][$column]['separator']); + $column_reference['content'] = SafeMarkup::concat($column_reference['content'], Xss::filterAdmin($options['info'][$column]['separator'])); } } else { $column_reference['content'] = ''; } - $column_reference['content'] .= $field_output; + $column_reference['content'] = SafeMarkup::concat($column_reference['content'], $field_output); } } $column_reference['attributes'] = new Attribute($column_reference['attributes']); diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc index e90ef35..3fe9789 100644 --- a/core/modules/views_ui/admin.inc +++ b/core/modules/views_ui/admin.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\NestedArray; +use Drupal\Component\Utility\String; use Drupal\Component\Utility\Tags; use Drupal\views\ViewExecutable; use Drupal\views\Views; @@ -89,16 +90,18 @@ function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_pa // always give the button a unique #value, rather than playing around with // #name. $button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key; - if (empty($seen_buttons[$button_title])) { - $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array( + $button_title_string = (string) $button_title; + if (empty($seen_buttons[$button_title_string])) { + // This code relies on check_plain()'ing the string because of the quotes. + $wrapping_element[$button_key]['#value'] = (string) t('Update "@title" choice', array( '@title' => $button_title, )); - $seen_buttons[$button_title] = 1; + $seen_buttons[$button_title_string] = 1; } else { - $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array( + $wrapping_element[$button_key]['#value'] = (string) t('Update "@title" choice (@number)', array( '@title' => $button_title, - '@number' => ++$seen_buttons[$button_title], + '@number' => ++$seen_buttons[$button_title_string], )); } diff --git a/core/modules/views_ui/src/Controller/ViewsUIController.php b/core/modules/views_ui/src/Controller/ViewsUIController.php index a09b063..735c95d 100644 --- a/core/modules/views_ui/src/Controller/ViewsUIController.php +++ b/core/modules/views_ui/src/Controller/ViewsUIController.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Template\SafeMarkup; use Drupal\views\ViewExecutable; use Drupal\views\ViewStorageInterface; use Drupal\views\Views; @@ -92,7 +93,7 @@ public function reportFields() { foreach ($views as $view) { $rows[$field_name]['data'][1][] = $this->l($view, 'views_ui.edit', array('view' => $view)); } - $rows[$field_name]['data'][1] = implode(', ', $rows[$field_name]['data'][1]); + $rows[$field_name]['data'][1] = SafeMarkup::implode(', ', $rows[$field_name]['data'][1]); } // Sort rows by field name. @@ -120,7 +121,7 @@ public function reportPlugins() { foreach ($row['views'] as $row_name => $view) { $row['views'][$row_name] = $this->l($view, 'views_ui.edit', array('view' => $view)); } - $row['views'] = implode(', ', $row['views']); + $row['views'] = SafeMarkup::implode(', ', $row['views']); } // Sort rows by field name. diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index 68ad5b0..a468589 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -230,7 +230,7 @@ protected function getDisplaysList(EntityInterface $view) { foreach ($view->get('display') as $display) { $definition = $this->displayManager->getDefinition($display['display_plugin']); if (!empty($definition['admin'])) { - $displays[$definition['admin']] = TRUE; + $displays[(string) $definition['admin']] = TRUE; } } diff --git a/core/themes/bartik/templates/block--system-branding-block.html.twig b/core/themes/bartik/templates/block--system-branding-block.html.twig index 5917f58..f6147a6 100644 --- a/core/themes/bartik/templates/block--system-branding-block.html.twig +++ b/core/themes/bartik/templates/block--system-branding-block.html.twig @@ -23,7 +23,7 @@
{% if site_name %} - {{ site_name|e }} + {{ site_name }} {% endif %} {% if site_slogan %} diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 1595bf8..47376dc 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -6,6 +6,7 @@ */ use Drupal\Core\Extension\Extension; +use Drupal\Core\Template\SafeMarkup; /** * Implements hook_theme(). @@ -93,7 +94,7 @@ function twig_render_template($template_file, $variables) { $output['debug_info'] .= "\n\n"; $output['debug_suffix'] .= "\n\n\n"; } - return implode('', $output); + return new SafeMarkup(implode('', $output)); } /** diff --git a/core/vendor/twig/twig/lib/Twig/Extension/Core.php b/core/vendor/twig/twig/lib/Twig/Extension/Core.php index 4e80c67..396a9f3 100644 --- a/core/vendor/twig/twig/lib/Twig/Extension/Core.php +++ b/core/vendor/twig/twig/lib/Twig/Extension/Core.php @@ -922,7 +922,12 @@ function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', if (!is_string($string)) { if (is_object($string) && method_exists($string, '__toString')) { - $string = (string) $string; + // @todo Move to twig engine. This is necessary because of + // RenderWrapper. + $string = $string->__toString(); + if ($autoescape && $string instanceof Twig_Markup) { + return $string; + } } else { return $string; }