diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 5d645e2..340ac77 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1372,7 +1372,7 @@ function template_preprocess_page(&$variables) { $variables['language'] = $language_interface; $variables['logo'] = theme_get_setting('logo.url'); $variables['site_name'] = (theme_get_setting('features.name') ? SafeMarkup::checkPlain($site_config->get('name')) : ''); - $variables['site_slogan'] = (theme_get_setting('features.slogan') ? Xss::filterAdmin($site_config->get('slogan')) : ''); + $variables['site_slogan'] = (theme_get_setting('features.slogan') ? SafeMarkup::checkAdminXss($site_config->get('slogan')) : ''); // An exception might be thrown. try { diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php index 69d12fe..b5d1d93 100644 --- a/core/lib/Drupal/Component/Utility/SafeMarkup.php +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php @@ -82,7 +82,7 @@ public static function set($string, $strategy = 'html') { /** * Checks if a string is safe to output. * - * @param string $string + * @param string|\Drupal\Component\Utility\SafeStringInterface $string * The content to be checked. * @param string $strategy * The escaping strategy. See self::set(). Defaults to 'html'. @@ -91,7 +91,9 @@ public static function set($string, $strategy = 'html') { * TRUE if the string has been marked secure, FALSE otherwise. */ public static function isSafe($string, $strategy = 'html') { - return isset(static::$safeStrings[(string) $string][$strategy]) || + // Do the instance of checks first to save unnecessarily casting the object + // to a string. + return $string instanceOf SafeStringInterface || isset(static::$safeStrings[(string) $string][$strategy]) || isset(static::$safeStrings[(string) $string]['all']); } @@ -149,7 +151,38 @@ public static function escape($string) { * @see \Drupal\Component\Utility\Xss::filterAdmin() */ public static function checkAdminXss($string) { - return static::isSafe($string) ? $string : Xss::filterAdmin($string); + if (!static::isSafe($string)) { + $string = Xss::filterAdmin($string); + static::set($string); + } + return $string; + } + + /** + * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities. + * + * This method is preferred to \Drupal\Component\Utility\Xss::filter() when + * the result is being used directly in the rendering system. + * + * @param $string + * The string with raw HTML in it. It will be stripped of everything that + * can cause an XSS attack. + * @param array $html_tags + * An array of HTML tags. + * + * @return string + * An XSS safe version of $string, or an empty string if $string is not + * valid UTF-8. The string has been marked safe. If the string has already + * been marked safe, it won't be escaped again. + * + * @see \Drupal\Component\Utility\Xss::filter() + */ + public static function filterXss($string, $html_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) { + if (!static::isSafe($string)) { + $string = Xss::filter($string, $html_tags); + static::set($string); + } + return $string; } /** @@ -209,10 +242,12 @@ public static function checkPlain($text) { * any key in $args are replaced with the corresponding value, after * optional sanitization and formatting. The type of sanitization and * formatting depends on the first character of the key: - * - @variable: Escaped to HTML using self::escape(). Use this as the - * default choice for anything displayed on a page on the site. - * - %variable: Escaped to HTML and formatted using self::placeholder(), - * which makes the following HTML code: + * - @variable: Escaped to plain text using htmlspecialchars() unless + * self::isSafe() returns TRUE to indicate it was already escaped. Use + * this as the default choice for anything displayed in HTML on the site. + * - %variable: Escaped to plain text using htmlspecialchars() unless + * self::isSafe() returns TRUE to indicate it was already escaped. It is + * formatted with EM tags to yield HTML code like: * @code * text output here. * @endcode @@ -240,13 +275,13 @@ public static function format($string, array $args = array()) { switch ($key[0]) { case '@': // Escaped only. - $args[$key] = static::escape($value); + $args[$key] = static::isSafe($value) ? $value : htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); break; case '%': default: // Escaped and placeholder. - $args[$key] = static::placeholder($value); + $args[$key] = static::doPlaceholder($value); break; case '!': @@ -268,21 +303,38 @@ public static function format($string, array $args = array()) { /** * Formats text for emphasized display in a placeholder inside a sentence. * - * Used automatically by self::format(). + * The return value is flagged as a safe markup string for rendering. * * @param string $text * The text to format (plain-text). * * @return string - * The formatted text (html). + * The escaped text formatted with EM tags to yield HTML code like: + * @code + * text output here. + * @endcode */ public static function placeholder($text) { - $string = '' . static::escape($text) . ''; + $string = static::doPlaceholder($text); static::$safeStrings[$string]['html'] = TRUE; return $string; } /** + * Formats text for emphasized display in a placeholder inside a sentence. + * + * @param string $text + * The text to format (plain-text). + * + * @return string + * The formatted text (HTML) with wrapping EM tags. + */ + protected static function doPlaceholder($text) { + $text = static::isSafe($text) ? $text : htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + return '' . $text . ''; + } + + /** * Replaces all occurrences of the search string with the replacement string. * * Functions identically to str_replace(), but marks the returned output as diff --git a/core/lib/Drupal/Component/Utility/SafeStringInterface.php b/core/lib/Drupal/Component/Utility/SafeStringInterface.php new file mode 100644 index 0000000..1414a37 --- /dev/null +++ b/core/lib/Drupal/Component/Utility/SafeStringInterface.php @@ -0,0 +1,14 @@ +]*(>|$) # 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/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php index a83898d..0898485 100644 --- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php +++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php @@ -198,15 +198,15 @@ protected function ajaxRender(Request $request) { $renderer = $this->getRenderer(); if (!empty($css_assets)) { $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css_assets); - $resource_commands[] = new AddCssCommand($renderer->render($css_render_array)); + $resource_commands[] = new AddCssCommand((string) $renderer->render($css_render_array)); } if (!empty($js_assets_header)) { $js_header_render_array = \Drupal::service('asset.js.collection_renderer')->render($js_assets_header); - $resource_commands[] = new PrependCommand('head', $renderer->render($js_header_render_array)); + $resource_commands[] = new PrependCommand('head', (string) $renderer->render($js_header_render_array)); } if (!empty($js_assets_footer)) { $js_footer_render_array = \Drupal::service('asset.js.collection_renderer')->render($js_assets_footer); - $resource_commands[] = new AppendCommand('body', $renderer->render($js_footer_render_array)); + $resource_commands[] = new AppendCommand('body', (string) $renderer->render($js_footer_render_array)); } foreach (array_reverse($resource_commands) as $resource_command) { $this->addCommand($resource_command, TRUE); diff --git a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php index fe117e1..d83db57 100644 --- a/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php +++ b/core/lib/Drupal/Core/Ajax/CommandWithAttachedAssetsTrait.php @@ -37,10 +37,10 @@ protected function getRenderedContent() { if (is_array($this->content)) { $html = \Drupal::service('renderer')->renderRoot($this->content); $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content); - return $html; + return (string) $html; } else { - return $this->content; + return (string) $this->content; } } diff --git a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php index 31192f3..c54799d 100644 --- a/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php @@ -64,7 +64,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch } } - $html = $this->drupalRenderRoot($main_content); + $html = (string) $this->drupalRenderRoot($main_content); $response->setAttachments($main_content['#attached']); // The selector for the insert command is NULL as the new content will @@ -72,7 +72,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // behavior can be changed with #ajax['method']. $response->addCommand(new InsertCommand(NULL, $html)); $status_messages = array('#type' => 'status_messages'); - $output = $this->drupalRenderRoot($status_messages); + $output = (string) $this->drupalRenderRoot($status_messages); if (!empty($output)) { $response->addCommand(new PrependCommand(NULL, $output)); } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index a91b14b..9125453 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -100,7 +100,8 @@ public function renderPlain(&$elements) { $this->resetStack(); $output = $this->renderRoot($elements); static::$stack = $current_stack; - return $output; + // Convert any \Drupal\Core\Render\SafeString objects to a string. + return (string) $output; } /** @@ -221,10 +222,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. - $elements['#markup'] = SafeMarkup::set($elements['#markup']); + $elements['#markup'] = new SafeString($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { - SafeMarkup::set($cached_element[$key]['#markup']); + $cached_element[$key]['#markup'] = new SafeString($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata @@ -392,7 +393,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } - $elements['#children'] = SafeMarkup::set($elements['#children']); + $elements['#children'] = new SafeString($elements['#children'], 'UTF-8'); } // If #theme is not implemented and the element has raw #markup as a @@ -403,7 +404,7 @@ protected function doRender(&$elements, $is_root_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'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); + $elements['#children'] = new SafeString($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered @@ -490,7 +491,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $this->bubbleStack(); $elements['#printed'] = TRUE; - $elements['#markup'] = SafeMarkup::set($elements['#markup']); + $elements['#markup'] = new SafeString($elements['#markup']); return $elements['#markup']; } diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 5d59d4d..bcee70e 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -25,7 +25,7 @@ * @param array $elements * The structured array describing the data to be rendered. * - * @return string + * @return \Drupal\Core\Render\SafeString * The rendered HTML. * * @see ::render() @@ -295,7 +295,7 @@ public function renderPlain(&$elements); * (Internal use only.) Whether this is a recursive call or not. See * ::renderRoot(). * - * @return string + * @return \Drupal\Core\Render\SafeString * The rendered HTML. * * @throws \LogicException diff --git a/core/lib/Drupal/Core/Render/SafeString.php b/core/lib/Drupal/Core/Render/SafeString.php new file mode 100644 index 0000000..d449284 --- /dev/null +++ b/core/lib/Drupal/Core/Render/SafeString.php @@ -0,0 +1,35 @@ +content = (string) $content; + } + + public function __toString() { + return $this->content; + } + + public function count() { + return Unicode::strlen($this->content); + } + +} diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index ad4e1b1..b8c0590 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Template; -use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\SafeStringInterface; /** * Collects, sanitizes, and renders HTML attributes. @@ -42,8 +42,7 @@ * The attribute keys and values are automatically sanitized for output with * htmlspecialchars() and the entire attribute string is marked safe for output. */ -class Attribute implements \ArrayAccess, \IteratorAggregate { - +class Attribute implements \ArrayAccess, \IteratorAggregate, SafeStringInterface { /** * Stores the attribute data. * @@ -259,10 +258,7 @@ public function __toString() { $return .= ' ' . $rendered; } } - // The implementations of AttributeValueBase::render() call - // htmlspecialchars() on the attribute name and value so we are confident - // that the return value can be set as safe. - return SafeMarkup::set($return); + return $return; } /** diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 5a73640..4dfbdb6 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -13,6 +13,7 @@ namespace Drupal\Core\Template; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\SafeStringInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Theme\ThemeManagerInterface; @@ -397,7 +398,7 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $ } // Keep Twig_Markup objects intact to support autoescaping. - if ($autoescape && $arg instanceOf \Twig_Markup) { + if ($autoescape && ($arg instanceOf \Twig_Markup || $arg instanceOf SafeStringInterface)) { return $arg; } @@ -423,13 +424,16 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $ // We have a string or an object converted to a string: Autoescape it! if (isset($return)) { - if ($autoescape && SafeMarkup::isSafe($return, $strategy)) { + if (SafeMarkup::isSafe($return, $strategy)) { return $return; } // Drupal only supports the HTML escaping strategy, so provide a // fallback for other strategies. if ($strategy == 'html') { - return SafeMarkup::checkPlain($return); + // Call htmlspecialchars() directly as there is no point in marking + // something safe at this point. + // @see \Drupal\Component\Utility\SafeMarkup::checkPlain() + return htmlspecialchars($return, ENT_QUOTES, 'UTF-8'); } return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); } diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index 6850fcb..caf9a90 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Theme; +use Drupal\Core\Render\SafeString; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\StackedRouteMatchInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -385,7 +386,7 @@ protected function theme($hook, $variables = array()) { $output = $render_function($template_file, $variables); } - return (string) $output; + return ($output instanceof SafeString) ? $output : (string) $output; } /** diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php index 02fec0f..8ac1062 100644 --- a/core/modules/block/src/Tests/BlockViewBuilderTest.php +++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php @@ -181,14 +181,14 @@ protected function verifyRenderCacheHandling() { public function testBlockViewBuilderAlter() { // Establish baseline. $build = $this->getBlockRenderArray(); - $this->assertIdentical(drupal_render($build), 'Llamas > unicorns!'); + $this->assertIdentical((string) drupal_render($build), 'Llamas > unicorns!'); // Enable the block view alter hook that adds a suffix, for basic testing. \Drupal::state()->set('block_test_view_alter_suffix', TRUE); Cache::invalidateTags($this->block->getCacheTags()); $build = $this->getBlockRenderArray(); $this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '
Goodbye!', 'A block with content is altered.'); - $this->assertIdentical(drupal_render($build), 'Llamas > unicorns!
Goodbye!'); + $this->assertIdentical((string) drupal_render($build), 'Llamas > unicorns!
Goodbye!'); \Drupal::state()->set('block_test_view_alter_suffix', FALSE); // Force a request via GET so we can get drupal_render() cache working. @@ -209,7 +209,7 @@ public function testBlockViewBuilderAlter() { $expected_keys = array_merge($default_keys, array($alter_add_key)); $build = $this->getBlockRenderArray(); $this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.'); - $this->assertIdentical(drupal_render($build), ''); + $this->assertIdentical((string) drupal_render($build), ''); $cache_entry = $this->container->get('cache.render')->get($cid); $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.'); $expected_tags = array_merge($default_tags, ['rendered']); @@ -224,7 +224,7 @@ public function testBlockViewBuilderAlter() { $build = $this->getBlockRenderArray(); sort($build['#cache']['tags']); $this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.'); - $this->assertIdentical(drupal_render($build), ''); + $this->assertIdentical((string) drupal_render($build), ''); $cache_entry = $this->container->get('cache.render')->get($cid); $this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.'); $expected_tags = array_merge($default_tags, [$alter_add_tag, 'rendered']); @@ -237,7 +237,7 @@ public function testBlockViewBuilderAlter() { \Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE); $build = $this->getBlockRenderArray(); $this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before calling drupal_render().'); - $this->assertIdentical(drupal_render($build), 'Hiya!
'); + $this->assertIdentical((string) drupal_render($build), 'Hiya!
'); $this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!
', 'A cached block without content is altered.'); // Restore the previous request method. diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index 28529eb..7086cc8 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -290,8 +290,8 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, En $title = $this->entityFormTitle($entity); // When editing the original values display just the entity label. if ($form_langcode != $entity_langcode) { - $t_args = array('%language' => $languages[$form_langcode]->getName(), '%title' => $entity->label(), '!title' => $title); - $title = empty($source_langcode) ? t('!title [%language translation]', $t_args) : t('Create %language translation of %title', $t_args); + $t_args = array('%language' => $languages[$form_langcode]->getName(), '%title' => $entity->label(), '@title' => $title); + $title = empty($source_langcode) ? t('@title [%language translation]', $t_args) : t('Create %language translation of %title', $t_args); } $form['#title'] = $title; } diff --git a/core/modules/contextual/src/ContextualController.php b/core/modules/contextual/src/ContextualController.php index aeee4a6..dd2bed4 100644 --- a/core/modules/contextual/src/ContextualController.php +++ b/core/modules/contextual/src/ContextualController.php @@ -44,7 +44,7 @@ public function render(Request $request) { '#type' => 'contextual_links', '#contextual_links' => _contextual_id_to_links($id), ); - $rendered[$id] = drupal_render($element); + $rendered[$id] = (string) drupal_render($element); } return new JsonResponse($rendered); diff --git a/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php b/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php index 99daa3b..c7f8e1d 100644 --- a/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php +++ b/core/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php @@ -105,7 +105,7 @@ public function providerTestFilterXss() { // Default SRC tag by leaving it empty. // @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Default_SRC_tag_by_leaving_it_empty - $data[] = array('', ''); + $data[] = array('', ''); // Default SRC tag by leaving it out entirely. // @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Default_SRC_tag_by_leaving_it_out_entirely diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 400d6cc..96d6aa3 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -430,7 +430,7 @@ function template_preprocess_filter_tips(&$variables) { foreach ($variables['tips'] as $name => $tiplist) { foreach ($tiplist as $tip_key => $tip) { $tiplist[$tip_key]['attributes'] = new Attribute(); - $tiplist[$tip_key]['tip'] = Xss::filterAdmin($tiplist[$tip_key]['tip']); + $tiplist[$tip_key]['tip'] = SafeMarkup::checkAdminXss($tiplist[$tip_key]['tip']); } $variables['tips'][$name] = array( diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php index 32977ec..acd4e09 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php +++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php @@ -45,7 +45,7 @@ public function process($text, $langcode) { // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only // allow inline tags that are allowed by default, plus
. $caption = Html::decodeEntities($caption); - $caption = Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br')); + $caption = SafeMarkup::filterXss($caption, array('a', 'em', 'strong', 'cite', 'code', 'br')); // The caption must be non-empty. if (Unicode::strlen($caption) === 0) { diff --git a/core/modules/quickedit/src/QuickEditController.php b/core/modules/quickedit/src/QuickEditController.php index 383f61b..067a73a 100644 --- a/core/modules/quickedit/src/QuickEditController.php +++ b/core/modules/quickedit/src/QuickEditController.php @@ -216,7 +216,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view $response->addCommand(new FieldFormSavedCommand($output, $other_view_modes)); } else { - $output = $this->renderer->renderRoot($form); + $output = (string) $this->renderer->renderRoot($form); // When working with a hidden form, we don't want its CSS/JS to be loaded. if ($request->request->get('nocssjs') !== 'true') { $response->setAttachments($form['#attached']); @@ -228,7 +228,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view $status_messages = array( '#type' => 'status_messages' ); - $response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages))); + $response->addCommand(new FieldFormValidationErrorsCommand((string) $this->renderer->renderRoot($status_messages))); } } @@ -275,7 +275,7 @@ protected function renderField(EntityInterface $entity, $field_name, $langcode, $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args); } - return $this->renderer->renderRoot($output); + return (string) $this->renderer->renderRoot($output); } /** diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php index 1eca0d2..a0f482a 100644 --- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php @@ -108,7 +108,7 @@ public function testSerializerResponses() { // Mock the request content type by setting it on the display handler. $view->display_handler->setContentType('json'); $output = $view->preview(); - $this->assertIdentical($actual_json, drupal_render_root($output), 'The expected JSON preview output was found.'); + $this->assertIdentical($actual_json, (string) drupal_render_root($output), 'The expected JSON preview output was found.'); // Test a 403 callback. $this->drupalGet('test/serialize/denied'); diff --git a/core/modules/search/search.module b/core/modules/search/search.module index b4c4126..a888b1b 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -8,7 +8,6 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; -use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -768,7 +767,7 @@ function search_excerpt($keys, $text, $langcode = NULL) { // Highlight keywords. Must be done at once to prevent conflicts ('strong' // and ''). $text = trim(preg_replace('/' . $boundary . '(?:' . implode('|', $keys) . ')' . $boundary . '/iu', '\0', ' ' . $text . ' ')); - return Xss::filter($text, ['strong']); + return SafeMarkup::filterXss($text, ['strong']); } /** diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index 240aea9..eb37d8e 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -397,7 +397,7 @@ protected function assertRaw($raw, $message = '', $group = 'Other') { if (!$message) { $message = SafeMarkup::format('Raw "@raw" found', array('@raw' => $raw)); } - return $this->assert(strpos($this->getRawContent(), $raw) !== FALSE, $message, $group); + return $this->assert(strpos($this->getRawContent(), (string) $raw) !== FALSE, $message, $group); } /** @@ -424,7 +424,7 @@ protected function assertNoRaw($raw, $message = '', $group = 'Other') { if (!$message) { $message = SafeMarkup::format('Raw "@raw" not found', array('@raw' => $raw)); } - return $this->assert(strpos($this->getRawContent(), $raw) === FALSE, $message, $group); + return $this->assert(strpos($this->getRawContent(), (string) $raw) === FALSE, $message, $group); } /** @@ -808,7 +808,7 @@ protected function assertNoTitle($title, $message = '', $group = 'Other') { * TRUE on pass, FALSE on fail. */ protected function assertThemeOutput($callback, array $variables = array(), $expected = '', $message = '', $group = 'Other') { - $output = \Drupal::theme()->render($callback, $variables); + $output = (string) \Drupal::theme()->render($callback, $variables); $this->verbose( '
' . 'Result:' . '
' . SafeMarkup::checkPlain(var_export($output, TRUE)) . '
' . '
' . 'Expected:' . '
' . SafeMarkup::checkPlain(var_export($expected, TRUE)) . '
' diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php index c5ebaa5..f619bd1 100644 --- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php @@ -7,6 +7,7 @@ namespace Drupal\system\Plugin\Block; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Config\ConfigFactoryInterface; @@ -173,7 +174,7 @@ public function build() { ); $build['site_slogan'] = array( - '#markup' => Xss::filterAdmin($site_config->get('slogan')), + '#markup' => SafeMarkup::checkAdminXss($site_config->get('slogan')), '#access' => $this->configuration['use_site_slogan'], ); diff --git a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php b/core/modules/system/src/Tests/Common/RenderElementTypesTest.php index 3aa3cec..822e77e 100644 --- a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php +++ b/core/modules/system/src/Tests/Common/RenderElementTypesTest.php @@ -43,7 +43,7 @@ protected function setUp() { * Assertion message. */ protected function assertElements(array $elements, $expected_html, $message) { - $actual_html = drupal_render($elements); + $actual_html = (string) drupal_render($elements); $out = ''; $out .= ''; diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php index 7d513eb..e8fe40d 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php @@ -609,10 +609,10 @@ protected function doTestLanguageFallback($entity_type) { // Get an view builder. $controller = $this->entityManager->getViewBuilder($entity_type); $entity2_build = $controller->view($entity2); - $entity2_output = drupal_render($entity2_build); + $entity2_output = (string) drupal_render($entity2_build); $translation = $this->entityManager->getTranslationFromContext($entity2, $default_langcode); $translation_build = $controller->view($translation); - $translation_output = drupal_render($translation_build); + $translation_output = (string) drupal_render($translation_build); $this->assertIdentical($entity2_output, $translation_output, 'When the entity has no translation no fallback is applied.'); // Checks that entity translations are rendered properly. diff --git a/core/modules/system/templates/dropbutton-wrapper.html.twig b/core/modules/system/templates/dropbutton-wrapper.html.twig index ca0ff7e..d4c8d90 100644 --- a/core/modules/system/templates/dropbutton-wrapper.html.twig +++ b/core/modules/system/templates/dropbutton-wrapper.html.twig @@ -12,7 +12,7 @@ * @ingroup themeable */ #} -{% if children %} +{% if children|length %} {% spaceless %}
diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php index 8c1e31f..a11e159 100644 --- a/core/modules/views/src/Plugin/views/PluginBase.php +++ b/core/modules/views/src/Plugin/views/PluginBase.php @@ -377,7 +377,7 @@ protected function viewsTokenReplace($text, $tokens) { '#context' => $twig_tokens, ); - return $this->getRenderer()->render($build); + return (string) $this->getRenderer()->render($build); } else { return $text; diff --git a/core/modules/views/src/Plugin/views/display/Feed.php b/core/modules/views/src/Plugin/views/display/Feed.php index 0798690..e4da10e 100644 --- a/core/modules/views/src/Plugin/views/display/Feed.php +++ b/core/modules/views/src/Plugin/views/display/Feed.php @@ -67,7 +67,7 @@ public static function buildResponse($view_id, $display_id, array $args = []) { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); - $output = $renderer->renderRoot($build); + $output = (string) $renderer->renderRoot($build); if (empty($output)) { throw new NotFoundHttpException(); diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index f17df18..415831f 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -1131,7 +1131,7 @@ public function advancedRender(ResultRow $values) { else { $value = $this->render($values); if (is_array($value)) { - $value = $this->getRenderer()->render($value); + $value = (string) $this->getRenderer()->render($value); } $this->last_render = $value; $this->original_value = $value; @@ -1144,7 +1144,7 @@ public function advancedRender(ResultRow $values) { foreach ($raw_items as $count => $item) { $value = $this->render_item($count, $item); if (is_array($value)) { - $value = $this->getRenderer()->render($value); + $value = (string) $this->getRenderer()->render($value); } $this->last_render = $value; $this->original_value = $this->last_render; @@ -1162,7 +1162,7 @@ public function advancedRender(ResultRow $values) { } if (is_array($value)) { - $value = $this->getRenderer()->render($value); + $value = (string) $this->getRenderer()->render($value); } // This happens here so that renderAsLink can get the unaltered value of // this field as a token rather than the altered value. diff --git a/core/modules/views/src/Plugin/views/row/OpmlFields.php b/core/modules/views/src/Plugin/views/row/OpmlFields.php index 2e936f5..4654bca 100644 --- a/core/modules/views/src/Plugin/views/row/OpmlFields.php +++ b/core/modules/views/src/Plugin/views/row/OpmlFields.php @@ -217,7 +217,7 @@ public function getField($index, $field_id) { if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) { return ''; } - return $this->view->style_plugin->getField($index, $field_id); + return (string) $this->view->style_plugin->getField($index, $field_id); } } diff --git a/core/modules/views/src/Tests/Handler/FieldFieldTest.php b/core/modules/views/src/Tests/Handler/FieldFieldTest.php index fd0da52..fb44dc6 100644 --- a/core/modules/views/src/Tests/Handler/FieldFieldTest.php +++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php @@ -251,18 +251,18 @@ public function testSimpleRender() { $executable = Views::getView('test_field_field_test'); $executable->execute(); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); - $this->assertEqual(3, $executable->getStyle()->getField(0, 'field_test')); - $this->assertEqual(2, $executable->getStyle()->getField(1, 'id')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'id')); + $this->assertEqual(3, (string) $executable->getStyle()->getField(0, 'field_test')); + $this->assertEqual(2, (string) $executable->getStyle()->getField(1, 'id')); // @todo Switch this assertion to assertIdentical('', ...) when // https://www.drupal.org/node/2488006 gets fixed. - $this->assertEqual(0, $executable->getStyle()->getField(1, 'field_test')); - $this->assertEqual(3, $executable->getStyle()->getField(2, 'id')); - $this->assertEqual(8, $executable->getStyle()->getField(2, 'field_test')); - $this->assertEqual(4, $executable->getStyle()->getField(3, 'id')); - $this->assertEqual(5, $executable->getStyle()->getField(3, 'field_test')); - $this->assertEqual(5, $executable->getStyle()->getField(4, 'id')); - $this->assertEqual(6, $executable->getStyle()->getField(4, 'field_test')); + $this->assertEqual(0, (string) $executable->getStyle()->getField(1, 'field_test')); + $this->assertEqual(3, (string) $executable->getStyle()->getField(2, 'id')); + $this->assertEqual(8, (string) $executable->getStyle()->getField(2, 'field_test')); + $this->assertEqual(4, (string) $executable->getStyle()->getField(3, 'id')); + $this->assertEqual(5, (string) $executable->getStyle()->getField(3, 'field_test')); + $this->assertEqual(5, (string) $executable->getStyle()->getField(4, 'id')); + $this->assertEqual(6, (string) $executable->getStyle()->getField(4, 'field_test')); } /** @@ -326,10 +326,10 @@ public function testFieldAliasRender() { $executable->execute(); for ($i = 0; $i < 5; $i++) { - $this->assertEqual($i + 1, $executable->getStyle()->getField($i, 'id')); - $this->assertEqual('test ' . $i, $executable->getStyle()->getField($i, 'name')); + $this->assertEqual($i + 1, (string) $executable->getStyle()->getField($i, 'id')); + $this->assertEqual('test ' . $i, (string) $executable->getStyle()->getField($i, 'name')); $entity = EntityTest::load($i + 1); - $this->assertEqual('test ' . $i . '', $executable->getStyle()->getField($i, 'name_alias')); + $this->assertEqual('test ' . $i . '', (string) $executable->getStyle()->getField($i, 'name_alias')); } } @@ -426,25 +426,25 @@ public function testRevisionRender() { $executable = Views::getView('test_field_field_revision_test'); $executable->execute(); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id')); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'field_test')); - $this->assertEqual('base value', $executable->getStyle()->getField(0, 'name')); - - $this->assertEqual(1, $executable->getStyle()->getField(1, 'id')); - $this->assertEqual(2, $executable->getStyle()->getField(1, 'revision_id')); - $this->assertEqual(2, $executable->getStyle()->getField(1, 'field_test')); - $this->assertEqual('revision value1', $executable->getStyle()->getField(1, 'name')); - - $this->assertEqual(1, $executable->getStyle()->getField(2, 'id')); - $this->assertEqual(3, $executable->getStyle()->getField(2, 'revision_id')); - $this->assertEqual(3, $executable->getStyle()->getField(2, 'field_test')); - $this->assertEqual('revision value2', $executable->getStyle()->getField(2, 'name')); - - $this->assertEqual(2, $executable->getStyle()->getField(3, 'id')); - $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id')); - $this->assertEqual(4, $executable->getStyle()->getField(3, 'field_test')); - $this->assertEqual('next entity value', $executable->getStyle()->getField(3, 'name')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'id')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'revision_id')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'field_test')); + $this->assertEqual('base value', (string) $executable->getStyle()->getField(0, 'name')); + + $this->assertEqual(1, (string) $executable->getStyle()->getField(1, 'id')); + $this->assertEqual(2, (string) $executable->getStyle()->getField(1, 'revision_id')); + $this->assertEqual(2, (string) $executable->getStyle()->getField(1, 'field_test')); + $this->assertEqual('revision value1', (string) $executable->getStyle()->getField(1, 'name')); + + $this->assertEqual(1, (string) $executable->getStyle()->getField(2, 'id')); + $this->assertEqual(3, (string) $executable->getStyle()->getField(2, 'revision_id')); + $this->assertEqual(3, (string) $executable->getStyle()->getField(2, 'field_test')); + $this->assertEqual('revision value2', (string) $executable->getStyle()->getField(2, 'name')); + + $this->assertEqual(2, (string) $executable->getStyle()->getField(3, 'id')); + $this->assertEqual(4, (string) $executable->getStyle()->getField(3, 'revision_id')); + $this->assertEqual(4, (string) $executable->getStyle()->getField(3, 'field_test')); + $this->assertEqual('next entity value', (string) $executable->getStyle()->getField(3, 'name')); } /** @@ -484,33 +484,33 @@ public function testRevisionComplexRender() { $executable = Views::getView('test_field_field_revision_complex_test'); $executable->execute(); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'id')); - $this->assertEqual(1, $executable->getStyle()->getField(0, 'revision_id')); - $this->assertEqual($this->testUsers[0]->getTimeZone(), $executable->getStyle()->getField(0, 'timezone')); - $this->assertEqual('1, 3, 7', $executable->getStyle()->getField(0, 'field_test_multiple')); - $this->assertEqual('1', $executable->getStyle()->getField(0, 'field_test_multiple_1')); - $this->assertEqual('3, 7', $executable->getStyle()->getField(0, 'field_test_multiple_2')); - - $this->assertEqual(1, $executable->getStyle()->getField(1, 'id')); - $this->assertEqual(2, $executable->getStyle()->getField(1, 'revision_id')); - $this->assertEqual($this->testUsers[1]->getTimeZone(), $executable->getStyle()->getField(1, 'timezone')); - $this->assertEqual('0, 3, 5', $executable->getStyle()->getField(1, 'field_test_multiple')); - $this->assertEqual('0', $executable->getStyle()->getField(1, 'field_test_multiple_1')); - $this->assertEqual('3, 5', $executable->getStyle()->getField(1, 'field_test_multiple_2')); - - $this->assertEqual(1, $executable->getStyle()->getField(2, 'id')); - $this->assertEqual(3, $executable->getStyle()->getField(2, 'revision_id')); - $this->assertEqual($this->testUsers[2]->getTimeZone(), $executable->getStyle()->getField(2, 'timezone')); - $this->assertEqual('9, 9, 9', $executable->getStyle()->getField(2, 'field_test_multiple')); - $this->assertEqual('9', $executable->getStyle()->getField(2, 'field_test_multiple_1')); - $this->assertEqual('9, 9', $executable->getStyle()->getField(2, 'field_test_multiple_2')); - - $this->assertEqual(2, $executable->getStyle()->getField(3, 'id')); - $this->assertEqual(4, $executable->getStyle()->getField(3, 'revision_id')); - $this->assertEqual($this->testUsers[3]->getTimeZone(), $executable->getStyle()->getField(3, 'timezone')); - $this->assertEqual('2, 9, 9', $executable->getStyle()->getField(3, 'field_test_multiple')); - $this->assertEqual('2', $executable->getStyle()->getField(3, 'field_test_multiple_1')); - $this->assertEqual('9, 9', $executable->getStyle()->getField(3, 'field_test_multiple_2')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'id')); + $this->assertEqual(1, (string) $executable->getStyle()->getField(0, 'revision_id')); + $this->assertEqual($this->testUsers[0]->getTimeZone(), (string) $executable->getStyle()->getField(0, 'timezone')); + $this->assertEqual('1, 3, 7', (string) $executable->getStyle()->getField(0, 'field_test_multiple')); + $this->assertEqual('1', (string) $executable->getStyle()->getField(0, 'field_test_multiple_1')); + $this->assertEqual('3, 7', (string) $executable->getStyle()->getField(0, 'field_test_multiple_2')); + + $this->assertEqual(1, (string) $executable->getStyle()->getField(1, 'id')); + $this->assertEqual(2, (string) $executable->getStyle()->getField(1, 'revision_id')); + $this->assertEqual($this->testUsers[1]->getTimeZone(), (string) $executable->getStyle()->getField(1, 'timezone')); + $this->assertEqual('0, 3, 5', (string) $executable->getStyle()->getField(1, 'field_test_multiple')); + $this->assertEqual('0', (string) $executable->getStyle()->getField(1, 'field_test_multiple_1')); + $this->assertEqual('3, 5', (string) $executable->getStyle()->getField(1, 'field_test_multiple_2')); + + $this->assertEqual(1, (string) $executable->getStyle()->getField(2, 'id')); + $this->assertEqual(3, (string) $executable->getStyle()->getField(2, 'revision_id')); + $this->assertEqual($this->testUsers[2]->getTimeZone(), (string) $executable->getStyle()->getField(2, 'timezone')); + $this->assertEqual('9, 9, 9', (string) $executable->getStyle()->getField(2, 'field_test_multiple')); + $this->assertEqual('9', (string) $executable->getStyle()->getField(2, 'field_test_multiple_1')); + $this->assertEqual('9, 9', (string) $executable->getStyle()->getField(2, 'field_test_multiple_2')); + + $this->assertEqual(2, (string) $executable->getStyle()->getField(3, 'id')); + $this->assertEqual(4, (string) $executable->getStyle()->getField(3, 'revision_id')); + $this->assertEqual($this->testUsers[3]->getTimeZone(), (string) $executable->getStyle()->getField(3, 'timezone')); + $this->assertEqual('2, 9, 9', (string) $executable->getStyle()->getField(3, 'field_test_multiple')); + $this->assertEqual('2', (string) $executable->getStyle()->getField(3, 'field_test_multiple_1')); + $this->assertEqual('9, 9', (string) $executable->getStyle()->getField(3, 'field_test_multiple_2')); } /** @@ -533,7 +533,7 @@ public function testMissingBundleFieldRender() { // @todo Switch this assertion to assertIdentical('', ...) when // https://www.drupal.org/node/2488006 gets fixed. - $this->assertEqual(0, $executable->getStyle()->getField(1, 'field_test')); + $this->assertEqual(0, (string) $executable->getStyle()->getField(1, 'field_test')); } } diff --git a/core/modules/views/src/Tests/QueryGroupByTest.php b/core/modules/views/src/Tests/QueryGroupByTest.php index 768aaaf..ad6e28e 100644 --- a/core/modules/views/src/Tests/QueryGroupByTest.php +++ b/core/modules/views/src/Tests/QueryGroupByTest.php @@ -253,9 +253,9 @@ public function testGroupByFieldWithCardinality() { $this->executeView($view); $this->assertEqual(2, count($view->result)); - $this->assertEqual(3, $view->getStyle()->getField(0, 'id')); + $this->assertEqual(3, (string) $view->getStyle()->getField(0, 'id')); $this->assertEqual('1', $view->getStyle()->getField(0, 'field_test')); - $this->assertEqual(6, $view->getStyle()->getField(1, 'id')); + $this->assertEqual(6, (string) $view->getStyle()->getField(1, 'id')); $this->assertEqual('2', $view->getStyle()->getField(1, 'field_test')); $entities[2]->field_test[0]->value = 3; @@ -267,15 +267,15 @@ public function testGroupByFieldWithCardinality() { $this->executeView($view); $this->assertEqual(5, count($view->result)); - $this->assertEqual(3, $view->getStyle()->getField(0, 'id')); + $this->assertEqual(3, (string) $view->getStyle()->getField(0, 'id')); $this->assertEqual('1', $view->getStyle()->getField(0, 'field_test')); - $this->assertEqual(3, $view->getStyle()->getField(1, 'id')); + $this->assertEqual(3, (string) $view->getStyle()->getField(1, 'id')); $this->assertEqual('2', $view->getStyle()->getField(1, 'field_test')); - $this->assertEqual(1, $view->getStyle()->getField(2, 'id')); + $this->assertEqual(1, (string) $view->getStyle()->getField(2, 'id')); $this->assertEqual('3', $view->getStyle()->getField(2, 'field_test')); - $this->assertEqual(1, $view->getStyle()->getField(3, 'id')); + $this->assertEqual(1, (string) $view->getStyle()->getField(3, 'id')); $this->assertEqual('4', $view->getStyle()->getField(3, 'field_test')); - $this->assertEqual(1, $view->getStyle()->getField(4, 'id')); + $this->assertEqual(1, (string) $view->getStyle()->getField(4, 'id')); $this->assertEqual('5', $view->getStyle()->getField(4, 'field_test')); // Check that translated values are correctly retrieved and are not grouped @@ -288,7 +288,7 @@ public function testGroupByFieldWithCardinality() { $this->executeView($view); $this->assertEqual(6, count($view->result)); - $this->assertEqual(3, $view->getStyle()->getField(5, 'id')); + $this->assertEqual(3, (string) $view->getStyle()->getField(5, 'id')); $this->assertEqual('6', $view->getStyle()->getField(5, 'field_test')); } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php index a3fe908..4472bcd 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php @@ -237,8 +237,8 @@ public function testUncacheableParent($element, $args, $placeholder_cid_keys, ar // No #cache on parent element. $element['#prefix'] = '

#cache disabled

'; $output = $this->renderer->renderRoot($element); - $this->assertSame('

#cache disabled

This is a rendered placeholder!

', $output, 'Output is overridden.'); - $this->assertSame('

#cache disabled

This is a rendered placeholder!

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

#cache disabled

This is a rendered placeholder!

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

#cache disabled

This is a rendered placeholder!

', (string) $element['#markup'], '#markup is overridden.'); $expected_js_settings = [ 'foo' => 'bar', 'dynamic_animal' => $args[0], @@ -283,9 +283,9 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys, $element['#cache'] = ['keys' => ['placeholder_test_GET']]; $element['#prefix'] = '

#cache enabled, GET

'; $output = $this->renderer->renderRoot($element); - $this->assertSame('

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

', (string) $element['#markup'], '#markup is overridden.'); $expected_js_settings = [ 'foo' => 'bar', 'dynamic_animal' => $args[0], @@ -321,9 +321,9 @@ public function testCacheableParent($test_element, $args, $placeholder_cid_keys, $element['#cache'] = ['keys' => ['placeholder_test_GET']]; $element['#prefix'] = '

#cache enabled, GET

'; $output = $this->renderer->renderRoot($element); - $this->assertSame('

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

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

#cache enabled, GET

This is a rendered placeholder!

', (string) $element['#markup'], '#markup is overridden.'); $expected_js_settings = [ 'foo' => 'bar', 'dynamic_animal' => $args[0], @@ -351,9 +351,9 @@ public function testCacheableParentWithPostRequest($test_element, $args) { $element['#cache'] = ['keys' => ['placeholder_test_POST']]; $element['#prefix'] = '

#cache enabled, POST

'; $output = $this->renderer->renderRoot($element); - $this->assertSame('

#cache enabled, POST

This is a rendered placeholder!

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

#cache enabled, POST

This is a rendered placeholder!

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

#cache enabled, POST

This is a rendered placeholder!

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

#cache enabled, POST

This is a rendered placeholder!

', (string) $element['#markup'], '#markup is overridden.'); $expected_js_settings = [ 'foo' => 'bar', 'dynamic_animal' => $args[0], @@ -383,7 +383,7 @@ public function testRecursivePlaceholder() { $output = $this->renderer->renderRoot($element); $this->assertEquals('

This is a rendered placeholder!

', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.'); - $this->assertSame($element['#markup'], '

This is a rendered placeholder!

', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.'); + $this->assertSame((string) $element['#markup'], '

This is a rendered placeholder!

', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.'); $expected_js_settings = [ 'dynamic_animal' => $args[0], ]; @@ -546,9 +546,9 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
HTML; - $this->assertSame($expected_output, $output, 'Output is not overridden.'); + $this->assertSame($expected_output, (string) $output, 'Output is not overridden.'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); - $this->assertSame($expected_output, $output, '#markup is not overridden.'); + $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.'); $expected_js_settings = [ 'foo' => 'bar', 'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE], @@ -595,7 +595,7 @@ public function testRenderChildrenPlaceholdersDifferentArguments() { // GET request: #cache enabled, cache hit. $element = $test_element; $output = $this->renderer->renderRoot($element); - $this->assertSame($expected_output, $output, 'Output is not overridden.'); + $this->assertSame($expected_output, (string) $output, 'Output is not overridden.'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.'); @@ -604,8 +604,8 @@ public function testRenderChildrenPlaceholdersDifferentArguments() { unset($test_element['#cache']); $element = $test_element; $output = $this->renderer->renderRoot($element); - $this->assertSame($expected_output, $output, 'Output is not overridden.'); - $this->assertSame($expected_output, $output, '#markup is not overridden.'); + $this->assertSame($expected_output, (string) $output, 'Output is not overridden.'); + $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.'); $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #lazy_builder callback exist.'); } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index a2f91ea..8439c0a 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\SafeString; use Drupal\Core\Template\Attribute; /** @@ -43,7 +44,7 @@ public function testRenderBasic($build, $expected, callable $setup_code = NULL) $setup_code(); } - $this->assertSame($expected, $this->renderer->render($build)); + $this->assertSame($expected, (string) $this->renderer->render($build)); } /** @@ -470,10 +471,10 @@ protected function assertAccess($build, $access) { $sensitive_content = $this->randomContextValue(); $build['#markup'] = $sensitive_content; if ($access) { - $this->assertSame($sensitive_content, $this->renderer->render($build)); + $this->assertSame($sensitive_content, (string) $this->renderer->render($build)); } else { - $this->assertSame('', $this->renderer->render($build)); + $this->assertSame('', (string) $this->renderer->render($build)); } } @@ -653,8 +654,8 @@ public function testRenderCacheProperties(array $expected_results) { ], // Collect expected property names. '#cache_properties' => array_keys(array_filter($expected_results)), - 'child1' => ['#markup' => 1], - 'child2' => ['#markup' => 2], + 'child1' => ['#markup' => new SafeString(1, 'UTF-8')], + 'child2' => ['#markup' => new SafeString(2, 'UTF-8')], '#custom_property' => ['custom_value'], ]; $this->renderer->render($element); diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 9f8bc4a..3c2ce61 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -7,6 +7,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Extension\Extension; +use Drupal\Core\Render\SafeString; /** * Implements hook_theme(). @@ -103,7 +104,8 @@ function twig_render_template($template_file, array $variables) { $output['debug_info'] .= "\n\n"; $output['debug_suffix'] .= "\n\n\n"; } - return SafeMarkup::set(implode('', $output)); + // This output has already been rendered and is therefore considered safe. + return new SafeString(implode('', $output), 'UTF-8'); } /**
' . SafeMarkup::checkPlain($expected_html) . '