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 
12 use Twig\Extension\AbstractExtension;
13 use Twig\Extension\DebugExtension;
14 use Twig\Extension\SandboxExtension;
15 use Twig\Extension\StringLoaderExtension;
16 use Twig\Node\Expression\ConstantExpression;
17 use Twig\Node\PrintNode;
18 use Twig\Sandbox\SecurityPolicy;
19 use Twig\Test\IntegrationTestCase;
20 use Twig\Token;
21 use Twig\TokenParser\AbstractTokenParser;
22 use Twig\TwigFilter;
23 use Twig\TwigFunction;
24 use Twig\TwigTest;
25 
26 // This function is defined to check that escaping strategies
27 // like html works even if a function with the same name is defined.
28 function html()
29 {
30     return 'foo';
31 }
32 
33 class Twig_Tests_IntegrationTest extends IntegrationTestCase
34 {
35     public function getExtensions()
36     {
37         $policy = new SecurityPolicy([], [], [], [], ['dump']);
38 
39         return [
40             new DebugExtension(),
41             new SandboxExtension($policy, false),
42             new StringLoaderExtension(),
43             new TwigTestExtension(),
44         ];
45     }
46 
47     public function getFixturesDir()
48     {
49         return __DIR__.'/Fixtures/';
50     }
51 }
52 
53 function test_foo($value = 'foo')
54 {
55     return $value;
56 }
57 
58 class TwigTestFoo implements Iterator
59 {
60     const BAR_NAME = 'bar';
61 
62     public $position = 0;
63     public $array = [1, 2];
64 
65     public function bar($param1 = null, $param2 = null)
66     {
67         return 'bar'.($param1 ? '_'.$param1 : '').($param2 ? '-'.$param2 : '');
68     }
69 
70     public function getFoo()
71     {
72         return 'foo';
73     }
74 
75     public function getSelf()
76     {
77         return $this;
78     }
79 
80     public function is()
81     {
82         return 'is';
83     }
84 
85     public function in()
86     {
87         return 'in';
88     }
89 
90     public function not()
91     {
92         return 'not';
93     }
94 
95     public function strToLower($value)
96     {
97         return strtolower($value);
98     }
99 
100     public function rewind()
101     {
102         $this->position = 0;
103     }
104 
105     public function current()
106     {
107         return $this->array[$this->position];
108     }
109 
110     public function key()
111     {
112         return 'a';
113     }
114 
115     public function next()
116     {
117         ++$this->position;
118     }
119 
120     public function valid()
121     {
122         return isset($this->array[$this->position]);
123     }
124 }
125 
126 class TwigTestTokenParser_§ extends AbstractTokenParser
127 {
128     public function parse(Token $token)
129     {
130         $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
131 
132         return new PrintNode(new ConstantExpression('§', -1), -1);
133     }
134 
135     public function getTag()
136     {
137         return '§';
138     }
139 }
140 
141 class TwigTestExtension extends AbstractExtension
142 {
143     public function getTokenParsers()
144     {
145         return [
146             new TwigTestTokenParser_§(),
147         ];
148     }
149 
150     public function getFilters()
151     {
152         return [
153             new TwigFilter('§', [$this, '§Filter']),
154             new TwigFilter('escape_and_nl2br', [$this, 'escape_and_nl2br'], ['needs_environment' => true, 'is_safe' => ['html']]),
155             new TwigFilter('nl2br', [$this, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
156             new TwigFilter('escape_something', [$this, 'escape_something'], ['is_safe' => ['something']]),
157             new TwigFilter('preserves_safety', [$this, 'preserves_safety'], ['preserves_safety' => ['html']]),
158             new TwigFilter('static_call_string', 'TwigTestExtension::staticCall'),
159             new TwigFilter('static_call_array', ['TwigTestExtension', 'staticCall']),
160             new TwigFilter('magic_call', [$this, 'magicCall']),
161             new TwigFilter('magic_call_string', 'TwigTestExtension::magicStaticCall'),
162             new TwigFilter('magic_call_array', ['TwigTestExtension', 'magicStaticCall']),
163             new TwigFilter('*_path', [$this, 'dynamic_path']),
164             new TwigFilter('*_foo_*_bar', [$this, 'dynamic_foo']),
165         ];
166     }
167 
168     public function getFunctions()
169     {
170         return [
171             new TwigFunction('§', [$this, '§Function']),
172             new TwigFunction('safe_br', [$this, 'br'], ['is_safe' => ['html']]),
173             new TwigFunction('unsafe_br', [$this, 'br']),
174             new TwigFunction('static_call_string', 'TwigTestExtension::staticCall'),
175             new TwigFunction('static_call_array', ['TwigTestExtension', 'staticCall']),
176             new TwigFunction('*_path', [$this, 'dynamic_path']),
177             new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']),
178         ];
179     }
180 
181     public function getTests()
182     {
183         return [
184             new TwigTest('multi word', [$this, 'is_multi_word']),
185             new TwigTest('test_*', [$this, 'dynamic_test']),
186         ];
187     }
188 
189     public function §Filter($value)
190     {
191         return {$value}§";
192     }
193 
194     public function §Function($value)
195     {
196         return {$value}§";
197     }
198 
199     /**
200      * nl2br which also escapes, for testing escaper filters.
201      */
202     public function escape_and_nl2br($env, $value, $sep = '<br />')
203     {
204         return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep);
205     }
206 
207     /**
208      * nl2br only, for testing filters with pre_escape.
209      */
210     public function nl2br($value, $sep = '<br />')
211     {
212         // not secure if $value contains html tags (not only entities)
213         // don't use
214         return str_replace("\n", "$sep\n", $value);
215     }
216 
217     public function dynamic_path($element, $item)
218     {
219         return $element.'/'.$item;
220     }
221 
222     public function dynamic_foo($foo, $bar, $item)
223     {
224         return $foo.'/'.$bar.'/'.$item;
225     }
226 
227     public function dynamic_test($element, $item)
228     {
229         return $element === $item;
230     }
231 
232     public function escape_something($value)
233     {
234         return strtoupper($value);
235     }
236 
237     public function preserves_safety($value)
238     {
239         return strtoupper($value);
240     }
241 
242     public static function staticCall($value)
243     {
244         return "*$value*";
245     }
246 
247     public function br()
248     {
249         return '<br />';
250     }
251 
252     public function is_multi_word($value)
253     {
254         return false !== strpos($value, ' ');
255     }
256 
257     public function __call($method, $arguments)
258     {
259         if ('magicCall' !== $method) {
260             throw new \BadMethodCallException('Unexpected call to __call');
261         }
262 
263         return 'magic_'.$arguments[0];
264     }
265 
266     public static function __callStatic($method, $arguments)
267     {
268         if ('magicStaticCall' !== $method) {
269             throw new \BadMethodCallException('Unexpected call to __callStatic');
270         }
271 
272         return 'static_magic_'.$arguments[0];
273     }
274 }
275 
276 /**
277  * This class is used in tests for the "length" filter and "empty" test. It asserts that __call is not
278  * used to convert such objects to strings.
279  */
280 class MagicCallStub
281 {
282     public function __call($name, $args)
283     {
284         throw new \Exception('__call shall not be called');
285     }
286 }
287 
288 class ToStringStub
289 {
290     /**
291      * @var string
292      */
293     private $string;
294 
295     public function __construct($string)
296     {
297         $this->string = $string;
298     }
299 
300     public function __toString()
301     {
302         return $this->string;
303     }
304 }
305 
306 /**
307  * This class is used in tests for the length filter and empty test to show
308  * that when \Countable is implemented, it is preferred over the __toString()
309  * method.
310  */
311 class CountableStub implements \Countable
312 {
313     private $count;
314 
315     public function __construct($count)
316     {
317         $this->count = $count;
318     }
319 
320     public function count()
321     {
322         return $this->count;
323     }
324 
325     public function __toString()
326     {
327         throw new \Exception('__toString shall not be called on \Countables');
328     }
329 }
330 
331 /**
332  * This class is used in tests for the length filter.
333  */
334 class IteratorAggregateStub implements \IteratorAggregate
335 {
336     private $data;
337 
338     public function __construct(array $data)
339     {
340         $this->data = $data;
341     }
342 
343     public function getIterator()
344     {
345         return new \ArrayIterator($this->data);
346     }
347 }
348