Skip to content

Commit

Permalink
Rebase
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Nov 22, 2023
1 parent aeeec9a commit cfa573b
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 2 deletions.
70 changes: 70 additions & 0 deletions src/Environment.php
Expand Up @@ -60,6 +60,8 @@ class Environment
private $resolvedGlobals;
private $loadedTemplates;
private $strictVariables;
private $arrayMethods;
private $strictProperties;
private $templateClassPrefix = '__TwigTemplate_';
private $originalCache;
private $extensionSet;
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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();

Expand Down Expand Up @@ -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.
*
Expand Down
22 changes: 20 additions & 2 deletions src/Extension/CoreExtension.php
Expand Up @@ -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)))
Expand All @@ -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)) {
Expand Down Expand Up @@ -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 = [];
Expand Down
61 changes: 61 additions & 0 deletions tests/ErrorTest.php
Expand Up @@ -12,6 +12,7 @@
*/

use PHPUnit\Framework\TestCase;
use stdClass;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Error\RuntimeError;
Expand Down Expand Up @@ -58,6 +59,66 @@ public function testTwigExceptionGuessWithMissingVarAndArrayLoader()
}
}

public function testTwigExceptionGuessWithMissingPropAndArrayLoader()
{
$loader = new ArrayLoader([
'base.html' => '{% block content %}{% endblock %}',
'index.html' => <<<EOHTML
{% extends 'base.html' %}
{% block content %}
{{ foo.bar }}
{% endblock %}
{% block foo %}
{{ foo.bar }}
{% endblock %}
EOHTML
]);

$twig = new Environment($loader, ['strict_properties' => 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' => <<<EOHTML
{% extends 'base.html' %}
{% block content %}
{{ foo.bar }}
{% endblock %}
{% block foo %}
{{ foo.bar }}
{% endblock %}
EOHTML
]);

$twig = new Environment($loader, ['strict_properties' => 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([
Expand Down
50 changes: 50 additions & 0 deletions tests/TemplateTest.php
Expand Up @@ -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;
Expand All @@ -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' => <<<EOHTML
{% extends 'base.html' %}
{% block content %}
{{ foo.bar }}
{% endblock %}
{% block foo %}
{{ foo.bar }}
{% endblock %}
EOHTML
]);

$twig = new Environment($loader, ['strict_properties' => 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' => <<<EOHTML
{% extends 'base.html' %}
{% block content %}
{{ foo.bar() }}
{% endblock %}
{% block foo %}
{{ foo.bar() }}
{% endblock %}
EOHTML
]);

$twig = new Environment($loader, ['array_methods' => true, 'debug' => true, 'cache' => false]);

$template = $twig->load('index.html');
$this->assertEquals('ok', trim($template->render(['foo' => [
'bar' => function () { return 'ok'; }
]])));
}

/**
* @dataProvider getAttributeExceptions
*/
Expand Down

0 comments on commit cfa573b

Please sign in to comment.