From cfa573bdae533b86a896080e2b36b755900aa370 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 22 Nov 2023 14:21:09 +0100 Subject: [PATCH] Rebase --- src/Environment.php | 70 +++++++++++++++++++++++++++++++++ src/Extension/CoreExtension.php | 22 ++++++++++- tests/ErrorTest.php | 61 ++++++++++++++++++++++++++++ tests/TemplateTest.php | 50 +++++++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 5329b143e..a46709891 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -60,6 +60,8 @@ class Environment private $resolvedGlobals; private $loadedTemplates; private $strictVariables; + private $arrayMethods; + private $strictProperties; private $templateClassPrefix = '__TwigTemplate_'; private $originalCache; private $extensionSet; @@ -88,6 +90,13 @@ class Environment * * strict_variables: Whether to ignore invalid variables in templates * (default to false). * + * * strict_properties: Whether to treat property accesses in templates only as property accesses, + * without ever invoking methods with the same name if the property does not exist + * (default to false). + * + * * array_methods: Whether to treat method calls on callable array elements as if they were object method calls + * (defaults to false). + * * * autoescape: Whether to enable auto-escaping (default to html): * * false: disable auto-escaping * * html, js: set the autoescaping to one of the supported strategies @@ -106,6 +115,8 @@ public function __construct(LoaderInterface $loader, $options = []) 'debug' => false, 'charset' => 'UTF-8', 'strict_variables' => false, + 'array_methods' => false, + 'strict_properties' => false, 'autoescape' => 'html', 'cache' => false, 'auto_reload' => null, @@ -116,6 +127,8 @@ public function __construct(LoaderInterface $loader, $options = []) $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; $this->strictVariables = (bool) $options['strict_variables']; + $this->arrayMethods = (bool) $options['array_methods']; + $this->strictProperties = (bool) $options['strict_properties']; $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); @@ -206,6 +219,63 @@ public function isStrictVariables() return $this->strictVariables; } + + /** + * Enables the strict_properties option. + */ + public function enableStrictProperties() + { + $this->strictProperties = true; + $this->updateOptionsHash(); + } + + /** + * Disables the strict_properties option. + */ + public function disableStrictProperties() + { + $this->strictProperties = false; + $this->updateOptionsHash(); + } + + /** + * Checks if the strict_properties option is enabled. + * + * @return bool true if strict_properties is enabled, false otherwise + */ + public function isStrictProperties() + { + return $this->strictProperties; + } + + /** + * Enables the array_methods option. + */ + public function enableArrayMethods() + { + $this->arrayMethods = true; + $this->updateOptionsHash(); + } + + /** + * Disables the array_methods option. + */ + public function disableArrayMethods() + { + $this->arrayMethods = false; + $this->updateOptionsHash(); + } + + /** + * Checks if the array_methods option is enabled. + * + * @return bool true if array_methods is enabled, false otherwise + */ + public function isArrayMethods() + { + return $this->arrayMethods; + } + /** * Gets the current cache implementation. * diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 36aa8f10a..267ba0e3d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1469,7 +1469,7 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (/* Template::METHOD_CALL */ 'method' !== $type || $env->isArrayMethods()) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) @@ -1479,7 +1479,13 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return true; } - return $object[$arrayItem]; + if ($type === 'method') { + if (is_callable($object[$arrayItem])) { + return $object[$arrayItem](...$arguments); + } + } else { + return $object[$arrayItem]; + } } if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { @@ -1554,6 +1560,18 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return $object->$item; } + + if ($env->isStrictProperties()) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(sprintf('The property "%1$s" does not exist in class "%2$s".', $item, get_class($object)), $lineno, $source); + } } static $cache = []; diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index db6418ed6..0f38d37ea 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -12,6 +12,7 @@ */ use PHPUnit\Framework\TestCase; +use stdClass; use Twig\Environment; use Twig\Error\Error; use Twig\Error\RuntimeError; @@ -58,6 +59,66 @@ public function testTwigExceptionGuessWithMissingVarAndArrayLoader() } } + public function testTwigExceptionGuessWithMissingPropAndArrayLoader() + { + $loader = new ArrayLoader([ + 'base.html' => '{% block content %}{% endblock %}', + 'index.html' => << true, 'strict_variables' => true, 'debug' => true, 'cache' => false]); + + $template = $twig->load('index.html'); + try { + $template->render(['foo' => new stdClass]); + + $this->fail("No exception was thrown!"); + } catch (RuntimeError $e) { + $this->assertEquals('The property "bar" does not exist in class "stdClass" in "index.html" at line 3.', $e->getMessage()); + $this->assertEquals(3, $e->getTemplateLine()); + $this->assertEquals('index.html', $e->getSourceContext()->getName()); + } + } + + public function testTwigExceptionArrayFetchOnCallableWithArrayMethodsEnabled() + { + $loader = new ArrayLoader([ + 'base.html' => '{% block content %}{% endblock %}', + 'index.html' => << true, 'strict_variables' => true, 'debug' => true, 'cache' => false]); + + $template = $twig->load('index.html'); + try { + $template->render(['foo' => [ + 'bar' => function () { return 'ok'; } + ]]); + + $this->fail("No exception was thrown!"); + } catch (RuntimeError $e) { + $this->assertEquals('An exception has been thrown during the rendering of a template ("Object of class Closure could not be converted to string") in "index.html" at line 3.', $e->getMessage()); + $this->assertEquals(3, $e->getTemplateLine()); + $this->assertEquals('index.html', $e->getSourceContext()->getName()); + } + } + public function testTwigExceptionGuessWithExceptionAndArrayLoader() { $loader = new ArrayLoader([ diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 9c2364c1d..d7ccac9eb 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -11,6 +11,7 @@ * file that was distributed with this source code. */ +use AssertionError; use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; @@ -33,6 +34,55 @@ public function testDisplayBlocksAcceptTemplateOnlyAsBlocks() $template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } + public function testTwigStrictPropertiesMissingPropAndMethodWithSameName() + { + $loader = new ArrayLoader([ + 'base.html' => '{% block content %}{% endblock %}', + 'index.html' => << true, 'debug' => true, 'cache' => false]); + + $template = $twig->load('index.html'); + + $this->assertEquals('', trim($template->render(['foo' => new class { + public function bar(): void { + throw new AssertionError("Impossible!"); + } + }]))); + } + + public function testTwigArrayMethods() + { + $loader = new ArrayLoader([ + 'base.html' => '{% block content %}{% endblock %}', + 'index.html' => << true, 'debug' => true, 'cache' => false]); + + $template = $twig->load('index.html'); + $this->assertEquals('ok', trim($template->render(['foo' => [ + 'bar' => function () { return 'ok'; } + ]]))); + } + /** * @dataProvider getAttributeExceptions */