mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-17 13:53:35 +00:00
bug #4841 Fix Markup truthiness in boolean expressions (xtrime-ru)
This PR was merged into the 3.x branch.
Discussion
----------
Fix Markup truthiness in boolean expressions
## Summary
Fixes Markup truthiness handling in boolean expressions.
`trim` can return a `Twig\Markup` instance for safe strings. Empty `Markup` objects must behave like empty strings in Twig truth tests, but PHP treats all objects as truthy. This caused expressions like this to incorrectly evaluate as true:
```twig
{% set x %} {% endset %}
{% if x|trim and x|trim %}fail{% else %}ok{% endif %}
```
This case was working properly in https://github.com/twigphp/Twig/releases/tag/v3.14.2 and earlier.
## Related commits
- Bug was introduced in [v3.15.0](https://github.com/twigphp/Twig/releases/tag/v3.15.0) in this commit https://github.com/twigphp/Twig/commit/10c3142d3b036910f63080070c101bfff61e0743
- Partially fixed here: https://github.com/twigphp/Twig/commit/10c3142d3b036910f63080070c101bfff61e0743
## Changes
- Added `TrueTest::wrap()` to centralize wrapping non-primitive expressions with Twig’s Markup-aware true test.
- Reused `TrueTest::wrap()` in:
- `IfNode`
- conditional ternary expressions
- `and`, `or`, and `xor` binary expressions
- Elvis expressions
- unary `not`
- Added regression coverage for boolean operators (`and`, `or`, `xor`, `not`) and ternary/Elvis expressions whose operands evaluate to empty `Markup`.
## Tests
```bash
./vendor/bin/simple-phpunit tests/IntegrationTest.php --filter markup_test
```
Commits
-------
f5afaabf54 Fix Markup truthiness in boolean expressions
This commit is contained in:
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
|||||||
|
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\ReturnBoolInterface;
|
use Twig\Node\Expression\ReturnBoolInterface;
|
||||||
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
|
||||||
class AndBinary extends AbstractBinary implements ReturnBoolInterface
|
class AndBinary extends AbstractBinary implements ReturnBoolInterface
|
||||||
{
|
{
|
||||||
|
public function __construct(Node $left, Node $right, int $lineno)
|
||||||
|
{
|
||||||
|
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||||
|
}
|
||||||
|
|
||||||
public function operator(Compiler $compiler): Compiler
|
public function operator(Compiler $compiler): Compiler
|
||||||
{
|
{
|
||||||
return $compiler->raw('&&');
|
return $compiler->raw('&&');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Twig\Node\Expression\Binary;
|
|||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\AbstractExpression;
|
use Twig\Node\Expression\AbstractExpression;
|
||||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||||
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
use Twig\Node\Node;
|
use Twig\Node\Node;
|
||||||
|
|
||||||
final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface
|
final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface
|
||||||
@@ -26,7 +27,7 @@ final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterfac
|
|||||||
{
|
{
|
||||||
parent::__construct($left, $right, $lineno);
|
parent::__construct($left, $right, $lineno);
|
||||||
|
|
||||||
$this->setNode('test', clone $left);
|
$this->setNode('test', TrueTest::wrap(clone $left));
|
||||||
$left->setAttribute('always_defined', true);
|
$left->setAttribute('always_defined', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
|||||||
|
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\ReturnBoolInterface;
|
use Twig\Node\Expression\ReturnBoolInterface;
|
||||||
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
|
||||||
class OrBinary extends AbstractBinary implements ReturnBoolInterface
|
class OrBinary extends AbstractBinary implements ReturnBoolInterface
|
||||||
{
|
{
|
||||||
|
public function __construct(Node $left, Node $right, int $lineno)
|
||||||
|
{
|
||||||
|
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||||
|
}
|
||||||
|
|
||||||
public function operator(Compiler $compiler): Compiler
|
public function operator(Compiler $compiler): Compiler
|
||||||
{
|
{
|
||||||
return $compiler->raw('||');
|
return $compiler->raw('||');
|
||||||
|
|||||||
@@ -14,9 +14,16 @@ namespace Twig\Node\Expression\Binary;
|
|||||||
|
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\ReturnBoolInterface;
|
use Twig\Node\Expression\ReturnBoolInterface;
|
||||||
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
|
||||||
class XorBinary extends AbstractBinary implements ReturnBoolInterface
|
class XorBinary extends AbstractBinary implements ReturnBoolInterface
|
||||||
{
|
{
|
||||||
|
public function __construct(Node $left, Node $right, int $lineno)
|
||||||
|
{
|
||||||
|
parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno);
|
||||||
|
}
|
||||||
|
|
||||||
public function operator(Compiler $compiler): Compiler
|
public function operator(Compiler $compiler): Compiler
|
||||||
{
|
{
|
||||||
return $compiler->raw('xor');
|
return $compiler->raw('xor');
|
||||||
|
|||||||
@@ -14,19 +14,13 @@ namespace Twig\Node\Expression\Ternary;
|
|||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\AbstractExpression;
|
use Twig\Node\Expression\AbstractExpression;
|
||||||
use Twig\Node\Expression\OperatorEscapeInterface;
|
use Twig\Node\Expression\OperatorEscapeInterface;
|
||||||
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
|
||||||
use Twig\Node\Expression\Test\TrueTest;
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
use Twig\TwigTest;
|
|
||||||
|
|
||||||
final class ConditionalTernary extends AbstractExpression implements OperatorEscapeInterface
|
final class ConditionalTernary extends AbstractExpression implements OperatorEscapeInterface
|
||||||
{
|
{
|
||||||
public function __construct(AbstractExpression $test, AbstractExpression $left, AbstractExpression $right, int $lineno)
|
public function __construct(AbstractExpression $test, AbstractExpression $left, AbstractExpression $right, int $lineno)
|
||||||
{
|
{
|
||||||
if (!$test instanceof ReturnPrimitiveTypeInterface) {
|
parent::__construct(['test' => TrueTest::wrap($test), 'left' => $left, 'right' => $right], [], $lineno);
|
||||||
$test = new TrueTest($test, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $test->getTemplateLine());
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::__construct(['test' => $test, 'left' => $left, 'right' => $right], [], $lineno);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function compile(Compiler $compiler): void
|
public function compile(Compiler $compiler): void
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
namespace Twig\Node\Expression\Test;
|
namespace Twig\Node\Expression\Test;
|
||||||
|
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
|
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
||||||
use Twig\Node\Expression\TestExpression;
|
use Twig\Node\Expression\TestExpression;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
use Twig\TwigTest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks that an expression is true.
|
* Checks that an expression is true.
|
||||||
@@ -23,6 +26,15 @@ use Twig\Node\Expression\TestExpression;
|
|||||||
*/
|
*/
|
||||||
class TrueTest extends TestExpression
|
class TrueTest extends TestExpression
|
||||||
{
|
{
|
||||||
|
public static function wrap(Node $node): Node
|
||||||
|
{
|
||||||
|
if ($node instanceof ReturnPrimitiveTypeInterface) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($node, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $node->getTemplateLine());
|
||||||
|
}
|
||||||
|
|
||||||
public function compile(Compiler $compiler): void
|
public function compile(Compiler $compiler): void
|
||||||
{
|
{
|
||||||
$compiler
|
$compiler
|
||||||
|
|||||||
@@ -13,9 +13,17 @@
|
|||||||
namespace Twig\Node\Expression\Unary;
|
namespace Twig\Node\Expression\Unary;
|
||||||
|
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
|
use Twig\Node\Expression\ReturnBoolInterface;
|
||||||
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
|
use Twig\Node\Node;
|
||||||
|
|
||||||
class NotUnary extends AbstractUnary
|
class NotUnary extends AbstractUnary implements ReturnBoolInterface
|
||||||
{
|
{
|
||||||
|
public function __construct(Node $node, int $lineno)
|
||||||
|
{
|
||||||
|
parent::__construct(TrueTest::wrap($node), $lineno);
|
||||||
|
}
|
||||||
|
|
||||||
public function operator(Compiler $compiler): Compiler
|
public function operator(Compiler $compiler): Compiler
|
||||||
{
|
{
|
||||||
return $compiler->raw('!');
|
return $compiler->raw('!');
|
||||||
|
|||||||
+1
-6
@@ -14,9 +14,7 @@ namespace Twig\Node;
|
|||||||
|
|
||||||
use Twig\Attribute\YieldReady;
|
use Twig\Attribute\YieldReady;
|
||||||
use Twig\Compiler;
|
use Twig\Compiler;
|
||||||
use Twig\Node\Expression\ReturnPrimitiveTypeInterface;
|
|
||||||
use Twig\Node\Expression\Test\TrueTest;
|
use Twig\Node\Expression\Test\TrueTest;
|
||||||
use Twig\TwigTest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an if node.
|
* Represents an if node.
|
||||||
@@ -29,10 +27,7 @@ class IfNode extends Node
|
|||||||
public function __construct(Node $tests, ?Node $else, int $lineno)
|
public function __construct(Node $tests, ?Node $else, int $lineno)
|
||||||
{
|
{
|
||||||
for ($i = 0, $count = \count($tests); $i < $count; $i += 2) {
|
for ($i = 0, $count = \count($tests); $i < $count; $i += 2) {
|
||||||
$test = $tests->getNode((string) $i);
|
$tests->setNode($i, TrueTest::wrap($tests->getNode((string) $i)));
|
||||||
if (!$test instanceof ReturnPrimitiveTypeInterface) {
|
|
||||||
$tests->setNode($i, new TrueTest($test, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $test->getTemplateLine()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$nodes = ['tests' => $tests];
|
$nodes = ['tests' => $tests];
|
||||||
if (null !== $else) {
|
if (null !== $else) {
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ Twig outputs 0 nodes correctly
|
|||||||
{% if spaces|trim %}KO{% else %}ok{% endif %}
|
{% if spaces|trim %}KO{% else %}ok{% endif %}
|
||||||
|
|
||||||
{% set bar %} {% endset %}{{ bar|trim ? 'KO' : 'ok' }}
|
{% set bar %} {% endset %}{{ bar|trim ? 'KO' : 'ok' }}
|
||||||
|
{% if bar|trim and bar|trim %}KO{% else %}ok{% endif %}{{- "\n" -}}
|
||||||
|
{% if bar|trim or empty|trim %}KO{% else %}ok{% endif %}{{- "\n" -}}
|
||||||
|
{% if bar|trim xor empty|trim %}KO{% else %}ok{% endif %}{{- "\n" -}}
|
||||||
|
{% if not bar|trim %}ok{% else %}KO{% endif %}{{- "\n" -}}
|
||||||
|
{{ bar|trim ?: 'ok' }}
|
||||||
|
{{ (bar|trim and bar|trim) ? 'KO' : 'ok' }}
|
||||||
|
{{ (bar|trim and bar|trim) ?: 'ok' }}
|
||||||
|
{{ (bar|trim and bar|trim) ?: (bar|trim ?: 'ok') }}
|
||||||
--DATA--
|
--DATA--
|
||||||
return ['spaces' => new Twig\Markup(' ', 'UTF-8'), 'empty' => new Twig\Markup('', 'UTF-8')]
|
return ['spaces' => new Twig\Markup(' ', 'UTF-8'), 'empty' => new Twig\Markup('', 'UTF-8')]
|
||||||
--EXPECT--
|
--EXPECT--
|
||||||
@@ -16,3 +24,11 @@ ok
|
|||||||
ok
|
ok
|
||||||
ok
|
ok
|
||||||
ok
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
ok
|
||||||
|
|||||||
Reference in New Issue
Block a user