mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-24 15:16:26 +00:00
534 lines
15 KiB
PHP
534 lines
15 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\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\TestCase;
|
|
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
|
|
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
|
|
{
|
|
use ExpectDeprecationTrait;
|
|
|
|
/**
|
|
* @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');
|
|
}
|
|
|
|
public function testLastModified()
|
|
{
|
|
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
|
|
}
|
|
|
|
/**
|
|
* @group legacy
|
|
*/
|
|
public function testCycleWithArrayAccessAndTraversableButNotCountable()
|
|
{
|
|
$this->expectDeprecation('Since twig/twig 3.12: Passing a non-countable sequence of values to "Twig\Extension\CoreExtension::cycle()" is deprecated.');
|
|
|
|
$seq = new class implements \ArrayAccess, \IteratorAggregate {
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function offsetGet($offset): mixed
|
|
{
|
|
return 'val';
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
}
|
|
|
|
public function getIterator(): \Traversable
|
|
{
|
|
yield 'odd';
|
|
yield 'even';
|
|
}
|
|
};
|
|
|
|
$result = CoreExtension::cycle($seq, 0);
|
|
|
|
$this->assertEquals('odd', $result, 'cycle should return the first item from the traversable sequence, not the sequence itself.');
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function current()
|
|
{
|
|
if ($this->allowValueAccess) {
|
|
return $this->array[$this->key()];
|
|
}
|
|
|
|
throw new \LogicException('Code should only use the keys, not the values provided by iterator.');
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function key()
|
|
{
|
|
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]);
|
|
}
|
|
}
|