mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-31 12:37:15 +00:00
Fix sandbox __toString bypasses
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# 3.26.0 (2026-XX-XX)
|
||||
|
||||
* Encode single quotes as `\x27` in `Compiler::string()` as a defense-in-depth measure
|
||||
* Fix sandbox `__toString` bypasses
|
||||
* Add `Twig\Node\CoercesChildrenToStringInterface` to let nodes declare which of their child nodes will be string-coerced at runtime so the sandbox wraps them with a `__toString` check
|
||||
|
||||
# 3.25.0 (2026-05-17)
|
||||
|
||||
|
||||
@@ -1686,6 +1686,9 @@ final class CoreExtension extends AbstractExtension
|
||||
public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1)
|
||||
{
|
||||
$propertyNotAllowedError = null;
|
||||
if ($sandboxed && $item instanceof \Stringable) {
|
||||
$env->getExtension(SandboxExtension::class)->ensureToStringAllowed($item, $lineno, $source);
|
||||
}
|
||||
|
||||
// array
|
||||
if (Template::METHOD_CALL !== $type) {
|
||||
|
||||
@@ -142,6 +142,24 @@ final class SandboxExtension extends AbstractExtension
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialises a spread operand and runs the policy on every element.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array
|
||||
{
|
||||
if ($obj instanceof \Traversable) {
|
||||
$obj = iterator_to_array($obj);
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
|
||||
{
|
||||
foreach ($obj as $k => $v) {
|
||||
|
||||
@@ -28,16 +28,17 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
#[YieldReady]
|
||||
class CheckToStringNode extends AbstractExpression
|
||||
{
|
||||
public function __construct(AbstractExpression $expr)
|
||||
public function __construct(AbstractExpression $expr, bool $spread = false)
|
||||
{
|
||||
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
|
||||
parent::__construct(['expr' => $expr], ['spread' => $spread], $expr->getTemplateLine());
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$expr = $this->getNode('expr');
|
||||
$method = $this->getAttribute('spread') ? 'ensureSpreadAllowed' : 'ensureToStringAllowed';
|
||||
$compiler
|
||||
->raw('$this->sandbox->ensureToStringAllowed(')
|
||||
->raw('$this->sandbox->'.$method.'(')
|
||||
->subcompile($expr)
|
||||
->raw(', ')
|
||||
->repr($expr->getTemplateLine())
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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\Node;
|
||||
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
|
||||
/**
|
||||
* Implemented by nodes that implicitly coerce one or more of their child
|
||||
* nodes to string at runtime (PHP string casts, regex matching, comparisons,
|
||||
* range bounds, template-name resolution by the loader, etc.).
|
||||
*
|
||||
* The sandbox node visitor wraps the listed children with a CheckToStringNode
|
||||
* so that an implicit `__toString()` call goes through the sandbox policy
|
||||
* check, independently of where this node's result is used.
|
||||
*
|
||||
* This is distinct from {@see OperatorEscapeInterface}, which describes
|
||||
* operands whose value becomes this expression's value (passthrough operators
|
||||
* like ternaries) and is consumed by the escaper.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* Returns the names of the child nodes that will be coerced to
|
||||
* string when this node is evaluated.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getStringCoercedChildNames(): array;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\ConstantExpression;
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class DeprecatedNode extends Node
|
||||
class DeprecatedNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, int $lineno)
|
||||
{
|
||||
@@ -70,4 +70,18 @@ class DeprecatedNode extends Node
|
||||
->raw(");\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the message is concatenated with `.`, and `package` / `version` are typed `string` on trigger_deprecation()
|
||||
$names = ['expr'];
|
||||
if ($this->hasNode('package')) {
|
||||
$names[] = 'package';
|
||||
}
|
||||
if ($this->hasNode('version')) {
|
||||
$names[] = 'version';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnStringInterface;
|
||||
|
||||
class ConcatBinary extends AbstractBinary implements ReturnStringInterface
|
||||
class ConcatBinary extends AbstractBinary implements ReturnStringInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('.');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class EqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class EqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class EqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('==');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class GreaterBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class GreaterBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('>');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('>=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class LessBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class LessBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('<');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('<=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
use Twig\Node\Node;
|
||||
|
||||
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class MatchesBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(Node $left, Node $right, int $lineno)
|
||||
{
|
||||
@@ -57,4 +58,9 @@ class MatchesBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnBoolInterface;
|
||||
|
||||
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -37,4 +38,9 @@ class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface
|
||||
{
|
||||
return $compiler->raw('!=');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnArrayInterface;
|
||||
|
||||
class RangeBinary extends AbstractBinary implements ReturnArrayInterface
|
||||
class RangeBinary extends AbstractBinary implements ReturnArrayInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
@@ -31,4 +32,9 @@ class RangeBinary extends AbstractBinary implements ReturnArrayInterface
|
||||
{
|
||||
return $compiler->raw('..');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
namespace Twig\Node\Expression\Binary;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ReturnNumberInterface;
|
||||
|
||||
class SpaceshipBinary extends AbstractBinary implements ReturnNumberInterface
|
||||
class SpaceshipBinary extends AbstractBinary implements ReturnNumberInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function operator(Compiler $compiler): Compiler
|
||||
{
|
||||
return $compiler->raw('<=>');
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Node;
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ use Twig\Node\Node;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -60,6 +61,12 @@ class BlockReferenceExpression extends AbstractExpression implements SupportDefi
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the template expression is resolved through the loader, which coerces it to a string
|
||||
return $this->hasNode('template') ? ['template'] : [];
|
||||
}
|
||||
|
||||
private function compileTemplateCall(Compiler $compiler, string $method): Compiler
|
||||
{
|
||||
if (!$this->hasNode('template')) {
|
||||
|
||||
@@ -14,11 +14,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class FilterExpression extends CallExpression
|
||||
class FilterExpression extends CallExpression implements CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $node
|
||||
@@ -77,4 +78,10 @@ class FilterExpression extends CallExpression
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// a filter may coerce its input and arguments to string (e.g. `upper`, `replace`)
|
||||
return ['node', 'arguments'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class FunctionExpression extends CallExpression implements SupportDefinedTestInterface
|
||||
class FunctionExpression extends CallExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -78,4 +79,10 @@ class FunctionExpression extends CallExpression implements SupportDefinedTestInt
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// a function may coerce its arguments to string (the host PHP code is opaque to Twig)
|
||||
return ['arguments'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Template;
|
||||
|
||||
class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
@@ -157,6 +158,12 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// for a method-like access, the host PHP method may coerce any of its arguments to string
|
||||
return $this->hasNode('arguments') ? ['arguments'] : [];
|
||||
}
|
||||
|
||||
private function changeIgnoreStrictCheck(self $node): void
|
||||
{
|
||||
$node->setAttribute('optimizable', false);
|
||||
|
||||
@@ -59,4 +59,10 @@ class DefinedTest extends TestExpression
|
||||
{
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the `defined` test does not coerce its node to string (it only inspects existence)
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,10 @@ class DivisiblebyTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ class EvenTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,10 @@ class NullTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// `=== null` is strict, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ class OddTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// PHP `%` rejects Stringable with a TypeError, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,10 @@ class SameasTest extends TestExpression
|
||||
->raw(')')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// `===` is strict, no coercion
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,10 @@ class TrueTest extends TestExpression
|
||||
->raw(') && $tmp instanceof Markup ? (string) $tmp : $tmp)')
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the `(string)` cast only fires for Markup instances, whose __toString is always allowed
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigTest;
|
||||
|
||||
class TestExpression extends CallExpression implements ReturnBoolInterface
|
||||
class TestExpression extends CallExpression implements ReturnBoolInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
#[FirstClassTwigCallableReady]
|
||||
/**
|
||||
@@ -70,4 +71,21 @@ class TestExpression extends CallExpression implements ReturnBoolInterface
|
||||
|
||||
$this->compileCallable($compiler);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
$names = [];
|
||||
|
||||
// the `empty` test triggers an implicit string coercion through `CoreExtension::testEmpty()`
|
||||
if ('empty' === $this->getAttribute('name')) {
|
||||
$names[] = 'node';
|
||||
}
|
||||
|
||||
// a test may coerce its arguments to string (the host PHP code is opaque to Twig)
|
||||
if ($this->hasNode('arguments')) {
|
||||
$names[] = 'arguments';
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use Twig\Node\Expression\Variable\ContextVariable;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class ImportNode extends Node
|
||||
class ImportNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno)
|
||||
{
|
||||
@@ -58,4 +58,10 @@ class ImportNode extends Node
|
||||
|
||||
$compiler->raw(";\n");
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the loader resolves the template-name expression by coercing it to a string
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class IncludeNode extends Node implements NodeOutputInterface
|
||||
class IncludeNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
|
||||
{
|
||||
@@ -130,4 +130,10 @@ class IncludeNode extends Node implements NodeOutputInterface
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the loader resolves the template-name expression by coercing it to a string
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use Twig\Source;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
final class ModuleNode extends Node
|
||||
final class ModuleNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
/**
|
||||
* @param BodyNode $body
|
||||
@@ -90,6 +90,12 @@ final class ModuleNode extends Node
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
// the parent expression is resolved through the loader, which coerces it to a string
|
||||
return $this->hasNode('parent') ? ['parent'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
#[YieldReady]
|
||||
class PrintNode extends Node implements NodeOutputInterface
|
||||
class PrintNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $expr, int $lineno)
|
||||
{
|
||||
@@ -41,4 +41,9 @@ class PrintNode extends Node implements NodeOutputInterface
|
||||
->raw(";\n")
|
||||
;
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,18 @@ use Twig\Environment;
|
||||
use Twig\Node\CheckSecurityCallNode;
|
||||
use Twig\Node\CheckSecurityNode;
|
||||
use Twig\Node\CheckToStringNode;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
use Twig\Node\Expression\Binary\RangeBinary;
|
||||
use Twig\Node\Expression\FilterExpression;
|
||||
use Twig\Node\Expression\FunctionExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\Nodes;
|
||||
use Twig\Node\PrintNode;
|
||||
use Twig\Node\SetNode;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -43,7 +42,6 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
private $filters;
|
||||
/** @var array<string, int> */
|
||||
private $functions;
|
||||
private $needsToStringWrap = false;
|
||||
|
||||
public function enterNode(Node $node, Environment $env): Node
|
||||
{
|
||||
@@ -52,8 +50,6 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
$this->tags = [];
|
||||
$this->filters = [];
|
||||
$this->functions = [];
|
||||
|
||||
return $node;
|
||||
} elseif ($this->inAModule) {
|
||||
// look for tags
|
||||
if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) {
|
||||
@@ -74,29 +70,13 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
|
||||
$this->functions['range'] = $node->getTemplateLine();
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof PrintNode) {
|
||||
$this->needsToStringWrap = true;
|
||||
$this->wrapNode($node, 'expr');
|
||||
}
|
||||
|
||||
if ($node instanceof SetNode && !$node->getAttribute('capture')) {
|
||||
$this->needsToStringWrap = true;
|
||||
}
|
||||
|
||||
// wrap outer nodes that can implicitly call __toString()
|
||||
if ($this->needsToStringWrap) {
|
||||
if ($node instanceof ConcatBinary) {
|
||||
$this->wrapNode($node, 'left');
|
||||
$this->wrapNode($node, 'right');
|
||||
}
|
||||
if ($node instanceof FilterExpression) {
|
||||
$this->wrapNode($node, 'node');
|
||||
$this->wrapArrayNode($node, 'arguments');
|
||||
}
|
||||
if ($node instanceof FunctionExpression) {
|
||||
$this->wrapArrayNode($node, 'arguments');
|
||||
}
|
||||
// wrap children that the node itself will string-coerce at runtime;
|
||||
// applies to ModuleNode (`parent` slot for {% extends %}) too
|
||||
if ($this->inAModule && $node instanceof CoercesChildrenToStringInterface) {
|
||||
foreach ($node->getStringCoercedChildNames() as $childName) {
|
||||
$this->wrapNode($node, $childName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +90,6 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
$node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
|
||||
$node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
|
||||
} elseif ($this->inAModule) {
|
||||
if ($node instanceof PrintNode || $node instanceof SetNode) {
|
||||
$this->needsToStringWrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $node;
|
||||
@@ -122,22 +98,24 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
private function wrapNode(Node $node, string $name): void
|
||||
{
|
||||
$expr = $node->getNode($name);
|
||||
// `_self` is internal: it compiles to `$this->getTemplateName()` and is always a string
|
||||
if ($expr instanceof ContextVariable && '_self' === $expr->getAttribute('name')) {
|
||||
return;
|
||||
}
|
||||
if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
} elseif ($expr instanceof SpreadUnary) {
|
||||
$this->wrapNode($expr, 'node');
|
||||
} elseif ($expr instanceof ArrayExpression) {
|
||||
$expr->setNode('node', new CheckToStringNode($expr->getNode('node'), true));
|
||||
} elseif ($expr instanceof ArrayExpression || $expr instanceof Nodes) {
|
||||
foreach ($expr as $name => $_) {
|
||||
$this->wrapNode($expr, $name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function wrapArrayNode(Node $node, string $name): void
|
||||
{
|
||||
$args = $node->getNode($name);
|
||||
foreach ($args as $name => $_) {
|
||||
$this->wrapNode($args, $name);
|
||||
} elseif ($expr instanceof OperatorEscapeInterface) {
|
||||
foreach ($expr->getOperandNamesToEscape() as $operandName) {
|
||||
$this->wrapNode($expr, $operandName);
|
||||
}
|
||||
} elseif ($expr instanceof FilterExpression || $expr instanceof FunctionExpression) {
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class SandboxTest extends TestCase
|
||||
'array_like' => new ArrayLikeObject(),
|
||||
'magic' => new MagicObject(),
|
||||
'recursion' => [4],
|
||||
'iterator' => new \ArrayIterator(['a', new FooObject()]),
|
||||
];
|
||||
self::$params['recursion'][] = &self::$params['recursion'];
|
||||
self::$params['recursion'][] = new FooObject();
|
||||
@@ -293,7 +294,8 @@ class SandboxTest extends TestCase
|
||||
*/
|
||||
public function testSandboxUnallowedToString($template)
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper', 'join', 'replace'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']);
|
||||
$twig = $this->getEnvironment(true, [], ['index' => $template], ['if', 'do', 'for', 'set'], ['upper', 'join', 'replace', 'format', 'split'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random', 'range', 'my_func']);
|
||||
$twig->addFunction(new \Twig\TwigFunction('my_func', fn ($a) => (string) $a));
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if an unallowed method "__toString()" method is called in the template');
|
||||
@@ -329,16 +331,171 @@ class SandboxTest extends TestCase
|
||||
'context' => ['{{ _context|join(", ") }}'],
|
||||
'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'],
|
||||
'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'],
|
||||
'spread_iterator_in_function_args' => ['{{ ["x", ...iterator]|join(",") }}'],
|
||||
'recursion' => ['{{ recursion|join(", ") }}'],
|
||||
'ternary_print' => ['{{ true ? obj : "" }}'],
|
||||
'ternary_filter_input' => ['{{ (true ? obj : "")|upper }}'],
|
||||
'elvis_filter_input' => ['{{ (obj ?: "")|upper }}'],
|
||||
'nullcoalesce_filter_input' => ['{{ (obj ?? "")|upper }}'],
|
||||
'function_arg_with_ternary' => ['{{ random(true ? obj : "") }}'],
|
||||
'filter_arg_with_ternary' => ['{{ "%s"|format(true ? obj : "") }}'],
|
||||
'matches_in_print' => ['{{ obj matches "/foo/" ? "1" : "0" }}'],
|
||||
'equal_in_print' => ['{{ obj == "x" ? "1" : "0" }}'],
|
||||
'equal_in_if' => ['{% if obj == "x" %}LEAK{% endif %}'],
|
||||
'notequal_in_if' => ['{% if obj != "x" %}LEAK{% endif %}'],
|
||||
'spaceship_in_if' => ['{% if (obj <=> "x") == 0 %}LEAK{% endif %}'],
|
||||
'less_in_if' => ['{% if obj < "B" %}LEAK{% endif %}'],
|
||||
'greater_in_if' => ['{% if obj > "A" %}LEAK{% endif %}'],
|
||||
'lessequal_in_if' => ['{% if obj <= "z" %}LEAK{% endif %}'],
|
||||
'greaterequal_in_if' => ['{% if obj >= "a" %}LEAK{% endif %}'],
|
||||
'concat_left_in_if' => ['{% if obj ~ "" %}LEAK{% endif %}'],
|
||||
'concat_right_in_if' => ['{% if "" ~ obj %}LEAK{% endif %}'],
|
||||
'range_left' => ['{% for x in obj..1 %}LEAK{% endfor %}'],
|
||||
'range_right' => ['{% for x in 1..obj %}LEAK{% endfor %}'],
|
||||
'do_tag_function_arg' => ['{% do my_func(obj) %}'],
|
||||
'do_tag_filter_input' => ['{% do obj|upper %}'],
|
||||
'do_tag_concat' => ['{% do obj ~ "" %}'],
|
||||
'set_tag_filter_input' => ['{% set _ = obj|upper %}'],
|
||||
'set_tag_concat' => ['{% set _ = obj ~ "" %}'],
|
||||
'set_capture_print' => ['{% set _ %}{{ obj }}{% endset %}'],
|
||||
'is_empty_in_if' => ['{% if obj is empty %}LEAK{% endif %}'],
|
||||
'is_empty_in_print' => ['{{ obj is empty ? "1" : "0" }}'],
|
||||
'method_argument' => ['{{ obj.foo(obj.anotherFooObject) }}'],
|
||||
'filter_input_in_if' => ['{% if obj|upper == "X" %}LEAK{% endif %}'],
|
||||
'filter_arg_in_if' => ['{% if "x"|replace({"x": obj}) == "y" %}LEAK{% endif %}'],
|
||||
'function_arg_in_if' => ['{% if not random(obj) %}LEAK{% endif %}'],
|
||||
'filter_input_in_for' => ['{% for x in (obj|split(",")) %}LEAK{% endfor %}'],
|
||||
'function_arg_in_for' => ['{% for x in [random(obj)] %}LEAK{% endfor %}'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnFunctionReturn()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{{ make_obj() }}'], [], [], [], [], ['make_obj']);
|
||||
$twig->addFunction(new \Twig\TwigFunction('make_obj', fn () => new FooObject()));
|
||||
try {
|
||||
$twig->load('index')->render([]);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on the return of an allowed function');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnFilterReturn()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{{ "x"|to_obj }}'], [], ['to_obj']);
|
||||
$twig->addFilter(new \Twig\TwigFilter('to_obj', fn () => new FooObject()));
|
||||
try {
|
||||
$twig->load('index')->render([]);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on the return of an allowed filter');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnDynamicAttributeName()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, ['strict_variables' => true], ['index' => '{{ arr[obj] }}'], [], [], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on a dynamic attribute name');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnIncludeTemplateName()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{% include obj %}'], ['include']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on an include template name');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnExtendsTemplateName()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{% extends obj %}'], ['extends']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on an extends template name');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnBlockFunctionTemplateName()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{{ block("content", obj) }}'], [], [], [], [], ['block']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on a block() template argument');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnEmbedTemplateName()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{% embed obj %}{% endembed %}'], ['embed', 'extends']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on an embed template name');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnIsConstantTestArgument()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{% if "x" is constant(obj) %}LEAK{% endif %}'], ['if']);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on a constant test argument');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxBlocksToStringOnDeprecatedMessage()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{% deprecated obj %}'], ['deprecated']);
|
||||
$previous = set_error_handler(static fn () => true, \E_USER_DEPRECATED);
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox throws a SecurityError exception if __toString is called on a deprecated tag message');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertEquals('__tostring', $e->getMethodName());
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
public function testSandboxKeepsSelfImportShortcut()
|
||||
{
|
||||
$tpl = "{% macro local_lower(s) %}{{ s|lower }}{% endmacro %}{% from _self import local_lower %}{{ local_lower('A') }}";
|
||||
$twig = $this->getEnvironment(true, [], ['index' => $tpl], ['from', 'macro', 'import'], ['lower']);
|
||||
|
||||
$this->assertSame('a', $twig->load('index')->render([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getSandboxAllowedToStringTests
|
||||
*/
|
||||
public function testSandboxAllowedToString($template, $output)
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => $template], ['set'], [], ['Twig\Tests\Extension\FooObject' => ['foo', 'getAnotherFooObject']]);
|
||||
$twig = $this->getEnvironment(true, [], ['index' => $template], ['set', 'do'], [], ['Twig\Tests\Extension\FooObject' => ['foo', 'getAnotherFooObject']]);
|
||||
$this->assertEquals($output, $twig->load('index')->render(self::$params));
|
||||
}
|
||||
|
||||
@@ -347,6 +504,8 @@ class SandboxTest extends TestCase
|
||||
return [
|
||||
'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''],
|
||||
'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'],
|
||||
'do_object_discarded' => ['{% do obj %}', ''],
|
||||
'set_object_assigned' => ['{% set a = obj %}{{ a is defined ? "1" : "0" }}', '1'],
|
||||
'is_defined1' => ['{{ obj.anotherFooObject is defined }}', '1'],
|
||||
'is_defined2' => ['{{ magic.foo is defined }}', ''],
|
||||
'is_null' => ['{{ obj is null }}', ''],
|
||||
|
||||
@@ -25,9 +25,12 @@ use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Node\BodyNode;
|
||||
use Twig\Node\CheckToStringNode;
|
||||
use Twig\Node\CoercesChildrenToStringInterface;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\ModuleNode;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\PrintNode;
|
||||
use Twig\NodeTraverser;
|
||||
use Twig\NodeVisitor\SandboxNodeVisitor;
|
||||
@@ -47,4 +50,64 @@ class SandboxTest extends TestCase
|
||||
$this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode(0)->getNode('expr'));
|
||||
$this->assertSame("// line 1\nyield from (\$context[\"foo\"] ?? null);\n", $env->compile($node->getNode('body')));
|
||||
}
|
||||
|
||||
public function testCustomNodeImplementingCoercesChildrenToStringInterfaceIsWrapped()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$custom = new CustomCoercingExpression(new ContextVariable('foo', 1), new ContextVariable('bar', 1), 1);
|
||||
// wrap inside a PrintNode so it lives in a module; the wrapping must happen on the
|
||||
// custom node itself regardless of the print context
|
||||
$node = new ModuleNode(new BodyNode([new PrintNode($custom, 1)]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo'));
|
||||
$traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]);
|
||||
$node = $traverser->traverse($node);
|
||||
|
||||
$custom = $node->getNode('body')->getNode(0)->getNode('expr');
|
||||
$this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('left'));
|
||||
$this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('right'));
|
||||
}
|
||||
|
||||
public function testCustomNonExpressionNodeImplementingCoercesChildrenToStringInterfaceIsWrapped()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$custom = new CustomCoercingNode(['expr' => new ContextVariable('foo', 1)], [], 1);
|
||||
$node = new ModuleNode(new BodyNode([$custom]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo'));
|
||||
$traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]);
|
||||
$node = $traverser->traverse($node);
|
||||
|
||||
$custom = $node->getNode('body')->getNode(0);
|
||||
$this->assertInstanceOf(CheckToStringNode::class, $custom->getNode('expr'));
|
||||
}
|
||||
|
||||
public function testSelfIsNeverWrapped()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$self = new ContextVariable('_self', 1);
|
||||
$custom = new CustomCoercingNode(['expr' => $self], [], 1);
|
||||
$node = new ModuleNode(new BodyNode([$custom]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo'));
|
||||
$traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]);
|
||||
$node = $traverser->traverse($node);
|
||||
|
||||
$this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode(0)->getNode('expr'));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCoercingExpression extends AbstractExpression implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno)
|
||||
{
|
||||
parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
|
||||
}
|
||||
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['left', 'right'];
|
||||
}
|
||||
}
|
||||
|
||||
class CustomCoercingNode extends Node implements CoercesChildrenToStringInterface
|
||||
{
|
||||
public function getStringCoercedChildNames(): array
|
||||
{
|
||||
return ['expr'];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user