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 */ #[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 */ #[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 */ #[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 */ #[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 */ #[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('')], ]; } /** * @dataProvider provideInFilterCases */ #[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('b')], ]; } /** * @dataProvider provideSliceFilterCases */ #[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('12'), 3], [[], new \ArrayIterator([1, 2]), 3], ]; } /** * @dataProvider provideCompareCases */ #[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 testSandboxedIncludeResultStaysEscapedWhenAssigned() { $twig = new Environment(new ArrayLoader([ 'index' => "{% set body = include('included', sandboxed: true) %}[{{ body }}]", 'included' => '{{ evil }}', ]), ['autoescape' => 'html']); $twig->addExtension(new SandboxExtension(new SecurityPolicy([], ['escape'], [], [], ['include']), false)); $this->assertSame('[<script>]', $twig->render('index', ['evil' => '