mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-16 12:26:30 +00:00
6beff61db4
* 3.x: Improve the way one can deprecate a Twig callable Bump version Prepare the 3.14.0 release Fix a security issue when an included sandboxed template has been loaded before without the sandbox context Fix test fix the version mergeGlobals() is deprecated since Tweak code Fix CS Add more tests Remove unused private methods Bump version
460 lines
13 KiB
PHP
460 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Twig\Tests\Extension;
|
|
|
|
/*
|
|
* 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\SecurityPolicy;
|
|
|
|
class CoreTest extends TestCase
|
|
{
|
|
#[DataProvider('provideCycleCases')]
|
|
public function testCycleFunction($values, $position, $expected)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::cycle($values, $position));
|
|
}
|
|
|
|
public static function provideCycleCases()
|
|
{
|
|
return [
|
|
[[1, 2, 3], 0, 1],
|
|
[[1, 2, 3], 1, 2],
|
|
[[1, 2, 3], 2, 3],
|
|
[[1, 2, 3], 3, 1],
|
|
[[false, 0, null], 0, false],
|
|
[[false, 0, null], 1, 0],
|
|
[[false, 0, null], 2, null],
|
|
|
|
[[['a', 'b'], ['c', 'd']], 3, ['c', 'd']],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideCycleInvalidCases')]
|
|
public function testCycleFunctionThrowRuntimeError($values, mixed $position = null)
|
|
{
|
|
$this->expectException(RuntimeError::class);
|
|
CoreExtension::cycle($values, $position ?? 0);
|
|
}
|
|
|
|
public static function provideCycleInvalidCases()
|
|
{
|
|
return [
|
|
'empty' => [[]],
|
|
'non-countable' => [new class() extends \ArrayObject {
|
|
}],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('getRandomFunctionTestData')]
|
|
public function testRandomFunction(array $expectedInArray, $value1, $value2 = null)
|
|
{
|
|
for ($i = 0; $i < 100; ++$i) {
|
|
$this->assertTrue(\in_array(CoreExtension::random('UTF-8', $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type
|
|
}
|
|
}
|
|
|
|
public static function getRandomFunctionTestData()
|
|
{
|
|
return [
|
|
'array' => [
|
|
['apple', 'orange', 'citrus'],
|
|
['apple', 'orange', 'citrus'],
|
|
],
|
|
'Traversable' => [
|
|
['apple', 'orange', 'citrus'],
|
|
new \ArrayObject(['apple', 'orange', 'citrus']),
|
|
],
|
|
'unicode string' => [
|
|
['Ä', '€', 'é'],
|
|
'Ä€é',
|
|
],
|
|
'numeric but string' => [
|
|
['1', '2', '3'],
|
|
'123',
|
|
],
|
|
'integer' => [
|
|
range(0, 5, 1),
|
|
5,
|
|
],
|
|
'float' => [
|
|
range(0, 5, 1),
|
|
5.9,
|
|
],
|
|
'negative' => [
|
|
[0, -1, -2],
|
|
-2,
|
|
],
|
|
'min max int' => [
|
|
range(50, 100),
|
|
50,
|
|
100,
|
|
],
|
|
'min max float' => [
|
|
range(-10, 10),
|
|
-9.5,
|
|
9.5,
|
|
],
|
|
'min null' => [
|
|
range(0, 100),
|
|
null,
|
|
100,
|
|
],
|
|
];
|
|
}
|
|
|
|
public function testRandomFunctionWithoutParameter()
|
|
{
|
|
$max = mt_getrandmax();
|
|
|
|
for ($i = 0; $i < 100; ++$i) {
|
|
$val = CoreExtension::random('UTF-8');
|
|
$this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max);
|
|
}
|
|
}
|
|
|
|
public function testRandomFunctionReturnsAsIs()
|
|
{
|
|
$this->assertSame('', CoreExtension::random('UTF-8', ''));
|
|
|
|
$instance = new \stdClass();
|
|
$this->assertSame($instance, CoreExtension::random('UTF-8', $instance));
|
|
}
|
|
|
|
public function testRandomFunctionOfEmptyArrayThrowsException()
|
|
{
|
|
$this->expectException(RuntimeError::class);
|
|
CoreExtension::random('UTF-8', []);
|
|
}
|
|
|
|
public function testRandomFunctionOnNonUTF8String()
|
|
{
|
|
$text = iconv('UTF-8', 'ISO-8859-1', 'Äé');
|
|
for ($i = 0; $i < 30; ++$i) {
|
|
$rand = CoreExtension::random('ISO-8859-1', $text);
|
|
$this->assertTrue(\in_array(iconv('ISO-8859-1', 'UTF-8', $rand), ['Ä', 'é'], true));
|
|
}
|
|
}
|
|
|
|
public function testReverseFilterOnNonUTF8String()
|
|
{
|
|
$input = iconv('UTF-8', 'ISO-8859-1', 'Äé');
|
|
$output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverse('ISO-8859-1', $input));
|
|
|
|
$this->assertEquals($output, 'éÄ');
|
|
}
|
|
|
|
#[DataProvider('provideTwigFirstCases')]
|
|
public function testTwigFirst($expected, $input)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::first('UTF-8', $input));
|
|
}
|
|
|
|
public static function provideTwigFirstCases()
|
|
{
|
|
$i = [1 => 'a', 2 => 'b', 3 => 'c'];
|
|
|
|
return [
|
|
['a', 'abc'],
|
|
[1, [1, 2, 3]],
|
|
['', null],
|
|
['', ''],
|
|
['a', new CoreTestIterator($i, array_keys($i), true, 3)],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideTwigLastCases')]
|
|
public function testTwigLast($expected, $input)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::last('UTF-8', $input));
|
|
}
|
|
|
|
public static function provideTwigLastCases()
|
|
{
|
|
$i = [1 => 'a', 2 => 'b', 3 => 'c'];
|
|
|
|
return [
|
|
['c', 'abc'],
|
|
[3, [1, 2, 3]],
|
|
['', null],
|
|
['', ''],
|
|
['c', new CoreTestIterator($i, array_keys($i), true)],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideArrayKeyCases')]
|
|
public function testArrayKeysFilter(array $expected, $input)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::keys($input));
|
|
}
|
|
|
|
public static function provideArrayKeyCases()
|
|
{
|
|
$array = ['a' => 'a1', 'b' => 'b1', 'c' => 'c1'];
|
|
$keys = array_keys($array);
|
|
|
|
return [
|
|
[$keys, $array],
|
|
[$keys, new CoreTestIterator($array, $keys)],
|
|
[$keys, new CoreTestIteratorAggregate($array, $keys)],
|
|
[$keys, new CoreTestIteratorAggregateAggregate($array, $keys)],
|
|
[[], null],
|
|
[['a'], new \SimpleXMLElement('<xml><a></a></xml>')],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideInFilterCases')]
|
|
public function testInFilter($expected, $value, $compare)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::inFilter($value, $compare));
|
|
}
|
|
|
|
public static function provideInFilterCases()
|
|
{
|
|
$array = [1, 2, 'a' => 3, 5, 6, 7];
|
|
$keys = array_keys($array);
|
|
|
|
return [
|
|
[true, 1, $array],
|
|
[true, '3', $array],
|
|
[true, '3', 'abc3def'],
|
|
[true, 1, new CoreTestIterator($array, $keys, true, 1)],
|
|
[true, '3', new CoreTestIterator($array, $keys, true, 3)],
|
|
[true, '3', new CoreTestIteratorAggregateAggregate($array, $keys, true, 3)],
|
|
[false, 4, $array],
|
|
[false, 4, new CoreTestIterator($array, $keys, true)],
|
|
[false, 4, new CoreTestIteratorAggregateAggregate($array, $keys, true)],
|
|
[false, 1, 1],
|
|
[true, 'b', new \SimpleXMLElement('<xml><a>b</a></xml>')],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideSliceFilterCases')]
|
|
public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::slice('UTF-8', $input, $start, $length, $preserveKeys));
|
|
}
|
|
|
|
public static function provideSliceFilterCases()
|
|
{
|
|
$i = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4];
|
|
$keys = array_keys($i);
|
|
|
|
return [
|
|
[['a' => 1], $i, 0, 1, true],
|
|
[['a' => 1], $i, 0, 1, false],
|
|
[['b' => 2, 'c' => 3], $i, 1, 2],
|
|
[[1], [1, 2, 3, 4], 0, 1],
|
|
[[2, 3], [1, 2, 3, 4], 1, 2],
|
|
[[2, 3], new CoreTestIterator($i, $keys, true), 1, 2],
|
|
[['c' => 3, 'd' => 4], new CoreTestIteratorAggregate($i, $keys, true), 2, null, true],
|
|
[$i, new CoreTestIterator($i, $keys, true), 0, \count($keys) + 10, true],
|
|
[[], new CoreTestIterator($i, $keys, true), \count($keys) + 10],
|
|
['de', 'abcdef', 3, 2],
|
|
[[], new \SimpleXMLElement('<items><item>1</item><item>2</item></items>'), 3],
|
|
[[], new \ArrayIterator([1, 2]), 3],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideCompareCases')]
|
|
public function testCompare($expected, $a, $b)
|
|
{
|
|
$this->assertSame($expected, CoreExtension::compare($a, $b));
|
|
$this->assertSame($expected, -CoreExtension::compare($b, $a));
|
|
}
|
|
|
|
public function testCompareNAN()
|
|
{
|
|
$this->assertSame(1, CoreExtension::compare(\NAN, 'NAN'));
|
|
$this->assertSame(1, CoreExtension::compare('NAN', \NAN));
|
|
$this->assertSame(1, CoreExtension::compare(\NAN, 'foo'));
|
|
$this->assertSame(1, CoreExtension::compare('foo', \NAN));
|
|
}
|
|
|
|
public static function provideCompareCases()
|
|
{
|
|
return [
|
|
[0, 'a', 'a'],
|
|
|
|
// from https://wiki.php.net/rfc/string_to_number_comparison
|
|
[0, 0, '0'],
|
|
[0, 0, '0.0'],
|
|
|
|
[-1, 0, 'foo'],
|
|
[1, 0, ''],
|
|
[0, 42, ' 42'],
|
|
[-1, 42, '42foo'],
|
|
|
|
[0, '0', '0'],
|
|
[0, '0', '0.0'],
|
|
[-1, '0', 'foo'],
|
|
[1, '0', ''],
|
|
[0, '42', ' 42'],
|
|
[-1, '42', '42foo'],
|
|
|
|
[0, 42, '000042'],
|
|
[0, 42, '42.0'],
|
|
[0, 42.0, '+42.0E0'],
|
|
[0, 0, '0e214987142012'],
|
|
|
|
[0, '42', '000042'],
|
|
[0, '42', '42.0'],
|
|
[0, '42.0', '+42.0E0'],
|
|
[0, '0', '0e214987142012'],
|
|
|
|
[0, 42, ' 42'],
|
|
[0, 42, '42 '],
|
|
[-1, 42, '42abc'],
|
|
[-1, 42, 'abc42'],
|
|
[-1, 0, 'abc42'],
|
|
|
|
[0, 42.0, ' 42.0'],
|
|
[0, 42.0, '42.0 '],
|
|
[-1, 42.0, '42.0abc'],
|
|
[-1, 42.0, 'abc42.0'],
|
|
[-1, 0.0, 'abc42.0'],
|
|
|
|
[0, \INF, 'INF'],
|
|
[0, -\INF, '-INF'],
|
|
[0, \INF, '1e1000'],
|
|
[0, -\INF, '-1e1000'],
|
|
|
|
[-1, 10, 20],
|
|
[-1, '10', 20],
|
|
[-1, 10, '20'],
|
|
|
|
[1, 42, ' foo'],
|
|
[0, 42, "42\f"],
|
|
[1, 42, "\x00\x34\x32"],
|
|
];
|
|
}
|
|
|
|
public function testSandboxedInclude()
|
|
{
|
|
$twig = new Environment(new ArrayLoader([
|
|
'index' => '{{ include("included", sandboxed: true) }}',
|
|
'included' => '{{ "included"|e }}',
|
|
]));
|
|
$policy = new SecurityPolicy(allowedFunctions: ['include']);
|
|
$sandbox = new SandboxExtension($policy, false);
|
|
$twig->addExtension($sandbox);
|
|
|
|
// We expect a compile error
|
|
$this->expectException(SecurityError::class);
|
|
$twig->render('index');
|
|
}
|
|
|
|
public function testSandboxedIncludeWithPreloadedTemplate()
|
|
{
|
|
$twig = new Environment(new ArrayLoader([
|
|
'index' => '{{ include("included", sandboxed: true) }}',
|
|
'included' => '{{ "included"|e }}',
|
|
]));
|
|
$policy = new SecurityPolicy(allowedFunctions: ['include']);
|
|
$sandbox = new SandboxExtension($policy, false);
|
|
$twig->addExtension($sandbox);
|
|
|
|
// The template is loaded without the sandbox enabled
|
|
// so, no compile error
|
|
$twig->load('included');
|
|
|
|
// We expect a runtime error
|
|
$this->expectException(SecurityError::class);
|
|
$twig->render('index');
|
|
}
|
|
}
|
|
|
|
final class CoreTestIteratorAggregate implements \IteratorAggregate
|
|
{
|
|
private $iterator;
|
|
|
|
public function __construct(array $array, array $keys, $allowAccess = false, $maxPosition = false)
|
|
{
|
|
$this->iterator = new CoreTestIterator($array, $keys, $allowAccess, $maxPosition);
|
|
}
|
|
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return $this->iterator;
|
|
}
|
|
}
|
|
|
|
final class CoreTestIteratorAggregateAggregate implements \IteratorAggregate
|
|
{
|
|
private $iterator;
|
|
|
|
public function __construct(array $array, array $keys, $allowValueAccess = false, $maxPosition = false)
|
|
{
|
|
$this->iterator = new CoreTestIteratorAggregate($array, $keys, $allowValueAccess, $maxPosition);
|
|
}
|
|
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return $this->iterator;
|
|
}
|
|
}
|
|
|
|
final class CoreTestIterator implements \Iterator
|
|
{
|
|
private $position;
|
|
private $array;
|
|
private $arrayKeys;
|
|
private $allowValueAccess;
|
|
private $maxPosition;
|
|
|
|
public function __construct(array $values, array $keys, $allowValueAccess = false, $maxPosition = false)
|
|
{
|
|
$this->array = $values;
|
|
$this->arrayKeys = $keys;
|
|
$this->position = 0;
|
|
$this->allowValueAccess = $allowValueAccess;
|
|
$this->maxPosition = false === $maxPosition ? \count($values) + 1 : $maxPosition;
|
|
}
|
|
|
|
public function rewind(): void
|
|
{
|
|
$this->position = 0;
|
|
}
|
|
|
|
public function current(): mixed
|
|
{
|
|
if ($this->allowValueAccess) {
|
|
return $this->array[$this->key()];
|
|
}
|
|
|
|
throw new \LogicException('Code should only use the keys, not the values provided by iterator.');
|
|
}
|
|
|
|
public function key(): mixed
|
|
{
|
|
return $this->arrayKeys[$this->position];
|
|
}
|
|
|
|
public function next(): void
|
|
{
|
|
++$this->position;
|
|
if ($this->position === $this->maxPosition) {
|
|
throw new \LogicException(\sprintf('Code should not iterate beyond %d.', $this->maxPosition));
|
|
}
|
|
}
|
|
|
|
public function valid(): bool
|
|
{
|
|
return isset($this->arrayKeys[$this->position]);
|
|
}
|
|
}
|