diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php index 7a7b8ea6581c20d2bd17967ad86abea3fe6372b4..15d522341d753119b0d856c9f1f76fbb0039e70a 100644 --- a/core/lib/Drupal/Component/Utility/SafeMarkup.php +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php @@ -25,7 +25,7 @@ * @link theme_render theme and render systems @endlink so that the output can * can be themed, escaped, and altered properly. * - * @see twig_drupal_escape_filter() + * @see TwigExtension::escapeFilter() * @see twig_render_template() * @see sanitization * @see theme_render diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index 8118105ebfc38eaac9cfb706a72ef23ee524393d..92257120c7c3a9d39bf1b4ae4b476e1e21210feb 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -44,7 +44,7 @@ public function __construct($root, \Twig_LoaderInterface $loader = NULL, $option $this->cache_object = \Drupal::cache(); // Ensure that twig.engine is loaded, given that it is needed to render a - // template because functions like twig_drupal_escape_filter are called. + // template because functions like TwigExtension::escapeFilter() are called. require_once $root . '/core/themes/engines/twig/twig.engine'; $this->templateClasses = array(); diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 44b0a2ab554c1d69d76ccf8646ea8d3e15858dd4..bb29cd172e6d6b93d7a9ffbc7f88edc0cefa0d78 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -12,6 +12,7 @@ namespace Drupal\Core\Template; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Url; @@ -89,7 +90,7 @@ public function setLinkGenerator(LinkGeneratorInterface $link_generator) { public function getFunctions() { return array( // This function will receive a renderable array, if an array is detected. - new \Twig_SimpleFunction('render_var', 'twig_render_var'), + new \Twig_SimpleFunction('render_var', array($this, 'renderVar')), // The url and path function are defined in close parallel to those found // in \Symfony\Bridge\Twig\Extension\RoutingExtension new \Twig_SimpleFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), @@ -118,7 +119,7 @@ public function getFilters() { new \Twig_SimpleFilter('placeholder', 'twig_raw_filter', array('is_safe' => array('html'))), // Replace twig's escape filter with our own. - new \Twig_SimpleFilter('drupal_escape', 'twig_drupal_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), + new \Twig_SimpleFilter('drupal_escape', [$this, 'escapeFilter'], array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')), // Implements safe joining. // @todo Make that the default for |join? Upstream issue: @@ -132,7 +133,7 @@ public function getFilters() { new \Twig_SimpleFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'), new \Twig_SimpleFilter('clean_id', '\Drupal\Component\Utility\Html::getId'), // This filter will render a renderable array to use the string results. - new \Twig_SimpleFilter('render', 'twig_render_var'), + new \Twig_SimpleFilter('render', array($this, 'renderVar')), ); } @@ -141,7 +142,7 @@ public function getFilters() { */ public function getNodeVisitors() { // The node visitor is needed to wrap all variables with - // render_var -> twig_render_var() function. + // render_var -> TwigExtension->renderVar() function. return array( new TwigNodeVisitor(), ); @@ -298,4 +299,148 @@ public function attachLibrary($library) { $this->renderer->render($template_attached); } + /** + * Overrides twig_escape_filter(). + * + * Replacement function for Twig's escape filter. + * + * @param \Twig_Environment $env + * A Twig_Environment instance. + * @param mixed $arg + * The value to be escaped. + * @param string $strategy + * The escaping strategy. Defaults to 'html'. + * @param string $charset + * The charset. + * @param bool $autoescape + * Whether the function is called by the auto-escaping feature (TRUE) or by + * the developer (FALSE). + * + * @return string|null + * The escaped, rendered output, or NULL if there is no valid output. + */ + public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $charset = NULL, $autoescape = FALSE) { + // Check for a numeric zero int or float. + if ($arg === 0 || $arg === 0.0) { + return 0; + } + + // Return early for NULL and empty arrays. + if ($arg == NULL) { + return NULL; + } + + // Keep Twig_Markup objects intact to support autoescaping. + if ($autoescape && $arg instanceOf \Twig_Markup) { + return $arg; + } + + $return = NULL; + + if (is_scalar($arg)) { + $return = (string) $arg; + } + elseif (is_object($arg)) { + if (method_exists($arg, '__toString')) { + $return = (string) $arg; + } + // You can't throw exceptions in the magic PHP __toString methods, see + // http://php.net/manual/en/language.oop5.magic.php#object.tostring so + // we also support a toString method. + elseif (method_exists($arg, 'toString')) { + $return = $arg->toString(); + } + else { + throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg)))); + } + } + + // We have a string or an object converted to a string: Autoescape it! + if (isset($return)) { + if ($autoescape && 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); + } + return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); + } + + // This is a normal render array, which is safe by definition, with + // special simple cases already handled. + + // 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) { + return $arg['#markup']; + } + $arg['#printed'] = FALSE; + return $this->renderer->render($arg); + } + + /** + * Wrapper around render() for twig printed output. + * + * If an object is passed that has no __toString method an exception is thrown; + * other objects are casted to string. However in the case that the object is an + * instance of a Twig_Markup object it is returned directly to support auto + * escaping. + * + * If an array is passed it is rendered via render() and scalar values are + * returned directly. + * + * @param mixed $arg + * String, Object or Render Array. + * + * @return mixed + * The rendered output or an Twig_Markup object. + * + * @see render + * @see TwigNodeVisitor + */ + public function renderVar($arg) { + // Check for a numeric zero int or float. + if ($arg === 0 || $arg === 0.0) { + return 0; + } + + // Return early for NULL and empty arrays. + if ($arg == NULL) { + return NULL; + } + + // Optimize for strings as it is likely they come from the escape filter. + if (is_string($arg)) { + return $arg; + } + + if (is_scalar($arg)) { + return $arg; + } + + if (is_object($arg)) { + if (method_exists($arg, '__toString')) { + return (string) $arg; + } + // You can't throw exceptions in the magic PHP __toString methods, see + // http://php.net/manual/en/language.oop5.magic.php#object.tostring so + // we also support a toString method. + elseif (method_exists($arg, 'toString')) { + return $arg->toString(); + } + else { + throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg)))); + } + } + + // This is a render array, with special simple cases already handled. + // 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) { + return $arg['#markup']; + } + $arg['#printed'] = FALSE; + return $this->renderer->render($arg); + } + } diff --git a/core/lib/Drupal/Core/Template/TwigNodeTrans.php b/core/lib/Drupal/Core/Template/TwigNodeTrans.php index c4fd4efce02f3d83b0ccca3c734e1cec9d4cfe2f..1bacb9ec68e76ba2dd3db879b57c3fc860ab7448 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeTrans.php +++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php @@ -136,7 +136,7 @@ protected function compileString(\Twig_NodeInterface $body) { $args = $n; - // Support twig_render_var function in chain. + // Support TwigExtension->renderVar() function in chain. if ($args instanceof \Twig_Node_Expression_Function) { $args = $n->getNode('arguments')->getNode(0); } diff --git a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php index 4915c16a350c44f8825ceb983ced6d5be3d065aa..a08ec2d47ae8fb4b748041bfe60f96d934ff5d17 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php +++ b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php @@ -11,7 +11,7 @@ * Provides a Twig_NodeVisitor to change the generated parse-tree. * * This is used to ensure that everything printed is wrapped via the - * twig_render_var() function in order to just write {{ content }} + * TwigExtension->renderVar() function in order to just write {{ content }} * in templates instead of having to write {{ render_var(content) }}. * * @see twig_render @@ -29,7 +29,7 @@ function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) { * {@inheritdoc} */ function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) { - // We use this to inject a call to render_var -> twig_render_var() + // We use this to inject a call to render_var -> TwigExtension->renderVar() // before anything is printed. if ($node instanceof \Twig_Node_Print) { if (!empty($this->skipRenderVarFunction)) { diff --git a/core/modules/system/src/Tests/Theme/TwigExtensionTest.php b/core/modules/system/src/Tests/Theme/TwigExtensionTest.php index 6719ab72b36b0f0e7b6e07d13670fc8055539772..7e25ae501620b2368f233af5c5a816357d256b53 100644 --- a/core/modules/system/src/Tests/Theme/TwigExtensionTest.php +++ b/core/modules/system/src/Tests/Theme/TwigExtensionTest.php @@ -63,4 +63,30 @@ function testTwigExtensionFunction() { $this->assertNoText('The Quick Brown Fox Jumps Over The Lazy Dog 123.', 'Success: No text left behind.'); } + /** + * Tests output of integer and double 0 values of TwigExtension::escapeFilter(). + * + * @see https://www.drupal.org/node/2417733 + */ + public function testsRenderEscapedZeroValue() { + /** @var \Drupal\Core\Template\TwigExtension $extension */ + $extension = \Drupal::service('twig.extension'); + /** @var \Drupal\Core\Template\TwigEnvironment $twig */ + $twig = \Drupal::service('twig'); + $this->assertIdentical($extension->escapeFilter($twig, 0), 0, 'TwigExtension::escapeFilter() returns zero correctly when provided as an integer.'); + $this->assertIdentical($extension->escapeFilter($twig, 0.0), 0, 'TwigExtension::escapeFilter() returns zero correctly when provided as a double.'); + } + + /** + * Tests output of integer and double 0 values of TwigExtension->renderVar(). + * + * @see https://www.drupal.org/node/2417733 + */ + public function testsRenderZeroValue() { + /** @var \Drupal\Core\Template\TwigExtension $extension */ + $extension = \Drupal::service('twig.extension'); + $this->assertIdentical($extension->renderVar(0), 0, 'TwigExtension::renderVar() renders zero correctly when provided as an integer.'); + $this->assertIdentical($extension->renderVar(0.0), 0, 'TwigExtension::renderVar() renders zero correctly when provided as a double.'); + } + } diff --git a/core/tests/Drupal/Tests/Core/Theme/TwigEngineTest.php b/core/tests/Drupal/Tests/Core/Theme/TwigEngineTest.php deleted file mode 100644 index baf45c1a4510ba3e498b228c95aafa28d3e6fcbe..0000000000000000000000000000000000000000 --- a/core/tests/Drupal/Tests/Core/Theme/TwigEngineTest.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Tests\Core\Theme\TwigEngineTest. - */ - -namespace Drupal\Tests\Core\Theme; - -use Drupal\Tests\UnitTestCase; - -/** - * Test coverage for the file core/themes/engines/twig/twig.engine. - * - * @group Theme - */ -class TwigEngineTest extends UnitTestCase { - - /** - * The mocked Twig environment. - * - * @var \Twig_Environment|\PHPUnit_Framework_MockObject_MockObject - */ - protected $twigEnvironment; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Ensure that twig.engine is loaded, it is needed to access - // twig_drupal_escape_filter(). - require_once $this->root . '/core/themes/engines/twig/twig.engine'; - - $this->twigEnvironment = $this->getMock('\Twig_Environment'); - } - - /** - * Tests output of integer and double 0 values of twig_render_var(). - * - * @see https://www.drupal.org/node/2417733 - */ - public function testsRenderZeroValue() { - $this->assertSame(twig_render_var(0), 0, 'twig_render_var() renders zero correctly when provided as an integer.'); - $this->assertSame(twig_render_var(0.0), 0, 'twig_render_var() renders zero correctly when provided as a double.'); - } - - /** - * Tests output of integer and double 0 values of twig_drupal_escape_filter(). - * - * @see https://www.drupal.org/node/2417733 - */ - public function testsRenderEscapedZeroValue() { - $this->assertSame(twig_drupal_escape_filter($this->twigEnvironment, 0), 0, 'twig_escape_filter() returns zero correctly when provided as an integer.'); - $this->assertSame(twig_drupal_escape_filter($this->twigEnvironment, 0.0), 0, 'twig_escape_filter() returns zero correctly when provided as a double.'); - } - -} diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index 324203e225a2d4be4a469e3567f08afd19170a53..9f8bc4a044bf0d1f180de6a580b499b69e5afbce 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -106,65 +106,6 @@ function twig_render_template($template_file, array $variables) { return SafeMarkup::set(implode('', $output)); } -/** - * Wrapper around render() for twig printed output. - * - * If an object is passed that has no __toString method an exception is thrown; - * other objects are casted to string. However in the case that the object is an - * instance of a Twig_Markup object it is returned directly to support auto - * escaping. - * - * If an array is passed it is rendered via render() and scalar values are - * returned directly. - * - * @param mixed $arg - * String, Object or Render Array. - * - * @return mixed - * The rendered output or an Twig_Markup object. - * - * @see render - * @see TwigNodeVisitor - */ -function twig_render_var($arg) { - // Check for a numeric zero int or float. - if ($arg === 0 || $arg === 0.0) { - return 0; - } - - // Return early for NULL and also true for empty arrays. - if ($arg == NULL) { - return NULL; - } - - // Optimize for strings as it is likely they come from the escape filter. - if (is_string($arg)) { - return $arg; - } - - if (is_scalar($arg)) { - return $arg; - } - - if (is_object($arg)) { - if (method_exists($arg, '__toString')) { - return (string) $arg; - } - // You can't throw exceptions in the magic PHP __toString methods, see - // http://php.net/manual/en/language.oop5.magic.php#object.tostring so - // we also support a toString method. - elseif (method_exists($arg, 'toString')) { - return $arg->toString(); - } - else { - throw new Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg)))); - } - } - - // This is a normal render array. - return render($arg); -} - /** * Removes child elements from a copy of the original array. * @@ -198,79 +139,6 @@ function twig_without($element) { return $filtered_element; } -/** - * Overrides twig_escape_filter(). - * - * Replacement function for Twig's escape filter. - * - * @param \Twig_Environment $env - * A Twig_Environment instance. - * @param string $string - * The value to be escaped. - * @param string $strategy - * The escaping strategy. Defaults to 'html'. - * @param string $charset - * The charset. - * @param bool $autoescape - * Whether the function is called by the auto-escaping feature (TRUE) or by - * the developer (FALSE). - * - * @return string|null - * The escaped, rendered output, or NULL if there is no valid output. - */ -function twig_drupal_escape_filter(\Twig_Environment $env, $string, $strategy = 'html', $charset = NULL, $autoescape = FALSE) { - // Check for a numeric zero int or float. - if ($string === 0 || $string === 0.0) { - return 0; - } - - // Return early for NULL or an empty array. - if ($string == NULL) { - return NULL; - } - - // Keep Twig_Markup objects intact to support autoescaping. - if ($autoescape && $string instanceOf \Twig_Markup) { - return $string; - } - - $return = NULL; - - if (is_scalar($string)) { - $return = (string) $string; - } - elseif (is_object($string)) { - if (method_exists($string, '__toString')) { - $return = (string) $string; - } - // You can't throw exceptions in the magic PHP __toString methods, see - // http://php.net/manual/en/language.oop5.magic.php#object.tostring so - // we also support a toString method. - elseif (method_exists($string, 'toString')) { - $return = $string->toString(); - } - else { - throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($string)))); - } - } - - // We have a string or an object converted to a string: Autoescape it! - if (isset($return)) { - if ($autoescape && 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); - } - return twig_escape_filter($env, $return, $strategy, $charset, $autoescape); - } - - // This is a normal render array, which is safe by definition. - return render($string); -} - /** * Overrides twig_join_filter(). *