mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-15 11:56:50 +00:00
28246c5d4e
* 3.x: Fixing minor typo in Update inline_css.rst Ignore static properties when using the dot operator Fix CS Fix constant() behavior when used with ?? Finish the work fix indentation Apply suggestions from code review typo fix merge docs and changelog typehint instead of checkArrow checkArrow, typehints invoke filter
465 lines
14 KiB
PHP
465 lines
14 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');
|
|
}
|
|
|
|
public function testLastModified()
|
|
{
|
|
$this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified());
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|