Upgraded drupal core with security updates
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Template / TwigExtensionTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Template;
4
5 use Drupal\Core\GeneratedLink;
6 use Drupal\Core\Render\RenderableInterface;
7 use Drupal\Core\StringTranslation\TranslatableMarkup;
8 use Drupal\Core\Template\Loader\StringLoader;
9 use Drupal\Core\Template\TwigEnvironment;
10 use Drupal\Core\Template\TwigExtension;
11 use Drupal\Core\Url;
12 use Drupal\Tests\UnitTestCase;
13
14 /**
15  * Tests the twig extension.
16  *
17  * @group Template
18  *
19  * @coversDefaultClass \Drupal\Core\Template\TwigExtension
20  */
21 class TwigExtensionTest extends UnitTestCase {
22
23   /**
24    * The renderer.
25    *
26    * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
27    */
28   protected $renderer;
29
30   /**
31    * The url generator.
32    *
33    * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
34    */
35   protected $urlGenerator;
36
37   /**
38    * The theme manager.
39    *
40    * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
41    */
42   protected $themeManager;
43
44   /**
45    * The date formatter.
46    *
47    * @var \Drupal\Core\Datetime\DateFormatterInterface|\PHPUnit_Framework_MockObject_MockObject
48    */
49   protected $dateFormatter;
50
51   /**
52    * The system under test.
53    *
54    * @var \Drupal\Core\Template\TwigExtension
55    */
56   protected $systemUnderTest;
57
58   /**
59    * {@inheritdoc}
60    */
61   public function setUp() {
62     parent::setUp();
63
64     $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
65     $this->urlGenerator = $this->getMock('\Drupal\Core\Routing\UrlGeneratorInterface');
66     $this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
67     $this->dateFormatter = $this->getMock('\Drupal\Core\Datetime\DateFormatterInterface');
68
69     $this->systemUnderTest = new TwigExtension($this->renderer, $this->urlGenerator, $this->themeManager, $this->dateFormatter);
70   }
71
72   /**
73    * Tests the escaping
74    *
75    * @dataProvider providerTestEscaping
76    */
77   public function testEscaping($template, $expected) {
78     $twig = new \Twig_Environment(NULL, [
79       'debug' => TRUE,
80       'cache' => FALSE,
81       'autoescape' => 'html',
82       'optimizations' => 0,
83     ]);
84     $twig->addExtension($this->systemUnderTest);
85
86     $nodes = $twig->parse($twig->tokenize($template));
87
88     $this->assertSame($expected, $nodes->getNode('body')
89       ->getNode(0)
90       ->getNode('expr') instanceof \Twig_Node_Expression_Filter);
91   }
92
93   /**
94    * Provides tests data for testEscaping
95    *
96    * @return array
97    *   An array of test data each containing of a twig template string and
98    *   a boolean expecting whether the path will be safe.
99    */
100   public function providerTestEscaping() {
101     return [
102       ['{{ path("foo") }}', FALSE],
103       ['{{ path("foo", {}) }}', FALSE],
104       ['{{ path("foo", { foo: "foo" }) }}', FALSE],
105       ['{{ path("foo", foo) }}', TRUE],
106       ['{{ path("foo", { foo: foo }) }}', TRUE],
107       ['{{ path("foo", { foo: ["foo", "bar"] }) }}', TRUE],
108       ['{{ path("foo", { foo: "foo", bar: "bar" }) }}', TRUE],
109       ['{{ path(name = "foo", parameters = {}) }}', FALSE],
110       ['{{ path(name = "foo", parameters = { foo: "foo" }) }}', FALSE],
111       ['{{ path(name = "foo", parameters = foo) }}', TRUE],
112       [
113         '{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}',
114         TRUE
115       ],
116       ['{{ path(name = "foo", parameters = { foo: foo }) }}', TRUE],
117       [
118         '{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}',
119         TRUE
120       ],
121     ];
122   }
123
124   /**
125    * Tests the active_theme function.
126    */
127   public function testActiveTheme() {
128     $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
129       ->disableOriginalConstructor()
130       ->getMock();
131     $active_theme->expects($this->once())
132       ->method('getName')
133       ->willReturn('test_theme');
134     $this->themeManager->expects($this->once())
135       ->method('getActiveTheme')
136       ->willReturn($active_theme);
137
138     $loader = new \Twig_Loader_String();
139     $twig = new \Twig_Environment($loader);
140     $twig->addExtension($this->systemUnderTest);
141     $result = $twig->render('{{ active_theme() }}');
142     $this->assertEquals('test_theme', $result);
143   }
144
145   /**
146    * Tests the format_date filter.
147    */
148   public function testFormatDate() {
149     $this->dateFormatter->expects($this->exactly(2))
150       ->method('format')
151       ->willReturn('1978-11-19');
152
153     $loader = new StringLoader();
154     $twig = new \Twig_Environment($loader);
155     $twig->addExtension($this->systemUnderTest);
156     $result = $twig->render('{{ time|format_date("html_date") }}');
157     $this->assertEquals($this->dateFormatter->format('html_date'), $result);
158   }
159
160   /**
161    * Tests the active_theme_path function.
162    */
163   public function testActiveThemePath() {
164     $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
165       ->disableOriginalConstructor()
166       ->getMock();
167     $active_theme
168       ->expects($this->once())
169       ->method('getPath')
170       ->willReturn('foo/bar');
171     $this->themeManager->expects($this->once())
172       ->method('getActiveTheme')
173       ->willReturn($active_theme);
174
175     $loader = new \Twig_Loader_String();
176     $twig = new \Twig_Environment($loader);
177     $twig->addExtension($this->systemUnderTest);
178     $result = $twig->render('{{ active_theme_path() }}');
179     $this->assertEquals('foo/bar', $result);
180   }
181
182   /**
183    * Tests the escaping of objects implementing MarkupInterface.
184    *
185    * @covers ::escapeFilter
186    */
187   public function testSafeStringEscaping() {
188     $twig = new \Twig_Environment(NULL, [
189       'debug' => TRUE,
190       'cache' => FALSE,
191       'autoescape' => 'html',
192       'optimizations' => 0,
193     ]);
194
195     // By default, TwigExtension will attempt to cast objects to strings.
196     // Ensure objects that implement MarkupInterface are unchanged.
197     $safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface');
198     $this->assertSame($safe_string, $this->systemUnderTest->escapeFilter($twig, $safe_string, 'html', 'UTF-8', TRUE));
199
200     // Ensure objects that do not implement MarkupInterface are escaped.
201     $string_object = new TwigExtensionTestString("<script>alert('here');</script>");
202     $this->assertSame('&lt;script&gt;alert(&#039;here&#039;);&lt;/script&gt;', $this->systemUnderTest->escapeFilter($twig, $string_object, 'html', 'UTF-8', TRUE));
203   }
204
205   /**
206    * @covers ::safeJoin
207    */
208   public function testSafeJoin() {
209     $this->renderer->expects($this->any())
210       ->method('render')
211       ->with(['#markup' => '<strong>will be rendered</strong>', '#printed' => FALSE])
212       ->willReturn('<strong>will be rendered</strong>');
213
214     $twig_environment = $this->prophesize(TwigEnvironment::class)->reveal();
215
216     // Simulate t().
217     $markup = $this->prophesize(TranslatableMarkup::class);
218     $markup->__toString()->willReturn('<em>will be markup</em>');
219     $markup = $markup->reveal();
220
221     $items = [
222       '<em>will be escaped</em>',
223       $markup,
224       ['#markup' => '<strong>will be rendered</strong>'],
225     ];
226     $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br/>');
227     $this->assertEquals('&lt;em&gt;will be escaped&lt;/em&gt;<br/><em>will be markup</em><br/><strong>will be rendered</strong>', $result);
228
229     // Ensure safe_join Twig filter supports Traversable variables.
230     $items = new \ArrayObject([
231       '<em>will be escaped</em>',
232       $markup,
233       ['#markup' => '<strong>will be rendered</strong>'],
234     ]);
235     $result = $this->systemUnderTest->safeJoin($twig_environment, $items, ', ');
236     $this->assertEquals('&lt;em&gt;will be escaped&lt;/em&gt;, <em>will be markup</em>, <strong>will be rendered</strong>', $result);
237
238     // Ensure safe_join Twig filter supports empty variables.
239     $items = NULL;
240     $result = $this->systemUnderTest->safeJoin($twig_environment, $items, '<br>');
241     $this->assertEmpty($result);
242   }
243
244   /**
245    * @dataProvider providerTestRenderVar
246    */
247   public function testRenderVar($result, $input) {
248     $this->renderer->expects($this->any())
249       ->method('render')
250       ->with($result += ['#printed' => FALSE])
251       ->willReturn('Rendered output');
252
253     $this->assertEquals('Rendered output', $this->systemUnderTest->renderVar($input));
254   }
255
256   public function providerTestRenderVar() {
257     $data = [];
258
259     $renderable = $this->prophesize(RenderableInterface::class);
260     $render_array = ['#type' => 'test', '#var' => 'giraffe'];
261     $renderable->toRenderable()->willReturn($render_array);
262     $data['renderable'] = [$render_array, $renderable->reveal()];
263
264     return $data;
265   }
266
267   /**
268    * @covers ::escapeFilter
269    * @covers ::bubbleArgMetadata
270    */
271   public function testEscapeWithGeneratedLink() {
272     $twig = new \Twig_Environment(NULL, [
273         'debug' => TRUE,
274         'cache' => FALSE,
275         'autoescape' => 'html',
276         'optimizations' => 0,
277       ]
278     );
279
280     $twig->addExtension($this->systemUnderTest);
281     $link = new GeneratedLink();
282     $link->setGeneratedLink('<a href="http://example.com"></a>');
283     $link->addCacheTags(['foo']);
284     $link->addAttachments(['library' => ['system/base']]);
285
286     $this->renderer->expects($this->atLeastOnce())
287       ->method('render')
288       ->with([
289         "#cache" => [
290           "contexts" => [],
291           "tags" => ["foo"],
292           "max-age" => -1,
293         ],
294         "#attached" => ['library' => ['system/base']],
295       ]);
296     $result = $this->systemUnderTest->escapeFilter($twig, $link, 'html', NULL, TRUE);
297     $this->assertEquals('<a href="http://example.com"></a>', $result);
298   }
299
300   /**
301    * @covers ::renderVar
302    * @covers ::bubbleArgMetadata
303    */
304   public function testRenderVarWithGeneratedLink() {
305     $link = new GeneratedLink();
306     $link->setGeneratedLink('<a href="http://example.com"></a>');
307     $link->addCacheTags(['foo']);
308     $link->addAttachments(['library' => ['system/base']]);
309
310     $this->renderer->expects($this->atLeastOnce())
311       ->method('render')
312       ->with([
313         "#cache" => [
314           "contexts" => [],
315           "tags" => ["foo"],
316           "max-age" => -1,
317         ],
318         "#attached" => ['library' => ['system/base']],
319       ]);
320     $result = $this->systemUnderTest->renderVar($link);
321     $this->assertEquals('<a href="http://example.com"></a>', $result);
322   }
323
324   /**
325    * Tests creating attributes within a Twig template.
326    *
327    * @covers ::createAttribute
328    */
329   public function testCreateAttribute() {
330     $loader = new StringLoader();
331     $twig = new \Twig_Environment($loader);
332     $twig->addExtension($this->systemUnderTest);
333
334     $iterations = [
335       ['class' => ['kittens'], 'data-toggle' => 'modal', 'data-lang' => 'es'],
336       ['id' => 'puppies', 'data-value' => 'foo', 'data-lang' => 'en'],
337       [],
338     ];
339     $result = $twig->render("{% for iteration in iterations %}<div{{ create_attribute(iteration) }}></div>{% endfor %}", ['iterations' => $iterations]);
340     $expected = '<div class="kittens" data-toggle="modal" data-lang="es"></div><div id="puppies" data-value="foo" data-lang="en"></div><div></div>';
341     $this->assertEquals($expected, $result);
342
343     // Test default creation of empty attribute object and using its method.
344     $result = $twig->render("<div{{ create_attribute().addClass('meow') }}></div>");
345     $expected = '<div class="meow"></div>';
346     $this->assertEquals($expected, $result);
347   }
348
349   /**
350    * @covers ::getLink
351    */
352   public function testLinkWithOverriddenAttributes() {
353     $url = Url::fromRoute('<front>', [], ['attributes' => ['class' => ['foo']]]);
354
355     $build = $this->systemUnderTest->getLink('test', $url, ['class' => ['bar']]);
356
357     $this->assertEquals(['foo', 'bar'], $build['#url']->getOption('attributes')['class']);
358   }
359
360 }
361
362 class TwigExtensionTestString {
363
364   protected $string;
365
366   public function __construct($string) {
367     $this->string = $string;
368   }
369
370   public function __toString() {
371     return $this->string;
372   }
373
374 }