1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12use Twig\Environment;
13
14class Twig_Tests_Extension_CoreTest extends \PHPUnit\Framework\TestCase
15{
16    /**
17     * @dataProvider getRandomFunctionTestData
18     */
19    public function testRandomFunction(array $expectedInArray, $value1, $value2 = null)
20    {
21        $env = new Twig_Environment($this->getMockBuilder('Twig_LoaderInterface')->getMock());
22        for ($i = 0; $i < 100; ++$i) {
23            $this->assertTrue(\in_array(twig_random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type
24        }
25    }
26
27    public function getRandomFunctionTestData()
28    {
29        return [
30            'array' => [
31                ['apple', 'orange', 'citrus'],
32                ['apple', 'orange', 'citrus'],
33            ],
34            'Traversable' => [
35                ['apple', 'orange', 'citrus'],
36                new ArrayObject(['apple', 'orange', 'citrus']),
37            ],
38            'unicode string' => [
39                ['Ä', '€', 'é'],
40                'Ä€é',
41            ],
42            'numeric but string' => [
43                ['1', '2', '3'],
44                '123',
45            ],
46            'integer' => [
47                range(0, 5, 1),
48                5,
49            ],
50            'float' => [
51                range(0, 5, 1),
52                5.9,
53            ],
54            'negative' => [
55                [0, -1, -2],
56                -2,
57            ],
58            'min max int' => [
59                range(50, 100),
60                50,
61                100,
62            ],
63            'min max float' => [
64                range(-10, 10),
65                -9.5,
66                9.5,
67            ],
68            'min null' => [
69                range(0, 100),
70                null,
71                100,
72            ],
73        ];
74    }
75
76    public function testRandomFunctionWithoutParameter()
77    {
78        $max = mt_getrandmax();
79
80        for ($i = 0; $i < 100; ++$i) {
81            $val = twig_random(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()));
82            $this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max);
83        }
84    }
85
86    public function testRandomFunctionReturnsAsIs()
87    {
88        $this->assertSame('', twig_random(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()), ''));
89        $this->assertSame('', twig_random(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['charset' => null]), ''));
90
91        $instance = new \stdClass();
92        $this->assertSame($instance, twig_random(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()), $instance));
93    }
94
95    /**
96     * @expectedException \Twig\Error\RuntimeError
97     */
98    public function testRandomFunctionOfEmptyArrayThrowsException()
99    {
100        twig_random(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()), []);
101    }
102
103    public function testRandomFunctionOnNonUTF8String()
104    {
105        if (!\function_exists('iconv') && !\function_exists('mb_convert_encoding')) {
106            $this->markTestSkipped('needs iconv or mbstring');
107        }
108
109        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
110        $twig->setCharset('ISO-8859-1');
111
112        $text = twig_convert_encoding('Äé', 'ISO-8859-1', 'UTF-8');
113        for ($i = 0; $i < 30; ++$i) {
114            $rand = twig_random($twig, $text);
115            $this->assertTrue(\in_array(twig_convert_encoding($rand, 'UTF-8', 'ISO-8859-1'), ['Ä', 'é'], true));
116        }
117    }
118
119    public function testReverseFilterOnNonUTF8String()
120    {
121        if (!\function_exists('iconv') && !\function_exists('mb_convert_encoding')) {
122            $this->markTestSkipped('needs iconv or mbstring');
123        }
124
125        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
126        $twig->setCharset('ISO-8859-1');
127
128        $input = twig_convert_encoding('Äé', 'ISO-8859-1', 'UTF-8');
129        $output = twig_convert_encoding(twig_reverse_filter($twig, $input), 'UTF-8', 'ISO-8859-1');
130
131        $this->assertEquals($output, 'éÄ');
132    }
133
134    /**
135     * @dataProvider provideCustomEscaperCases
136     */
137    public function testCustomEscaper($expected, $string, $strategy)
138    {
139        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
140        $twig->getExtension('\Twig\Extension\CoreExtension')->setEscaper('foo', 'foo_escaper_for_test');
141
142        $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
143    }
144
145    public function provideCustomEscaperCases()
146    {
147        return [
148            ['fooUTF-8', 'foo', 'foo'],
149            ['UTF-8', null, 'foo'],
150            ['42UTF-8', 42, 'foo'],
151        ];
152    }
153
154    /**
155     * @expectedException \Twig\Error\RuntimeError
156     */
157    public function testUnknownCustomEscaper()
158    {
159        twig_escape_filter(new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock()), 'foo', 'bar');
160    }
161
162    /**
163     * @dataProvider provideTwigFirstCases
164     */
165    public function testTwigFirst($expected, $input)
166    {
167        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
168        $this->assertSame($expected, twig_first($twig, $input));
169    }
170
171    public function provideTwigFirstCases()
172    {
173        $i = [1 => 'a', 2 => 'b', 3 => 'c'];
174
175        return [
176            ['a', 'abc'],
177            [1, [1, 2, 3]],
178            ['', null],
179            ['', ''],
180            ['a', new CoreTestIterator($i, array_keys($i), true, 3)],
181        ];
182    }
183
184    /**
185     * @dataProvider provideTwigLastCases
186     */
187    public function testTwigLast($expected, $input)
188    {
189        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
190        $this->assertSame($expected, twig_last($twig, $input));
191    }
192
193    public function provideTwigLastCases()
194    {
195        $i = [1 => 'a', 2 => 'b', 3 => 'c'];
196
197        return [
198            ['c', 'abc'],
199            [3, [1, 2, 3]],
200            ['', null],
201            ['', ''],
202            ['c', new CoreTestIterator($i, array_keys($i), true)],
203        ];
204    }
205
206    /**
207     * @dataProvider provideArrayKeyCases
208     */
209    public function testArrayKeysFilter(array $expected, $input)
210    {
211        $this->assertSame($expected, twig_get_array_keys_filter($input));
212    }
213
214    public function provideArrayKeyCases()
215    {
216        $array = ['a' => 'a1', 'b' => 'b1', 'c' => 'c1'];
217        $keys = array_keys($array);
218
219        return [
220            [$keys, $array],
221            [$keys, new CoreTestIterator($array, $keys)],
222            [$keys, new CoreTestIteratorAggregate($array, $keys)],
223            [$keys, new CoreTestIteratorAggregateAggregate($array, $keys)],
224            [[], null],
225            [['a'], new \SimpleXMLElement('<xml><a></a></xml>')],
226        ];
227    }
228
229    /**
230     * @dataProvider provideInFilterCases
231     */
232    public function testInFilter($expected, $value, $compare)
233    {
234        $this->assertSame($expected, twig_in_filter($value, $compare));
235    }
236
237    public function provideInFilterCases()
238    {
239        $array = [1, 2, 'a' => 3, 5, 6, 7];
240        $keys = array_keys($array);
241
242        return [
243            [true, 1, $array],
244            [true, '3', $array],
245            [true, '3', 'abc3def'],
246            [true, 1, new CoreTestIterator($array, $keys, true, 1)],
247            [true, '3', new CoreTestIterator($array, $keys, true, 3)],
248            [true, '3', new CoreTestIteratorAggregateAggregate($array, $keys, true, 3)],
249            [false, 4, $array],
250            [false, 4, new CoreTestIterator($array, $keys, true)],
251            [false, 4, new CoreTestIteratorAggregateAggregate($array, $keys, true)],
252            [false, 1, 1],
253            [true, 'b', new \SimpleXMLElement('<xml><a>b</a></xml>')],
254        ];
255    }
256
257    /**
258     * @dataProvider provideSliceFilterCases
259     */
260    public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false)
261    {
262        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
263        $this->assertSame($expected, twig_slice($twig, $input, $start, $length, $preserveKeys));
264    }
265
266    public function provideSliceFilterCases()
267    {
268        $i = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4];
269        $keys = array_keys($i);
270
271        return [
272            [['a' => 1], $i, 0, 1, true],
273            [['a' => 1], $i, 0, 1, false],
274            [['b' => 2, 'c' => 3], $i, 1, 2],
275            [[1], [1, 2, 3, 4], 0, 1],
276            [[2, 3], [1, 2, 3, 4], 1, 2],
277            [[2, 3], new CoreTestIterator($i, $keys, true), 1, 2],
278            [['c' => 3, 'd' => 4], new CoreTestIteratorAggregate($i, $keys, true), 2, null, true],
279            [$i, new CoreTestIterator($i, $keys, true), 0, \count($keys) + 10, true],
280            [[], new CoreTestIterator($i, $keys, true), \count($keys) + 10],
281            ['de', 'abcdef', 3, 2],
282            [[], new \SimpleXMLElement('<items><item>1</item><item>2</item></items>'), 3],
283            [[], new \ArrayIterator([1, 2]), 3],
284        ];
285    }
286}
287
288function foo_escaper_for_test(Environment $env, $string, $charset)
289{
290    return $string.$charset;
291}
292
293final class CoreTestIteratorAggregate implements \IteratorAggregate
294{
295    private $iterator;
296
297    public function __construct(array $array, array $keys, $allowAccess = false, $maxPosition = false)
298    {
299        $this->iterator = new CoreTestIterator($array, $keys, $allowAccess, $maxPosition);
300    }
301
302    public function getIterator()
303    {
304        return $this->iterator;
305    }
306}
307
308final class CoreTestIteratorAggregateAggregate implements \IteratorAggregate
309{
310    private $iterator;
311
312    public function __construct(array $array, array $keys, $allowValueAccess = false, $maxPosition = false)
313    {
314        $this->iterator = new CoreTestIteratorAggregate($array, $keys, $allowValueAccess, $maxPosition);
315    }
316
317    public function getIterator()
318    {
319        return $this->iterator;
320    }
321}
322
323final class CoreTestIterator implements Iterator
324{
325    private $position;
326    private $array;
327    private $arrayKeys;
328    private $allowValueAccess;
329    private $maxPosition;
330
331    public function __construct(array $values, array $keys, $allowValueAccess = false, $maxPosition = false)
332    {
333        $this->array = $values;
334        $this->arrayKeys = $keys;
335        $this->position = 0;
336        $this->allowValueAccess = $allowValueAccess;
337        $this->maxPosition = false === $maxPosition ? \count($values) + 1 : $maxPosition;
338    }
339
340    public function rewind()
341    {
342        $this->position = 0;
343    }
344
345    public function current()
346    {
347        if ($this->allowValueAccess) {
348            return $this->array[$this->key()];
349        }
350
351        throw new \LogicException('Code should only use the keys, not the values provided by iterator.');
352    }
353
354    public function key()
355    {
356        return $this->arrayKeys[$this->position];
357    }
358
359    public function next()
360    {
361        ++$this->position;
362        if ($this->position === $this->maxPosition) {
363            throw new \LogicException(sprintf('Code should not iterate beyond %d.', $this->maxPosition));
364        }
365    }
366
367    public function valid()
368    {
369        return isset($this->arrayKeys[$this->position]);
370    }
371}
372