diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 52a849c..c9fae11 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -518,7 +518,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; - $elements['#markup'] = $prefix . $elements['#children'] . $suffix; + $elements['#markup'] = SafeString::create($prefix . $elements['#children'] . $suffix); // We've rendered this element (and its subtree!), now update the context. $context->update($elements); @@ -553,7 +553,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $context->bubble(); $elements['#printed'] = TRUE; - return SafeString::create($elements['#markup']); + return $elements['#markup']; } /** diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php index 31c10a7..f6c5714 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManager.php +++ b/core/lib/Drupal/Core/Theme/ThemeManager.php @@ -7,12 +7,13 @@ namespace Drupal\Core\Theme; +use Drupal\Component\Utility\SafeStringInterface; +use Drupal\Core\Render\SafeString; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\StackedRouteMatchInterface; use Symfony\Component\HttpFoundation\RequestStack; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Template\Attribute; -use Drupal\Component\Utility\SafeMarkup; /** * Provides the default implementation of a theme manager. @@ -317,7 +318,10 @@ public function render($hook, array $variables) { $output = ''; if (isset($info['function'])) { if (function_exists($info['function'])) { - $output = SafeMarkup::set($info['function']($variables)); + // Theme functions do not render via the theme engine, so the output is + // not autoescaped. However, we can only presume that the theme function + // has been written correctly and that the markup is safe. + $output = SafeString::create($info['function']($variables)); } } else { @@ -387,7 +391,7 @@ public function render($hook, array $variables) { $output = $render_function($template_file, $variables); } - return (string) $output; + return ($output instanceof SafeStringInterface) ? $output : (string) $output; } /** diff --git a/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php b/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php index ebbfcb5..b61ff84 100644 --- a/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php +++ b/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php @@ -26,8 +26,8 @@ * @param array $variables * An associative array of theme variables. * - * @return string - * The rendered output. + * @return string|\Drupal\Component\Utility\SafeStringInterface + * The rendered output, or a SafeString object. */ public function render($hook, array $variables); diff --git a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php index 2fcf468..42182c9 100644 --- a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php +++ b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php @@ -91,11 +91,11 @@ public function testCommentFieldName() { $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['field_name']->advancedRender($view->result[0]); }); - $this->assertIdentical($this->comment->getFieldName(), $output); + $this->assertEqual($this->comment->getFieldName(), $output); $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['field_name']->advancedRender($view->result[1]); }); - $this->assertIdentical($this->customComment->getFieldName(), $output); + $this->assertEqual($this->customComment->getFieldName(), $output); } } diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml index 4c9397a..318a90c 100644 --- a/core/modules/serialization/serialization.services.yml +++ b/core/modules/serialization/serialization.services.yml @@ -30,6 +30,10 @@ services: arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem'] tags: - { name: normalizer, priority: 20 } + serializer.normalizer.safe_string: + class: Drupal\serialization\Normalizer\SafeStringNormalizer + tags: + - { name: normalizer } serializer.normalizer.typed_data: class: Drupal\serialization\Normalizer\TypedDataNormalizer tags: diff --git a/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php b/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php new file mode 100644 index 0000000..16b7fcd --- /dev/null +++ b/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php @@ -0,0 +1,29 @@ +executeInRenderContext(new RenderContext(), function() use ($callback, $variables) { + // The string cast is necessary because theme functions return + // SafeStringInterface objects. This means we can assert that $expected + // matches the theme output without having to worry about 0 == ''. + $output = (string) $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) { return \Drupal::theme()->render($callback, $variables); }); $this->verbose( diff --git a/core/modules/system/src/Tests/Theme/ThemeTest.php b/core/modules/system/src/Tests/Theme/ThemeTest.php index c2c0e0b..a0095b8 100644 --- a/core/modules/system/src/Tests/Theme/ThemeTest.php +++ b/core/modules/system/src/Tests/Theme/ThemeTest.php @@ -13,6 +13,7 @@ use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; +use Drupal\Component\Utility\SafeStringInterface; /** * Tests low-level theme functions. @@ -59,12 +60,19 @@ function testAttributeMerging() { * Test that _theme() returns expected data types. */ function testThemeDataTypes() { - // theme_test_false is an implemented theme hook so \Drupal::theme() service should - // return a string, even though the theme function itself can return anything. - $foos = array('null' => NULL, 'false' => FALSE, 'integer' => 1, 'string' => 'foo'); + // theme_test_false is an implemented theme hook so \Drupal::theme() service + // should return a string or an object that implements SafeStringInterface, + // even though the theme function itself can return anything. + $foos = array('null' => NULL, 'false' => FALSE, 'integer' => 1, 'string' => 'foo', 'empty_string' => ''); foreach ($foos as $type => $example) { $output = \Drupal::theme()->render('theme_test_foo', array('foo' => $example)); - $this->assertTrue(is_string($output), format_string('\Drupal::theme() returns a string for data type !type.', array('!type' => $type))); + $this->assertTrue($output instanceof SafeStringInterface || is_string($output), format_string('\Drupal::theme() returns an object that implements SafeStringInterface or a string for data type !type.', array('!type' => $type))); + if ($output instanceof SafeStringInterface) { + $this->assertIdentical((string) $example, $output->__toString()); + } + elseif (is_string($output)) { + $this->assertIdentical($output, '', 'A string will be return when the theme returns an empty string.'); + } } // suggestionnotimplemented is not an implemented theme hook so \Drupal::theme() service diff --git a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php index 5db1e3e..42f9b429 100644 --- a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php +++ b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php @@ -63,7 +63,7 @@ public function testUserName() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.'); + $this->assertEqual($render, $username, 'If the user is not linked the username should be printed out for a normal user.'); } diff --git a/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php b/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php index 6cd22ac..975c10d 100644 --- a/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php +++ b/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php @@ -167,8 +167,10 @@ public function preRender(&$values); * @param \Drupal\views\ResultRow $values * The values retrieved from a single row of a view's query result. * - * @return string - * The rendered output. + * @return string|\Drupal\Component\Utility\SafeStringInterface + * The rendered output. If the output is safe it will be wrapped in an + * object that implements SafeStringInterface. If it is empty or unsafe it + * will be a string. * */ public function render(ResultRow $values); @@ -202,8 +204,10 @@ public function postRender(ResultRow $row, $output); * @param \Drupal\views\ResultRow $values * The values retrieved from a single row of a view's query result. * - * @return string - * The advanced rendered output. + * @return string|\Drupal\Component\Utility\SafeStringInterface + * The advanced rendered output. If the output is safe it will be wrapped in + * an object that implements SafeStringInterface. If it is empty or unsafe + * it will be a string. * */ public function advancedRender(ResultRow $values); @@ -236,30 +240,14 @@ public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE); * - ellipsis: Show an ellipsis (…) at the end of the trimmed string. * - html: Make sure that the html is correct. * - * @return string - * The rendered string. + * @return string|\Drupal\Component\Utility\SafeStringInterface + * The rendered output. If the output is safe it will be wrapped in an + * object that implements SafeStringInterface. If it is empty or unsafe it + * will be a string. */ public function renderText($alter); /** - * Trims the field down to the specified length. - * - * @param array $alter - * The alter array of options to use. - * - max_length: Maximum length of the string, the rest gets truncated. - * - word_boundary: Trim only on a word boundary. - * - ellipsis: Show an ellipsis (…) at the end of the trimmed string. - * - html: Make sure that the html is correct. - * - * @param string $value - * The string which should be trimmed. - * - * @return string - * The rendered trimmed string. - */ - public function renderTrimText($alter, $value); - - /** * Gets the 'render' tokens to use for advanced rendering. * * This runs through all of the fields and arguments that @@ -280,8 +268,10 @@ public function getRenderTokens($item); * @param \Drupal\views\ResultRow $values * Holds single row of a view's result set. * - * @return string|false - * Returns rendered output of the given theme implementation. + * @return string|\Drupal\Component\Utility\SafeStringInterface + * Returns rendered output of the given theme implementation. If the output + * is safe it will be wrapped in an object that implements + * SafeStringInterface. If it is empty or unsafe it will be a string. */ function theme(ResultRow $values); diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 80ceb75..996ef31 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\SafeStringInterface; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; @@ -1138,7 +1139,7 @@ public function advancedRender(ResultRow $values) { else { $value = $this->render($values); if (is_array($value)) { - $value = (string) $this->getRenderer()->render($value); + $value = $this->getRenderer()->render($value); } $this->last_render = $value; $this->original_value = $value; @@ -1169,14 +1170,16 @@ public function advancedRender(ResultRow $values) { } if (is_array($value)) { - $value = (string) $this->getRenderer()->render($value); + $value = $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. $this->last_render = $value; } - if (empty($this->last_render)) { + // String cast is necessary to test emptiness of SafeStringInterface + // objects. + if (empty((string) $this->last_render)) { if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) { $alter = $this->options['alter']; $alter['alter_text'] = 1; @@ -1185,9 +1188,6 @@ public function advancedRender(ResultRow $values) { $this->last_render = $this->renderText($alter); } } - // @todo Fix this in https://www.drupal.org/node/2280961. - $this->last_render = SafeMarkup::set($this->last_render); - return $this->last_render; } @@ -1196,6 +1196,10 @@ public function advancedRender(ResultRow $values) { * {@inheritdoc} */ public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) { + // Convert SafeStringInterface to a string for checking. + if ($value instanceof SafeStringInterface) { + $value = (string) $value; + } if (!isset($value)) { $empty = TRUE; } @@ -1213,7 +1217,14 @@ public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) { * {@inheritdoc} */ public function renderText($alter) { - $value = $this->last_render; + // We need to preserve the safeness of the value regardless of the + // alterations made by this method. Any alterations or replacements made + // within this method need to ensure that at the minimum the result is + // XSS admin filtered. See self::renderAltered() as an example that does. + $value_is_safe = SafeMarkup::isSafe($this->last_render); + // Cast to a string so that empty checks and string functions work as + // expected. + $value = (string) $this->last_render; if (!empty($alter['alter_text']) && $alter['text'] !== '') { $tokens = $this->getRenderTokens($alter); @@ -1239,6 +1250,9 @@ public function renderText($alter) { if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) { // If we got here then $alter contains the value of "No results text" // and so there is nothing left to do. + if ($value_is_safe) { + $value = ViewsRenderPipelineSafeString::create($value); + } return $value; } @@ -1275,6 +1289,12 @@ public function renderText($alter) { if (!empty($alter['nl2br'])) { $value = nl2br($value); } + + // Preserve whether or not the string is safe. Since $suffix comes from + // \Drupal::l(), it is safe to append. + if ($value_is_safe) { + $value = ViewsRenderPipelineSafeString::create($value . $suffix); + } $this->last_render_text = $value; if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) { @@ -1284,20 +1304,42 @@ public function renderText($alter) { $value = $this->renderAsLink($alter, $value, $tokens); } - return $value . $suffix; + // Preserve whether or not the string is safe. Since $suffix comes from + // \Drupal::l(), it is safe to append. + if ($value_is_safe) { + return ViewsRenderPipelineSafeString::create($value . $suffix); + } + else { + // If the string is not already marked safe, it is still OK to return it + // because it will be sanitized by Twig. + return $value . $suffix; + } } /** * Render this field as user-defined altered text. */ protected function renderAltered($alter, $tokens) { - return ViewsRenderPipelineSafeString::create($this->viewsTokenReplace($alter['text'], $tokens)); + return $this->viewsTokenReplace($alter['text'], $tokens); } /** - * {@inheritdoc} + * Trims the field down to the specified length. + * + * @param array $alter + * The alter array of options to use. + * - max_length: Maximum length of the string, the rest gets truncated. + * - word_boundary: Trim only on a word boundary. + * - ellipsis: Show an ellipsis (…) at the end of the trimmed string. + * - html: Make sure that the html is correct. + * + * @param string $value + * The string which should be trimmed. + * + * @return string + * The rendered trimmed string. */ - public function renderTrimText($alter, $value) { + protected function renderTrimText($alter, $value) { if (!empty($alter['strip_tags'])) { // NOTE: It's possible that some external fields might override the // element type. diff --git a/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php b/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php index c2c9af5..70edf22 100644 --- a/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php +++ b/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php @@ -64,9 +64,9 @@ public function testFieldFileSize() { // Test with the bytes option. $view->field['age']->options['file_size_display'] = 'bytes'; $this->assertEqual($view->field['age']->advancedRender($view->result[0]), ''); - $this->assertEqual($view->field['age']->advancedRender($view->result[1]), 10); - $this->assertEqual($view->field['age']->advancedRender($view->result[2]), 1000); - $this->assertEqual($view->field['age']->advancedRender($view->result[3]), 10000); + $this->assertEqual($view->field['age']->advancedRender($view->result[1]), '10'); + $this->assertEqual($view->field['age']->advancedRender($view->result[2]), '1000'); + $this->assertEqual($view->field['age']->advancedRender($view->result[3]), '10000'); } } diff --git a/core/modules/views/src/Tests/Handler/FieldUnitTest.php b/core/modules/views/src/Tests/Handler/FieldUnitTest.php index 85fabe3..7ad9cdc 100644 --- a/core/modules/views/src/Tests/Handler/FieldUnitTest.php +++ b/core/modules/views/src/Tests/Handler/FieldUnitTest.php @@ -324,7 +324,8 @@ function testEmpty() { /** * Tests the hide if empty functionality. * - * This tests alters the result to get easier and less coupled results. + * This tests alters the result to get easier and less coupled results. It is + * important that assertIdentical() is used in this test since in PHP 0 == ''. */ function _testHideIfEmpty() { /** @var \Drupal\Core\Render\RendererInterface $renderer */ @@ -349,7 +350,7 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'By default, a string should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; @@ -363,14 +364,14 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, '0', 'By default, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'By default, "0" should not be treated as empty.'); // Test when results are not rewritten and non-zero empty values are hidden. $view->field['name']->options['hide_alter_empty'] = TRUE; @@ -382,7 +383,7 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; @@ -396,14 +397,14 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.'); // Test when results are not rewritten and all empty values are hidden. $view->field['name']->options['hide_alter_empty'] = TRUE; @@ -437,28 +438,28 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.'); // Test when results are rewritten to an empty string and non-zero empty results are hidden. $view->field['name']->options['hide_alter_empty'] = TRUE; @@ -472,7 +473,7 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; @@ -486,14 +487,14 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.'); // Test when results are rewritten to zero as a string and non-zero empty // results are hidden. @@ -508,28 +509,28 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.'); // Test zero as an integer. $view->result[0]->{$column_map_reversed['name']} = 0; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.'); // Test when results are rewritten to a valid string and non-zero empty // results are hidden. @@ -544,7 +545,7 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; @@ -558,14 +559,14 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.'); // Test zero as a string. $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.'); + $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.'); // Test when results are rewritten to zero as a string and all empty // original values and results are hidden. @@ -580,7 +581,7 @@ function _testHideIfEmpty() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.'); + $this->assertIdentical((string) $render, "", 'If the rewritten string is zero, it should be treated as empty.'); // Test an empty string. $view->result[0]->{$column_map_reversed['name']} = ""; @@ -623,20 +624,20 @@ function _testEmptyText() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.'); + $this->assertIdentical((string) $render, $empty_text, 'If a field is empty, the empty text should be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = "0"; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.'); + $this->assertIdentical((string) $render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = "0"; $view->field['name']->options['empty_zero'] = TRUE; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.'); + $this->assertIdentical((string) $render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.'); $view->result[0]->{$column_map_reversed['name']} = ""; $view->field['name']->options['alter']['alter_text'] = TRUE; @@ -645,13 +646,13 @@ function _testEmptyText() { $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.'); + $this->assertIdentical((string) $render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.'); $view->field['name']->options['hide_alter_empty'] = TRUE; $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) { return $view->field['name']->advancedRender($view->result[0]); }); - $this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.'); + $this->assertIdentical((string) $render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.'); } /** diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index c6845f5..d48346c 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -36,6 +36,18 @@ class RendererTest extends RendererTestBase { ]; /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + // Reset the static list of SafeStrings to prevent bleeding between tests. + $reflected_class = new \ReflectionClass('\Drupal\Component\Utility\SafeMarkup'); + $reflected_property = $reflected_class->getProperty('safeStrings'); + $reflected_property->setAccessible(true); + $reflected_property->setValue([]); + } + + /** * @covers ::render * @covers ::doRender * @@ -47,7 +59,15 @@ public function testRenderBasic($build, $expected, callable $setup_code = NULL) $setup_code(); } - $this->assertSame($expected, (string) $this->renderer->renderRoot($build)); + if (isset($build['#markup'])) { + $this->assertFalse(SafeMarkup::isSafe($build['#markup']), 'The #markup value is not marked safe before rendering.'); + } + $render_output = $this->renderer->renderRoot($build); + $this->assertSame($expected, (string) $render_output); + if ($render_output !== '') { + $this->assertTrue(SafeMarkup::isSafe($render_output), 'Output of render is marked safe.'); + $this->assertTrue(SafeMarkup::isSafe($build['#markup']), 'The #markup value is marked safe after rendering.'); + } } /** diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index af5ebe7..de085ff 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Render\SafeString; use Drupal\Core\Extension\Extension; /** @@ -44,7 +45,7 @@ function twig_init(Extension $theme) { * @param array $variables * A keyed array of variables that will appear in the output. * - * @return string + * @return string|\Drupal\Component\Utility\SafeStringInterface * The output generated by the template, plus any debug information. */ function twig_render_template($template_file, array $variables) { @@ -74,7 +75,7 @@ function twig_render_template($template_file, array $variables) { } if ($twig_service->isDebug()) { $output['debug_prefix'] .= "\n\n"; - $output['debug_prefix'] .= "\n"; + $output['debug_prefix'] .= "\n"; // If there are theme suggestions, reverse the array so more specific // suggestions are shown first. if (!empty($variables['theme_hook_suggestions'])) { @@ -108,12 +109,13 @@ function twig_render_template($template_file, array $variables) { $prefix = ($template == $current_template) ? 'x' : '*'; $suggestion = $prefix . ' ' . $template; } - $output['debug_info'] .= "\n"; + $output['debug_info'] .= "\n"; } - $output['debug_info'] .= "\n\n"; - $output['debug_suffix'] .= "\n\n\n"; + $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 SafeString::create(implode('', $output)); } /**