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