Files
Twig/tests/Extension/CoreTest.php
T
Fabien Potencier 6beff61db4 Merge branch '3.x' into 4.x
* 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
2024-09-09 21:19:33 +02:00

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]);
}
}