mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-30 12:06:56 +00:00
f640320202
* 3.x: (26 commits) Remove the documentation comments compilation overhead Clarify source function trust requirements Throw on PCRE errors in the matches operator Document that reusing a non-rewindable iterator after destructuring is unsupported Release destructuring temporaries after assignment Deprecate prefixed macro definedness checks Fix duplicate macro deprecation wording Throw when list formatting fails Document that sequence destructuring consumes one value per pattern slot Fix the html_attr documentation about iterables in data attributes Warn about untrusted input with the default Tempest markdown converter Document that overriding MacroNode::compile() is not supported anymore Merge overlapping CHANGELOG entries for the destructuring fatal error fix Document that include_only keeps global variables available Remove lazy macro import resolution Honor date formatter prototype calendars Fix Stringable keys for ArrayAccess implementations Fix repeated object destructuring evaluation Restore void return type compatibility for extension points Reject destructuring patterns containing no variables ... # Conflicts: # CHANGELOG # doc/deprecated.rst # doc/filters/format_datetime.rst # extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php # extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php # extra/twig-extra-bundle/TwigExtraBundle.php # src/MacroNamespace.php # src/Node/MacrosNode.php # src/Parser.php # src/Test/IntegrationTestCase.php # src/Test/NodeTestCase.php # tests/CallMacroTest.php # tests/ExpressionParserTest.php # tests/Fixtures/macros/duplicate_definition.legacy.test # tests/Node/MacrosTest.php # tests/ParserTest.php
1070 lines
36 KiB
PHP
1070 lines
36 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of Twig.
|
|
*
|
|
* (c) Fabien Potencier
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Twig\Tests;
|
|
|
|
/*
|
|
* This file is part of Twig.
|
|
*
|
|
* (c) Fabien Potencier
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Twig\Environment;
|
|
use Twig\Error\RuntimeError;
|
|
use Twig\Extension\CoreExtension;
|
|
use Twig\Extension\SandboxExtension;
|
|
use Twig\Loader\ArrayLoader;
|
|
use Twig\Sandbox\SecurityError;
|
|
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
|
use Twig\Sandbox\SecurityPolicy;
|
|
use Twig\Source;
|
|
use Twig\Template;
|
|
use Twig\TemplateWrapper;
|
|
|
|
class TemplateTest extends TestCase
|
|
{
|
|
public function testDisplayBlocksAcceptTemplateOnlyAsBlocks(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->expectException(\LogicException::class);
|
|
$template->renderBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]);
|
|
}
|
|
|
|
#[DataProvider('getAttributeExceptions')]
|
|
public function testGetAttributeExceptions($template, $message): void
|
|
{
|
|
$templates = ['index' => $template];
|
|
$env = new Environment(new ArrayLoader($templates), ['strict_variables' => true]);
|
|
$template = $env->load('index');
|
|
|
|
$context = [
|
|
'string' => 'foo',
|
|
'null' => null,
|
|
'empty_array' => [],
|
|
'array' => ['foo' => 'foo'],
|
|
'array_access' => new TemplateArrayAccessObject(),
|
|
'magic_exception' => new TemplateMagicPropertyObjectWithException(),
|
|
'object' => new \stdClass(),
|
|
];
|
|
|
|
try {
|
|
$template->render($context);
|
|
$this->fail('Accessing an invalid attribute should throw an exception.');
|
|
} catch (RuntimeError $e) {
|
|
$this->assertSame(\sprintf($message, 'index'), $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public static function getAttributeExceptions()
|
|
{
|
|
return [
|
|
['{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ empty_array["a"] }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'],
|
|
['{{ array["a"] }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
|
|
['{{ array_access["a"] }}', 'Key "a" does not exist in ArrayAccess-able object of class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
|
|
['{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'],
|
|
['{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1.'],
|
|
['{{ array.a() }}', 'Impossible to invoke a method ("a") on a sequence/mapping in "%s" at line 1.'],
|
|
['{{ empty_array.a }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'],
|
|
['{{ array.a }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
|
|
['{{ array.(-10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'],
|
|
['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()", "isa()", "hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
|
|
['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()", "ismissing_method()", "hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'],
|
|
['{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.'],
|
|
['{{ object["a"] }}', 'Impossible to access a key "a" on an object of class "stdClass" that does not implement ArrayAccess interface in "%s" at line 1.'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('getGetAttributeWithSandbox')]
|
|
public function testGetAttributeWithSandbox($object, $item, $allowed): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$policy = new SecurityPolicy([], [], [/* method */], [/* prop */], []);
|
|
$twig->addExtension(new SandboxExtension($policy, !$allowed));
|
|
$template = new TemplateForTest($twig);
|
|
|
|
try {
|
|
CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true);
|
|
|
|
if (!$allowed) {
|
|
$this->fail();
|
|
} else {
|
|
$this->addToAssertionCount(1);
|
|
}
|
|
} catch (SecurityError $e) {
|
|
if ($allowed) {
|
|
$this->fail();
|
|
} else {
|
|
$this->addToAssertionCount(1);
|
|
}
|
|
|
|
$this->assertStringContainsString('is not allowed', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public static function getGetAttributeWithSandbox()
|
|
{
|
|
return [
|
|
[new TemplatePropertyObject(), 'defined', false],
|
|
[new TemplatePropertyObject(), 'defined', true],
|
|
[new TemplateMethodObject(), 'defined', false],
|
|
[new TemplateMethodObject(), 'defined', true],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('getRenderTemplateWithoutOutputData')]
|
|
public function testRenderTemplateWithoutOutput(string $template): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => $template]));
|
|
$this->assertSame('', $twig->render('index'));
|
|
}
|
|
|
|
public static function getRenderTemplateWithoutOutputData()
|
|
{
|
|
return [
|
|
[''],
|
|
['{% for var in [] %}{% endfor %}'],
|
|
['{% if false %}{% endif %}'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('getNullCoalesceWithImportedMacroData')]
|
|
public function testNullCoalesceWithImportedMacro(array $templates, string $expected): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader($templates));
|
|
|
|
$this->assertSame($expected, trim($twig->render('index.twig')));
|
|
}
|
|
|
|
public static function getNullCoalesceWithImportedMacroData(): array
|
|
{
|
|
return [
|
|
'from import' => [
|
|
[
|
|
'index.twig' => '{% from "helper.twig" import foo %}{{ foo("bar") ?? "" }}',
|
|
'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}',
|
|
],
|
|
'bar',
|
|
],
|
|
'from import with undefined macro falls back' => [
|
|
[
|
|
'index.twig' => '{% from "helper.twig" import foo, nonexistent %}{{ nonexistent("bar") ?? "fallback" }}',
|
|
'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}',
|
|
],
|
|
'fallback',
|
|
],
|
|
'from import used multiple times' => [
|
|
[
|
|
'index.twig' => '{% from "helper.twig" import foo %}{{ foo("a") ?? "" }}-{{ foo("b") ?? "" }}',
|
|
'helper.twig' => '{% macro foo(param) %}{{ param }}{% endmacro %}',
|
|
],
|
|
'a-b',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testRenderBlockWithUndefinedBlock(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig, 'index.twig');
|
|
|
|
$this->expectException(RuntimeError::class);
|
|
$this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".');
|
|
|
|
$template->renderBlock('unknown', []);
|
|
}
|
|
|
|
public function testRenderBlockWithUndefinedParentBlock(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig, 'parent.twig');
|
|
|
|
$this->expectException(RuntimeError::class);
|
|
$this->expectExceptionMessage('Block "foo" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig"');
|
|
$template->renderBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false);
|
|
}
|
|
|
|
public function testGetAttributeOnArrayWithConfusableKey(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$array = ['Zero', 'One', -1 => 'MinusOne', '' => 'EmptyString', '1.5' => 'FloatButString', '01' => 'IntegerButStringWithLeadingZeros'];
|
|
|
|
$this->assertSame('Zero', $array[false]);
|
|
$this->assertSame('One', $array[true]);
|
|
$this->assertSame('One', $array['1']);
|
|
$this->assertSame('FloatButString', $array['1.5']);
|
|
$this->assertSame('IntegerButStringWithLeadingZeros', $array['01']);
|
|
$this->assertSame('EmptyString', $array['']);
|
|
|
|
$this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing a sequence/mapping (equals PHP behavior)');
|
|
$this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)');
|
|
}
|
|
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testArrayWithStringableKeyIsConsistentAcrossStrictModes(bool $strict): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
|
|
$key = new TemplateStringableKey();
|
|
|
|
$this->assertSame('value', $twig->render('index', ['array' => ['string' => 'value'], 'object' => $key]));
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStringableKeyArrayAccessContainers
|
|
*/
|
|
#[DataProvider('getStringableKeyArrayAccessContainers')]
|
|
public function testStringableKeyIsCoercedForInternalArrayAccess(bool $strict, bool $sandboxed, \ArrayAccess $data): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
$key = new TemplateStringableKey();
|
|
if ($sandboxed) {
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true));
|
|
}
|
|
|
|
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
|
|
$this->assertSame(1, $key->toStringCalls);
|
|
}
|
|
|
|
public static function getStringableKeyArrayAccessContainers(): iterable
|
|
{
|
|
foreach (['lax' => false, 'strict' => true] as $mode => $strict) {
|
|
foreach (['unsandboxed' => false, 'sandboxed' => true] as $sandboxMode => $sandboxed) {
|
|
yield $mode.' '.$sandboxMode.' ArrayObject' => [$strict, $sandboxed, new \ArrayObject(['string' => 'value'])];
|
|
yield $mode.' '.$sandboxMode.' ArrayIterator' => [$strict, $sandboxed, new \ArrayIterator(['string' => 'value'])];
|
|
yield $mode.' '.$sandboxMode.' RecursiveArrayIterator' => [$strict, $sandboxed, new \RecursiveArrayIterator(['string' => 'value'])];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testArrayAccessWithObjectKeyKeepsTheObjectKey(bool $strict): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
|
|
$key = new TemplateStringableKey();
|
|
$data = new \SplObjectStorage();
|
|
$data[$key] = 'value';
|
|
|
|
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testArrayAccessLookupDoesNotRepeatOffsetChecks(bool $strict): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
|
|
$key = new TemplateStringableKey();
|
|
$data = new TemplateTrackingArrayAccess($key, false);
|
|
|
|
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
|
|
$this->assertSame([$key], $data->offsetExistsCalls);
|
|
$this->assertSame([$key], $data->offsetGetCalls);
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
}
|
|
|
|
public function testArrayAccessDefinedTestDoesNotReadTheOffset(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] is defined ? "yes" : "no" }}']));
|
|
$key = new TemplateStringableKey();
|
|
$data = new TemplateTrackingArrayAccess($key, false);
|
|
|
|
$this->assertSame('yes', $twig->render('index', ['data' => $data, 'key' => $key]));
|
|
$this->assertSame([$key], $data->offsetExistsCalls);
|
|
$this->assertSame([], $data->offsetGetCalls);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testRejectedStringableArrayAccessKeyRethrowsOriginalTypeError(bool $strict): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
$key = new TemplateStringableKey();
|
|
$data = new TemplateTrackingArrayAccess($key, true);
|
|
|
|
try {
|
|
$twig->render('index', ['data' => $data, 'key' => $key]);
|
|
$this->fail('The original TypeError must be rethrown.');
|
|
} catch (RuntimeError $e) {
|
|
$this->assertSame($data->offsetExistsError, $e->getPrevious());
|
|
}
|
|
|
|
$this->assertSame([$key], $data->offsetExistsCalls);
|
|
$this->assertSame([], $data->offsetGetCalls);
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testSandboxDoesNotAuthorizeStringPropertyForArrayAccessObjectKey(bool $strict): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), [
|
|
'strict_variables' => $strict,
|
|
'autoescape' => false,
|
|
]);
|
|
$key = new TemplateStringableKey();
|
|
$data = new TemplateTrackingArrayAccess($key, false);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [$data::class => ['string']], []), true));
|
|
|
|
$this->expectException(SecurityNotAllowedPropertyError::class);
|
|
|
|
try {
|
|
$twig->render('index', ['data' => $data, 'key' => $key]);
|
|
} finally {
|
|
$this->assertSame([], $data->offsetExistsCalls);
|
|
$this->assertSame([], $data->offsetGetCalls);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testArrayWithStringableKeyIsCheckedBySandbox(bool $strict): void
|
|
{
|
|
$key = new TemplateStringableKey();
|
|
$context = ['array' => ['string' => 'value'], 'key' => $key];
|
|
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true));
|
|
|
|
try {
|
|
$twig->render('index', $context);
|
|
$this->fail('The sandbox must reject the __toString() coercion of the array key.');
|
|
} catch (SecurityError $e) {
|
|
$this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage());
|
|
}
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ array[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true));
|
|
|
|
$this->assertSame('value', $twig->render('index', $context));
|
|
$this->assertSame(1, $key->toStringCalls);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider getStrictVariablesModes
|
|
*/
|
|
#[DataProvider('getStrictVariablesModes')]
|
|
public function testInternalArrayAccessWithStringableKeyIsCheckedBySandbox(bool $strict): void
|
|
{
|
|
$key = new TemplateStringableKey();
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), ['strict_variables' => $strict, 'autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true));
|
|
|
|
try {
|
|
$twig->render('index', ['data' => new \ArrayObject(['string' => 'value']), 'key' => $key]);
|
|
$this->fail('The sandbox must reject the __toString() coercion of the ArrayAccess key.');
|
|
} catch (SecurityError $e) {
|
|
$this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage());
|
|
}
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
}
|
|
|
|
public function testSandboxedArrayAccessWithObjectKeyKeepsTheObjectKey(): void
|
|
{
|
|
$key = new TemplateStringableKey();
|
|
$data = new \SplObjectStorage();
|
|
$data[$key] = 'value';
|
|
|
|
$twig = new Environment(new ArrayLoader(['index' => '{{ data[key] }}']), ['autoescape' => false]);
|
|
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [], []), true));
|
|
|
|
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
|
|
$this->assertSame(0, $key->toStringCalls);
|
|
}
|
|
|
|
public static function getStrictVariablesModes(): iterable
|
|
{
|
|
yield 'lax' => [false];
|
|
yield 'strict' => [true];
|
|
}
|
|
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttribute($defined, $value, $object, $item, $arguments, $type): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
}
|
|
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(), ['strict_variables' => true]);
|
|
$template = new TemplateForTest($twig);
|
|
|
|
if ($defined) {
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
} else {
|
|
$this->expectException(RuntimeError::class);
|
|
if (null !== $exceptionMessage) {
|
|
$this->expectExceptionMessage($exceptionMessage);
|
|
}
|
|
$this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type));
|
|
}
|
|
}
|
|
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true));
|
|
}
|
|
|
|
#[DataProvider('getGetAttributeTests')]
|
|
public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(), ['strict_variables' => true]);
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true));
|
|
}
|
|
|
|
public function testGetAttributeCallExceptions(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
$template = new TemplateForTest($twig);
|
|
|
|
$object = new TemplateMagicMethodExceptionObject();
|
|
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo'));
|
|
}
|
|
|
|
#[DataProvider('provideNonStringPrintValues')]
|
|
public function testPrintingANonStringReportsTheErrorAtThePrintLocation($value, string $expectedMessage): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader(['index' => "foo\n{{ value }}\nbar"]));
|
|
|
|
set_error_handler(static function (int $type, string $msg, string $file, int $line): bool {
|
|
throw new \ErrorException($msg, 0, $type, $file, $line);
|
|
}, \E_WARNING);
|
|
|
|
try {
|
|
$twig->render('index', ['value' => $value]);
|
|
$this->fail('Printing a non-string value should fail.');
|
|
} catch (RuntimeError $e) {
|
|
$this->assertSame('index', $e->getSourceContext()->getName());
|
|
$this->assertSame(2, $e->getTemplateLine());
|
|
$this->assertSame($expectedMessage, $e->getPrevious()->getMessage());
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
}
|
|
|
|
public static function provideNonStringPrintValues(): iterable
|
|
{
|
|
yield 'array' => [['a', 'b'], 'Array to string conversion'];
|
|
yield 'non-Stringable object' => [new \stdClass(), 'Object of class stdClass could not be converted to string'];
|
|
}
|
|
|
|
public static function getGetAttributeTests()
|
|
{
|
|
$array = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
$objectArray = new TemplateArrayAccessObject();
|
|
$arrayObject = new \ArrayObject($array);
|
|
$stdObject = (object) $array;
|
|
$magicPropertyObject = new TemplateMagicPropertyObject();
|
|
$propertyObject = new TemplatePropertyObject();
|
|
$propertyObject1 = new TemplatePropertyObjectAndIterator();
|
|
$propertyObject2 = new TemplatePropertyObjectAndArrayAccess();
|
|
$propertyObject3 = new TemplatePropertyObjectDefinedWithUndefinedValue();
|
|
$methodObject = new TemplateMethodObject();
|
|
$magicMethodObject = new TemplateMagicMethodObject();
|
|
|
|
$anyType = Template::ANY_CALL;
|
|
$methodType = Template::METHOD_CALL;
|
|
$arrayType = Template::ARRAY_CALL;
|
|
|
|
$basicTests = [
|
|
// array(defined, value, property to fetch)
|
|
[true, 'defined', 'defined'],
|
|
[false, null, 'undefined'],
|
|
[false, null, 'protected'],
|
|
[true, 0, 'zero'],
|
|
[true, 1, 1],
|
|
[true, 1, 1.0],
|
|
[true, null, 'null'],
|
|
[true, true, 'bar'],
|
|
[true, true, 'foo'],
|
|
[true, 'baz', 'baz'],
|
|
[true, 'baf', 'baf'],
|
|
[true, '09', '09'],
|
|
[true, '+4', '+4'],
|
|
];
|
|
$testObjects = [
|
|
// array(object, type of fetch)
|
|
[$array, $arrayType],
|
|
[$objectArray, $arrayType],
|
|
[$arrayObject, $anyType],
|
|
[$stdObject, $anyType],
|
|
[$magicPropertyObject, $anyType],
|
|
[$methodObject, $methodType],
|
|
[$methodObject, $anyType],
|
|
[$propertyObject, $anyType],
|
|
[$propertyObject1, $anyType],
|
|
[$propertyObject2, $anyType],
|
|
];
|
|
|
|
$tests = [];
|
|
foreach ($testObjects as $testObject) {
|
|
foreach ($basicTests as $test) {
|
|
// properties cannot be numbers
|
|
if (($testObject[0] instanceof \stdClass || $testObject[0] instanceof TemplatePropertyObject) && is_numeric($test[2])) {
|
|
continue;
|
|
}
|
|
|
|
if ('+4' === $test[2] && $methodObject === $testObject[0]) {
|
|
continue;
|
|
}
|
|
|
|
$tests[] = [$test[0], $test[1], $testObject[0], $test[2], [], $testObject[1]];
|
|
}
|
|
}
|
|
|
|
// additional properties tests
|
|
$tests = array_merge($tests, [
|
|
[true, null, $propertyObject3, 'foo', [], $anyType],
|
|
]);
|
|
|
|
// additional method tests
|
|
$tests = array_merge($tests, [
|
|
[true, 'defined', $methodObject, 'defined', [], $methodType],
|
|
[true, 'defined', $methodObject, 'DEFINED', [], $methodType],
|
|
[true, 'defined', $methodObject, 'getDefined', [], $methodType],
|
|
[true, 'defined', $methodObject, 'GETDEFINED', [], $methodType],
|
|
[true, 'static', $methodObject, 'static', [], $methodType],
|
|
[true, 'static', $methodObject, 'getStatic', [], $methodType],
|
|
|
|
[true, '__call_undefined', $magicMethodObject, 'undefined', [], $methodType],
|
|
[true, '__call_UNDEFINED', $magicMethodObject, 'UNDEFINED', [], $methodType],
|
|
]);
|
|
|
|
// add the same tests for the any type
|
|
foreach ($tests as $test) {
|
|
if ($anyType !== $test[5]) {
|
|
$test[5] = $anyType;
|
|
$tests[] = $test;
|
|
}
|
|
}
|
|
|
|
$methodAndPropObject = new TemplateMethodAndPropObject();
|
|
|
|
// additional method tests
|
|
$tests = array_merge($tests, [
|
|
[true, 'a', $methodAndPropObject, 'a', [], $anyType],
|
|
[true, 'a', $methodAndPropObject, 'a', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'a', [], $arrayType],
|
|
|
|
[true, 'b_prop', $methodAndPropObject, 'b', [], $anyType],
|
|
[true, 'b', $methodAndPropObject, 'B', [], $anyType],
|
|
[true, 'b', $methodAndPropObject, 'b', [], $methodType],
|
|
[true, 'b', $methodAndPropObject, 'B', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'b', [], $arrayType],
|
|
|
|
[false, null, $methodAndPropObject, 'c', [], $anyType],
|
|
[false, null, $methodAndPropObject, 'c', [], $methodType],
|
|
[false, null, $methodAndPropObject, 'c', [], $arrayType],
|
|
]);
|
|
|
|
$arrayAccess = new TemplateArrayAccess();
|
|
$tests = array_merge($tests, [
|
|
[true, ['foo' => 'bar'], $arrayAccess, 'vars', [], $anyType],
|
|
]);
|
|
|
|
// test for Closure::__invoke()
|
|
$tests[] = [true, 'closure called', static fn (): string => 'closure called', '__invoke', [], $anyType];
|
|
$tests[] = [true, 'closure called', static fn (): string => 'closure called', '__invoke', [], $methodType];
|
|
|
|
// tests when input is not an array or object
|
|
$tests = array_merge($tests, [
|
|
[false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a int variable ("42") in "index.twig".'],
|
|
[false, null, 'string', 'a', [], $anyType, 'Impossible to access an attribute ("a") on a string variable ("string") in "index.twig".'],
|
|
[false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the sequence/mapping is empty in "index.twig".'],
|
|
]);
|
|
|
|
return $tests;
|
|
}
|
|
|
|
public function testGetIsMethods(): void
|
|
{
|
|
$twig = new Environment(new ArrayLoader());
|
|
|
|
$getIsObject = new TemplateGetIsMethods();
|
|
$template = new TemplateForTest($twig, 'index.twig');
|
|
// first time should not create a cache for "get"
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 'get'));
|
|
// 0 should be in the method cache now, so this should fail
|
|
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 0));
|
|
}
|
|
}
|
|
|
|
class TemplateForTest extends Template
|
|
{
|
|
private $name;
|
|
|
|
public function __construct(Environment $env, $name = 'index.twig')
|
|
{
|
|
parent::__construct($env);
|
|
$this->name = $name;
|
|
}
|
|
|
|
public function getZero()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public function getEmpty()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function getString()
|
|
{
|
|
return 'some_string';
|
|
}
|
|
|
|
public function getTrue()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function getTemplateName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
public function getDebugInfo(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function getSourceContext(): Source
|
|
{
|
|
return new Source('', $this->getTemplateName());
|
|
}
|
|
|
|
protected function doGetParent(array $context): bool|string|Template|TemplateWrapper
|
|
{
|
|
return false;
|
|
}
|
|
|
|
protected function doDisplay(array $context, array $blocks = []): iterable
|
|
{
|
|
}
|
|
|
|
public function block_name($context, array $blocks = []): void
|
|
{
|
|
}
|
|
}
|
|
|
|
final class TemplateStringableKey implements \Stringable
|
|
{
|
|
public int $toStringCalls = 0;
|
|
|
|
public function __toString(): string
|
|
{
|
|
++$this->toStringCalls;
|
|
|
|
return 'string';
|
|
}
|
|
}
|
|
|
|
final class TemplateTrackingArrayAccess implements \ArrayAccess
|
|
{
|
|
public array $offsetExistsCalls = [];
|
|
public ?\TypeError $offsetExistsError = null;
|
|
public array $offsetGetCalls = [];
|
|
|
|
public function __construct(
|
|
private object $key,
|
|
private bool $rejectObjectOffset,
|
|
) {
|
|
}
|
|
|
|
public function offsetExists(mixed $offset): bool
|
|
{
|
|
$this->offsetExistsCalls[] = $offset;
|
|
|
|
if ($this->rejectObjectOffset && \is_object($offset)) {
|
|
throw $this->offsetExistsError = new \TypeError('Object offsets are not supported.');
|
|
}
|
|
|
|
return $this->key === $offset;
|
|
}
|
|
|
|
public function offsetGet(mixed $offset): mixed
|
|
{
|
|
$this->offsetGetCalls[] = $offset;
|
|
|
|
return 'value';
|
|
}
|
|
|
|
public function offsetSet(mixed $offset, mixed $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset(mixed $offset): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateArrayAccessObject implements \ArrayAccess
|
|
{
|
|
protected $protected = 'protected';
|
|
|
|
public $attributes = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
public function offsetExists($name): bool
|
|
{
|
|
return \array_key_exists($name, $this->attributes);
|
|
}
|
|
|
|
public function offsetGet($name): mixed
|
|
{
|
|
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
|
|
}
|
|
|
|
public function offsetSet($name, $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset($name): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateMagicPropertyObject
|
|
{
|
|
public $defined = 'defined';
|
|
|
|
public $attributes = [
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'1' => 1,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
'09' => '09',
|
|
'+4' => '+4',
|
|
];
|
|
|
|
protected $protected = 'protected';
|
|
|
|
public function __isset($name): bool
|
|
{
|
|
return \array_key_exists($name, $this->attributes);
|
|
}
|
|
|
|
public function __get($name)
|
|
{
|
|
return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
|
|
}
|
|
}
|
|
|
|
class TemplateMagicPropertyObjectWithException
|
|
{
|
|
public function __isset($key): bool
|
|
{
|
|
throw new \Exception('Hey! Don\'t try to isset me!');
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObject
|
|
{
|
|
public $defined = 'defined';
|
|
public $zero = 0;
|
|
public $null;
|
|
public $bar = true;
|
|
public $foo = true;
|
|
public $baz = 'baz';
|
|
public $baf = 'baf';
|
|
|
|
protected $protected = 'protected';
|
|
}
|
|
|
|
class TemplatePropertyObjectAndIterator extends TemplatePropertyObject implements \IteratorAggregate
|
|
{
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return new \ArrayIterator(['foo', 'bar']);
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObjectAndArrayAccess extends TemplatePropertyObject implements \ArrayAccess
|
|
{
|
|
private $data = [
|
|
'defined' => 'defined',
|
|
'zero' => 0,
|
|
'null' => null,
|
|
'bar' => true,
|
|
'foo' => true,
|
|
'baz' => 'baz',
|
|
'baf' => 'baf',
|
|
];
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return \array_key_exists($offset, $this->data);
|
|
}
|
|
|
|
public function offsetGet($offset): mixed
|
|
{
|
|
return $this->offsetExists($offset) ? $this->data[$offset] : 'n/a';
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplatePropertyObjectDefinedWithUndefinedValue
|
|
{
|
|
public $foo;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->foo = @$notExist;
|
|
}
|
|
}
|
|
|
|
class TemplateMethodObject
|
|
{
|
|
public function getDefined()
|
|
{
|
|
return 'defined';
|
|
}
|
|
|
|
public function get1()
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public function get09()
|
|
{
|
|
return '09';
|
|
}
|
|
|
|
public function getZero()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public function getNull(): void
|
|
{
|
|
}
|
|
|
|
public function isBar()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function hasFoo()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function hasBaz()
|
|
{
|
|
return 'should never be returned (has)';
|
|
}
|
|
|
|
public function isBaz()
|
|
{
|
|
return 'should never be returned (is)';
|
|
}
|
|
|
|
public function getBaz()
|
|
{
|
|
return 'Baz';
|
|
}
|
|
|
|
public function baz()
|
|
{
|
|
return 'baz';
|
|
}
|
|
|
|
public function hasBaf()
|
|
{
|
|
return 'should never be returned (has)';
|
|
}
|
|
|
|
public function isBaf()
|
|
{
|
|
return 'baf';
|
|
}
|
|
|
|
protected function getProtected()
|
|
{
|
|
return 'protected';
|
|
}
|
|
|
|
public static function getStatic()
|
|
{
|
|
return 'static';
|
|
}
|
|
}
|
|
|
|
class TemplateGetIsMethods
|
|
{
|
|
public function get(): void
|
|
{
|
|
}
|
|
|
|
public function is(): void
|
|
{
|
|
}
|
|
}
|
|
|
|
class TemplateMethodAndPropObject
|
|
{
|
|
private $a = 'a_prop';
|
|
|
|
public function getA()
|
|
{
|
|
return 'a';
|
|
}
|
|
|
|
public $b = 'b_prop';
|
|
|
|
public function getB()
|
|
{
|
|
return 'b';
|
|
}
|
|
|
|
private $c = 'c_prop';
|
|
|
|
private function getC()
|
|
{
|
|
return 'c';
|
|
}
|
|
}
|
|
|
|
class TemplateArrayAccess implements \ArrayAccess
|
|
{
|
|
public $vars = [
|
|
'foo' => 'bar',
|
|
];
|
|
private $children = [];
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return \array_key_exists($offset, $this->children);
|
|
}
|
|
|
|
public function offsetGet($offset): mixed
|
|
{
|
|
return $this->children[$offset];
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
$this->children[$offset] = $value;
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
unset($this->children[$offset]);
|
|
}
|
|
}
|
|
|
|
class TemplateMagicMethodObject
|
|
{
|
|
public function __call($method, $arguments)
|
|
{
|
|
return '__call_'.$method;
|
|
}
|
|
}
|
|
|
|
class TemplateMagicMethodExceptionObject
|
|
{
|
|
public function __call($method, $arguments): void
|
|
{
|
|
throw new \BadMethodCallException(\sprintf('Unknown method "%s".', $method));
|
|
}
|
|
}
|