mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-02 09:56:49 +00:00
Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
|
||||
* Add the macro name to the deprecation about omitting parentheses when calling a macro
|
||||
* Deprecate cloning a `Twig\Environment` instance
|
||||
* Fix array access with a `Stringable` key on subclasses of `ArrayObject` and `ArrayIterator`
|
||||
|
||||
# 3.29.0 (2026-09-18)
|
||||
|
||||
|
||||
@@ -131,14 +131,6 @@ final class CoreExtension extends AbstractExtension
|
||||
'SplStack',
|
||||
'WeakMap',
|
||||
];
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public const STRINGABLE_KEY_ARRAY_ACCESS_CLASSES = [
|
||||
'ArrayIterator',
|
||||
'ArrayObject',
|
||||
'RecursiveArrayIterator',
|
||||
];
|
||||
|
||||
private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B";
|
||||
|
||||
@@ -1757,6 +1749,10 @@ final class CoreExtension extends AbstractExtension
|
||||
if (Template::METHOD_CALL !== $type) {
|
||||
$arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item;
|
||||
|
||||
if ($arrayItem instanceof \Stringable && ($object instanceof \ArrayObject || $object instanceof \ArrayIterator)) {
|
||||
$arrayItem = (string) $arrayItem;
|
||||
}
|
||||
|
||||
if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) {
|
||||
try {
|
||||
$env->getExtension(SandboxExtension::class)->getChecker()->checkPropertyAllowed($object, $arrayItem, $lineno, $source);
|
||||
@@ -1769,10 +1765,6 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
}
|
||||
|
||||
if ($object instanceof \ArrayAccess && $arrayItem instanceof \Stringable && \in_array($object::class, self::STRINGABLE_KEY_ARRAY_ACCESS_CLASSES, true)) {
|
||||
$arrayItem = (string) $arrayItem;
|
||||
}
|
||||
|
||||
if (match (true) {
|
||||
\is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object),
|
||||
$object instanceof \ArrayAccess => $object->offsetExists($arrayItem),
|
||||
|
||||
@@ -195,7 +195,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
$compiler
|
||||
->raw('(('.$key.' = ')
|
||||
->subcompile($attribute)
|
||||
->raw(') instanceof \Stringable && (is_array('.$var.') || in_array('.$var.'::class, CoreExtension::STRINGABLE_KEY_ARRAY_ACCESS_CLASSES, true)) ? (string) '.$key.' : '.$key.')')
|
||||
->raw(') instanceof \Stringable && (is_array('.$var.') || '.$var.' instanceof \ArrayObject || '.$var.' instanceof \ArrayIterator) ? (string) '.$key.' : '.$key.')')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -352,6 +352,36 @@ class TemplateTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getStringableKeySubclassArrayAccessContainers
|
||||
*/
|
||||
#[DataProvider('getStringableKeySubclassArrayAccessContainers')]
|
||||
public function testStringableKeyIsCoercedForSubclassesOfInternalArrayAccess(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) {
|
||||
// subclasses are not part of CoreExtension::ARRAY_LIKE_CLASSES, so the sandbox checks the key as a property
|
||||
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$key::class => ['__toString']], [$data::class => ['string']], []), true));
|
||||
}
|
||||
|
||||
$this->assertSame('value', $twig->render('index', ['data' => $data, 'key' => $key]));
|
||||
$this->assertSame(1, $key->toStringCalls);
|
||||
}
|
||||
|
||||
public static function getStringableKeySubclassArrayAccessContainers(): iterable
|
||||
{
|
||||
foreach (['lax' => false, 'strict' => true] as $mode => $strict) {
|
||||
foreach (['unsandboxed' => false, 'sandboxed' => true] as $sandboxMode => $sandboxed) {
|
||||
yield $mode.' '.$sandboxMode.' ArrayObject subclass' => [$strict, $sandboxed, new TemplateArrayObjectSubclass(['string' => 'value'])];
|
||||
yield $mode.' '.$sandboxMode.' ArrayIterator subclass' => [$strict, $sandboxed, new TemplateArrayIteratorSubclass(['string' => 'value'])];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getStrictVariablesModes
|
||||
*/
|
||||
@@ -829,6 +859,14 @@ class TemplateForTest extends Template
|
||||
}
|
||||
}
|
||||
|
||||
final class TemplateArrayObjectSubclass extends \ArrayObject
|
||||
{
|
||||
}
|
||||
|
||||
final class TemplateArrayIteratorSubclass extends \ArrayIterator
|
||||
{
|
||||
}
|
||||
|
||||
final class TemplateStringableKey implements \Stringable
|
||||
{
|
||||
public int $toStringCalls = 0;
|
||||
|
||||
Reference in New Issue
Block a user