diff --git a/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php b/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php new file mode 100644 index 0000000..f418e07 --- /dev/null +++ b/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php @@ -0,0 +1,49 @@ +getArrayCopy()); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($index, $newval) {} + + /** + * {@inheritdoc} + */ + public function append($values) {} + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) {} + + /** + * {@inheritdoc} + */ + public function exchangeArray($serialized) {} + +} diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index 21755a5..45f346d 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Template; +use Drupal\Component\Utility\ReadOnlyArrayObject; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Render\Markup; use Drupal\Core\State\StateInterface; @@ -140,7 +141,43 @@ public function getTemplateClass($name, $index = NULL) { public function renderInline($template_string, array $context = []) { // Prefix all inline templates with a special comment. $template_string = '{# inline_template_start #}' . $template_string; + $this->wrapContext($context); return Markup::create($this->loadTemplate($template_string, NULL)->render($context)); } + /** + * Wraps render arrays with objects, to prevent modification from Twig. + * + * @param array &$context + * An array of parameters. + */ + public function wrapContext(&$context) { + if (is_array($context)) { + foreach ($context as $key => $value) { + if (is_array($context[$key])) { + $context[$key] = new ReadOnlyArrayObject($context[$key]); + } + } + } + } + + /** + * Unwraps read only objects into render arrays. + * + * @param array &$context + * An array of parameters. + */ + public function unWrapContext(&$context) { + if (is_object($context) && $context instanceof ReadOnlyArrayObject) { + $context = $context->getArrayCopy(); + } + if (is_array($context)) { + foreach ($context as $key => $value) { + if (is_object($context[$key]) && $context[$key] instanceof ReadOnlyArrayObject) { + $context[$key] = $context[$key]->getArrayCopy(); + } + } + } + } + } diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 520a8be..58b0432 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Render\MarkupInterface; +use Drupal\Component\Utility\ReadOnlyArrayObject; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Render\AttachmentsInterface; @@ -300,7 +301,7 @@ public function getLink($text, $url, $attributes = []) { '#title' => $text, '#url' => $url, ]; - return $build; + return new ReadOnlyArrayObject($build); } /** @@ -448,7 +449,7 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $ if (is_scalar($arg)) { $return = (string) $arg; } - elseif (is_object($arg)) { + elseif (is_object($arg) && !($arg instanceof ReadOnlyArrayObject)) { if ($arg instanceof RenderableInterface) { $arg = $arg->toRenderable(); } @@ -465,6 +466,9 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $ throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.'); } } + elseif (is_array($arg)) { + throw new \Exception('Render arrays cannot be printed.'); + } // We have a string or an object converted to a string: Autoescape it! if (isset($return)) { @@ -479,8 +483,9 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $ return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); } - // This is a normal render array, which is safe by definition, with + // This is a wrapped render array, which is safe by definition, with // special simple cases already handled. + $arg = $arg->getArrayCopy(); // Early return if this element was pre-rendered (no need to re-render). if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) { @@ -561,8 +566,7 @@ public function renderVar($arg) { if (is_scalar($arg)) { return $arg; } - - if (is_object($arg)) { + else if (is_object($arg) && !($arg instanceof ReadOnlyArrayObject)) { $this->bubbleArgMetadata($arg); if ($arg instanceof RenderableInterface) { $arg = $arg->toRenderable(); @@ -580,6 +584,11 @@ public function renderVar($arg) { throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.'); } } + elseif (is_array($arg)) { + throw new \Exception('Render arrays cannot be printed.'); + } + + $arg = $arg->getArrayCopy(); // This is a render array, with special simple cases already handled. // Early return if this element was pre-rendered (no need to re-render). diff --git a/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php b/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php new file mode 100644 index 0000000..42db8b9 --- /dev/null +++ b/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php @@ -0,0 +1,41 @@ + 'twig_theme_test_read_only', + '#renderable' => [ + '#markup' => 'Original', + ], + ]; + + // To modify a ReadOnlyArrayObject, Twig will convert it to an array and + // attempt to use that for rendering. This should throw an error. + try { + $result = \Drupal::service('renderer')->renderRoot($filter_test); + } + catch (\Exception $e) {} + $this->assertFalse(isset($result), 'Variables cannot be modified.'); + } + +} diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig new file mode 100644 index 0000000..4bfeec8 --- /dev/null +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig @@ -0,0 +1,2 @@ +{% set renderable = renderable|merge({'#markup': 'Modified'}) %} +{{ renderable }} diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module index a8b4086..337dca6 100644 --- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module +++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module @@ -73,6 +73,12 @@ function twig_theme_test_theme($existing, $type, $theme, $path) { ], 'template' => 'twig_theme_test.renderable', ]; + $items['twig_theme_test_read_only'] = [ + 'variables' => [ + 'renderable' => NULL, + ], + 'template' => 'twig_theme_test.read_only', + ]; return $items; } diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 791f908..760d366 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -52,7 +52,7 @@ function twig_init(Extension $theme) { * The output generated by the template, plus any debug information. */ function twig_render_template($template_file, array $variables) { - /** @var \Twig_Environment $twig_service */ + /** @var \Drupal\Core\Template\TwigEnvironment $twig_service */ $twig_service = \Drupal::service('twig'); $output = [ 'debug_prefix' => '', @@ -61,7 +61,9 @@ function twig_render_template($template_file, array $variables) { 'debug_suffix' => '', ]; try { + $twig_service->wrapContext($variables); $output['rendered_markup'] = $twig_service->loadTemplate($template_file)->render($variables); + $twig_service->unWrapContext($variables); } catch (\Twig_Error_Runtime $e) { // In case there is a previous exception, re-throw the previous exception,