Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Utility / LinkGeneratorTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Utility;
4
5 use Drupal\Component\Render\MarkupInterface;
6 use Drupal\Core\GeneratedNoLink;
7 use Drupal\Core\GeneratedUrl;
8 use Drupal\Core\Language\Language;
9 use Drupal\Core\Link;
10 use Drupal\Core\Render\Markup;
11 use Drupal\Core\Url;
12 use Drupal\Core\Utility\LinkGenerator;
13 use Drupal\Tests\UnitTestCase;
14 use Drupal\Core\DependencyInjection\ContainerBuilder;
15
16 /**
17  * @coversDefaultClass \Drupal\Core\Utility\LinkGenerator
18  * @group Utility
19  */
20 class LinkGeneratorTest extends UnitTestCase {
21
22   /**
23    * The tested link generator.
24    *
25    * @var \Drupal\Core\Utility\LinkGenerator
26    */
27   protected $linkGenerator;
28
29   /**
30    * The mocked url generator.
31    *
32    * @var \PHPUnit_Framework_MockObject_MockObject
33    */
34   protected $urlGenerator;
35
36   /**
37    * The mocked module handler.
38    *
39    * @var \PHPUnit_Framework_MockObject_MockObject
40    */
41   protected $moduleHandler;
42
43   /**
44    * The mocked renderer service.
45    *
46    * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Render\RendererInterface
47    */
48   protected $renderer;
49
50   /**
51    * The mocked URL Assembler service.
52    *
53    * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Utility\UnroutedUrlAssemblerInterface
54    */
55   protected $urlAssembler;
56
57   /**
58    * Contains the LinkGenerator default options.
59    */
60   protected $defaultOptions = [
61     'query' => [],
62     'language' => NULL,
63     'set_active_class' => FALSE,
64     'absolute' => FALSE,
65   ];
66
67   /**
68    * {@inheritdoc}
69    */
70   protected function setUp() {
71     parent::setUp();
72
73     $this->urlGenerator = $this->getMockBuilder('\Drupal\Core\Routing\UrlGenerator')
74       ->disableOriginalConstructor()
75       ->getMock();
76     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
77     $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
78     $this->linkGenerator = new LinkGenerator($this->urlGenerator, $this->moduleHandler, $this->renderer);
79     $this->urlAssembler = $this->getMock('\Drupal\Core\Utility\UnroutedUrlAssemblerInterface');
80   }
81
82   /**
83    * Provides test data for testing the link method.
84    *
85    * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::testGenerateHrefs()
86    *
87    * @return array
88    *   Returns some test data.
89    */
90   public function providerTestGenerateHrefs() {
91     return [
92       // Test that the url returned by the URL generator is used.
93       ['test_route_1', [], FALSE, '/test-route-1'],
94         // Test that $parameters is passed to the URL generator.
95       ['test_route_2', ['value' => 'example'], FALSE, '/test-route-2/example'],
96         // Test that the 'absolute' option is passed to the URL generator.
97       ['test_route_3', [], TRUE, 'http://example.com/test-route-3'],
98     ];
99   }
100
101   /**
102    * Tests the link method with certain hrefs.
103    *
104    * @see \Drupal\Core\Utility\LinkGenerator::generate()
105    * @see \Drupal\Tests\Core\Utility\LinkGeneratorTest::providerTestGenerate()
106    *
107    * @dataProvider providerTestGenerateHrefs
108    */
109   public function testGenerateHrefs($route_name, array $parameters, $absolute, $expected_url) {
110     $this->urlGenerator->expects($this->once())
111       ->method('generateFromRoute')
112       ->with($route_name, $parameters, ['absolute' => $absolute] + $this->defaultOptions)
113       ->willReturn((new GeneratedUrl())->setGeneratedUrl($expected_url));
114     $this->moduleHandler->expects($this->once())
115       ->method('alter');
116
117     $url = new Url($route_name, $parameters, ['absolute' => $absolute]);
118     $url->setUrlGenerator($this->urlGenerator);
119     $result = $this->linkGenerator->generate('Test', $url);
120     $this->assertLink([
121       'attributes' => ['href' => $expected_url],
122       ], $result);
123   }
124
125   /**
126    * Tests the generate() method with a route.
127    *
128    * @covers ::generate
129    */
130   public function testGenerate() {
131     $this->urlGenerator->expects($this->once())
132       ->method('generateFromRoute')
133       ->with('test_route_1', [], ['fragment' => 'the-fragment'] + $this->defaultOptions)
134       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1#the-fragment'));
135
136     $this->moduleHandler->expects($this->once())
137       ->method('alter')
138       ->with('link', $this->isType('array'));
139
140     $url = new Url('test_route_1', [], ['fragment' => 'the-fragment']);
141     $url->setUrlGenerator($this->urlGenerator);
142
143     $result = $this->linkGenerator->generate('Test', $url);
144     $this->assertLink([
145       'attributes' => [
146         'href' => '/test-route-1#the-fragment',
147       ],
148       'content' => 'Test',
149     ], $result);
150   }
151
152   /**
153    * Tests the generate() method with the <nolink> route.
154    *
155    * @covers ::generate
156    */
157   public function testGenerateNoLink() {
158     $this->urlGenerator->expects($this->never())
159       ->method('generateFromRoute');
160     $this->moduleHandler->expects($this->once())
161       ->method('alter')
162       ->with('link', $this->isType('array'));
163
164     $url = Url::fromRoute('<nolink>');
165     $url->setUrlGenerator($this->urlGenerator);
166
167     $result = $this->linkGenerator->generate('Test', $url);
168     $this->assertTrue($result instanceof GeneratedNoLink);
169     $this->assertSame('<span>Test</span>', (string) $result);
170   }
171
172   /**
173    * Tests the generate() method with an external URL.
174    *
175    * The set_active_class option is set to TRUE to ensure this does not cause
176    * an error together with an external URL.
177    *
178    * @covers ::generate
179    */
180   public function testGenerateExternal() {
181     $this->urlAssembler->expects($this->once())
182       ->method('assemble')
183       ->with('https://www.drupal.org', ['set_active_class' => TRUE, 'external' => TRUE] + $this->defaultOptions)
184       ->will($this->returnArgument(0));
185
186     $this->moduleHandler->expects($this->once())
187       ->method('alter')
188       ->with('link', $this->isType('array'));
189
190     $this->urlAssembler->expects($this->once())
191       ->method('assemble')
192       ->with('https://www.drupal.org', ['set_active_class' => TRUE, 'external' => TRUE] + $this->defaultOptions)
193       ->willReturnArgument(0);
194
195     $url = Url::fromUri('https://www.drupal.org');
196     $url->setUrlGenerator($this->urlGenerator);
197     $url->setUnroutedUrlAssembler($this->urlAssembler);
198     $url->setOption('set_active_class', TRUE);
199
200     $result = $this->linkGenerator->generate('Drupal', $url);
201     $this->assertLink([
202       'attributes' => [
203         'href' => 'https://www.drupal.org',
204       ],
205       'content' => 'Drupal',
206     ], $result);
207   }
208
209   /**
210    * Tests the generate() method with a url containing double quotes.
211    *
212    * @covers ::generate
213    */
214   public function testGenerateUrlWithQuotes() {
215     $this->urlAssembler->expects($this->once())
216       ->method('assemble')
217       ->with('base:example', ['query' => ['foo' => '"bar"', 'zoo' => 'baz']] + $this->defaultOptions)
218       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/example?foo=%22bar%22&zoo=baz'));
219
220     $path_validator = $this->getMock('Drupal\Core\Path\PathValidatorInterface');
221     $container_builder = new ContainerBuilder();
222     $container_builder->set('path.validator', $path_validator);
223     \Drupal::setContainer($container_builder);
224
225     $path = '/example?foo="bar"&zoo=baz';
226     $url = Url::fromUserInput($path);
227     $url->setUrlGenerator($this->urlGenerator);
228     $url->setUnroutedUrlAssembler($this->urlAssembler);
229
230     $result = $this->linkGenerator->generate('Drupal', $url);
231
232     $this->assertLink([
233       'attributes' => [
234         'href' => '/example?foo=%22bar%22&zoo=baz',
235       ],
236       'content' => 'Drupal',
237     ], $result, 1);
238   }
239
240   /**
241    * Tests the link method with additional attributes.
242    *
243    * @see \Drupal\Core\Utility\LinkGenerator::generate()
244    */
245   public function testGenerateAttributes() {
246     $this->urlGenerator->expects($this->once())
247       ->method('generateFromRoute')
248       ->with('test_route_1', [], $this->defaultOptions)
249       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1'));
250
251     // Test that HTML attributes are added to the anchor.
252     $url = new Url('test_route_1', [], [
253       'attributes' => ['title' => 'Tooltip'],
254     ]);
255     $url->setUrlGenerator($this->urlGenerator);
256     $result = $this->linkGenerator->generate('Test', $url);
257     $this->assertLink([
258       'attributes' => [
259         'href' => '/test-route-1',
260         'title' => 'Tooltip',
261       ],
262     ], $result);
263   }
264
265   /**
266    * Tests the link method with passed query options.
267    *
268    * @see \Drupal\Core\Utility\LinkGenerator::generate()
269    */
270   public function testGenerateQuery() {
271     $this->urlGenerator->expects($this->once())
272       ->method('generateFromRoute')
273       ->with('test_route_1', [], ['query' => ['test' => 'value']] + $this->defaultOptions)
274       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
275
276     $url = new Url('test_route_1', [], [
277       'query' => ['test' => 'value'],
278     ]);
279     $url->setUrlGenerator($this->urlGenerator);
280     $result = $this->linkGenerator->generate('Test', $url);
281     $this->assertLink([
282       'attributes' => [
283         'href' => '/test-route-1?test=value',
284       ],
285     ], $result);
286   }
287
288   /**
289    * Tests the link method with passed query options via parameters.
290    *
291    * @see \Drupal\Core\Utility\LinkGenerator::generate()
292    */
293   public function testGenerateParametersAsQuery() {
294     $this->urlGenerator->expects($this->once())
295       ->method('generateFromRoute')
296       ->with('test_route_1', ['test' => 'value'], $this->defaultOptions)
297       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
298
299     $url = new Url('test_route_1', ['test' => 'value'], []);
300     $url->setUrlGenerator($this->urlGenerator);
301     $result = $this->linkGenerator->generate('Test', $url);
302     $this->assertLink([
303       'attributes' => [
304         'href' => '/test-route-1?test=value',
305       ],
306     ], $result);
307   }
308
309   /**
310    * Tests the link method with arbitrary passed options.
311    *
312    * @see \Drupal\Core\Utility\LinkGenerator::generate()
313    */
314   public function testGenerateOptions() {
315     $this->urlGenerator->expects($this->once())
316       ->method('generateFromRoute')
317       ->with('test_route_1', [], ['key' => 'value'] + $this->defaultOptions)
318       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-1?test=value'));
319     $url = new Url('test_route_1', [], [
320       'key' => 'value',
321     ]);
322     $url->setUrlGenerator($this->urlGenerator);
323     $result = $this->linkGenerator->generate('Test', $url);
324     $this->assertLink([
325       'attributes' => [
326         'href' => '/test-route-1?test=value',
327       ],
328     ], $result);
329   }
330
331   /**
332    * Tests the link method with a script tab.
333    *
334    * @see \Drupal\Core\Utility\LinkGenerator::generate()
335    */
336   public function testGenerateXss() {
337     $this->urlGenerator->expects($this->once())
338       ->method('generateFromRoute')
339       ->with('test_route_4', [], $this->defaultOptions)
340       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-4'));
341
342     // Test that HTML link text is escaped by default.
343     $url = new Url('test_route_4');
344     $url->setUrlGenerator($this->urlGenerator);
345     $result = $this->linkGenerator->generate("<script>alert('XSS!')</script>", $url);
346     $this->assertNoXPathResults('//a[@href="/test-route-4"]/script', $result);
347   }
348
349   /**
350    * Tests the link method with html.
351    *
352    * @see \Drupal\Core\Utility\LinkGenerator::generate()
353    */
354   public function testGenerateWithHtml() {
355     $this->urlGenerator->expects($this->at(0))
356       ->method('generateFromRoute')
357       ->with('test_route_5', [], $this->defaultOptions)
358       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-5'));
359     $this->urlGenerator->expects($this->at(1))
360       ->method('generateFromRoute')
361       ->with('test_route_5', [], $this->defaultOptions)
362       ->willReturn((new GeneratedUrl())->setGeneratedUrl('/test-route-5'));
363
364     // Test that HTML tags are stripped from the 'title' attribute.
365     $url = new Url('test_route_5', [], [
366       'attributes' => ['title' => '<em>HTML Tooltip</em>'],
367     ]);
368     $url->setUrlGenerator($this->urlGenerator);
369     $result = $this->linkGenerator->generate('Test', $url);
370     $this->assertLink([
371       'attributes' => [
372         'href' => '/test-route-5',
373         'title' => 'HTML Tooltip',
374       ],
375     ], $result);
376
377     // Test that safe HTML is output inside the anchor tag unescaped. The
378     // Markup::create() call is an intentional unit test for the interaction
379     // between MarkupInterface and the LinkGenerator.
380     $url = new Url('test_route_5', []);
381     $url->setUrlGenerator($this->urlGenerator);
382     $result = $this->linkGenerator->generate(Markup::create('<em>HTML output</em>'), $url);
383     $this->assertLink([
384       'attributes' => ['href' => '/test-route-5'],
385       'child' => [
386         'tag' => 'em',
387       ],
388     ], $result);
389     $this->assertTrue(strpos($result, '<em>HTML output</em>') !== FALSE);
390   }
391
392   /**
393    * Tests the active class on the link method.
394    *
395    * @see \Drupal\Core\Utility\LinkGenerator::generate()
396    */
397   public function testGenerateActive() {
398     $this->urlGenerator->expects($this->exactly(5))
399       ->method('generateFromRoute')
400       ->willReturnCallback(function ($name, $parameters = [], $options = [], $collect_bubbleable_metadata = FALSE) {
401         switch ($name) {
402           case 'test_route_1':
403             return (new GeneratedUrl())->setGeneratedUrl('/test-route-1');
404           case 'test_route_3':
405             return (new GeneratedUrl())->setGeneratedUrl('/test-route-3');
406           case 'test_route_4':
407             if ($parameters['object'] == '1') {
408               return (new GeneratedUrl())->setGeneratedUrl('/test-route-4/1');
409             }
410         }
411       });
412
413     $this->urlGenerator->expects($this->exactly(4))
414       ->method('getPathFromRoute')
415       ->will($this->returnValueMap([
416         ['test_route_1', [], 'test-route-1'],
417         ['test_route_3', [], 'test-route-3'],
418         ['test_route_4', ['object' => '1'], 'test-route-4/1'],
419       ]));
420
421     $this->moduleHandler->expects($this->exactly(5))
422       ->method('alter');
423
424     // Render a link.
425     $url = new Url('test_route_1', [], ['set_active_class' => TRUE]);
426     $url->setUrlGenerator($this->urlGenerator);
427     $result = $this->linkGenerator->generate('Test', $url);
428     $this->assertLink([
429       'attributes' => ['data-drupal-link-system-path' => 'test-route-1'],
430     ], $result);
431
432     // Render a link with the set_active_class option disabled.
433     $url = new Url('test_route_1', [], ['set_active_class' => FALSE]);
434     $url->setUrlGenerator($this->urlGenerator);
435     $result = $this->linkGenerator->generate('Test', $url);
436     $this->assertNoXPathResults('//a[@data-drupal-link-system-path="test-route-1"]', $result);
437
438     // Render a link with an associated language.
439     $url = new Url('test_route_1', [], [
440       'language' => new Language(['id' => 'de']),
441       'set_active_class' => TRUE,
442     ]);
443     $url->setUrlGenerator($this->urlGenerator);
444     $result = $this->linkGenerator->generate('Test', $url);
445     $this->assertLink([
446       'attributes' => [
447         'data-drupal-link-system-path' => 'test-route-1',
448         'hreflang' => 'de',
449       ],
450     ], $result);
451
452     // Render a link with a query parameter.
453     $url = new Url('test_route_3', [], [
454       'query' => ['value' => 'example_1'],
455       'set_active_class' => TRUE,
456     ]);
457     $url->setUrlGenerator($this->urlGenerator);
458     $result = $this->linkGenerator->generate('Test', $url);
459     $this->assertLink([
460       'attributes' => [
461         'data-drupal-link-system-path' => 'test-route-3',
462         'data-drupal-link-query' => '{"value":"example_1"}',
463       ],
464     ], $result);
465
466     // Render a link with route parameters and a query parameter.
467     $url = new Url('test_route_4', ['object' => '1'], [
468       'query' => ['value' => 'example_1'],
469       'set_active_class' => TRUE,
470     ]);
471     $url->setUrlGenerator($this->urlGenerator);
472     $result = $this->linkGenerator->generate('Test', $url);
473     $this->assertLink([
474       'attributes' => [
475         'data-drupal-link-system-path' => 'test-route-4/1',
476         'data-drupal-link-query' => '{"value":"example_1"}',
477       ],
478     ], $result);
479   }
480
481   /**
482    * Tests the LinkGenerator's support for collecting bubbleable metadata.
483    *
484    * @see \Drupal\Core\Utility\LinkGenerator::generate()
485    * @see \Drupal\Core\Utility\LinkGenerator::generateFromLink()
486    */
487   public function testGenerateBubbleableMetadata() {
488     $options = ['query' => [], 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE];
489     $this->urlGenerator->expects($this->any())
490       ->method('generateFromRoute')
491       ->will($this->returnValueMap([
492         ['test_route_1', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-1')],
493       ]));
494
495     $url = new Url('test_route_1');
496     $url->setUrlGenerator($this->urlGenerator);
497     $expected_link_markup = '<a href="/test-route-1">Test</a>';
498
499     // Test ::generate().
500     $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url));
501     $generated_link = $this->linkGenerator->generate('Test', $url);
502     $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
503     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
504
505     // Test ::generateFromLink().
506     $link = new Link('Test', $url);
507     $this->assertSame($expected_link_markup, (string) $this->linkGenerator->generateFromLink($link));
508     $generated_link = $this->linkGenerator->generateFromLink($link);
509     $this->assertSame($expected_link_markup, (string) $generated_link->getGeneratedLink());
510     $this->assertInstanceOf('\Drupal\Core\Render\BubbleableMetadata', $generated_link);
511   }
512
513   /**
514    * Tests altering the URL object using hook_link_alter().
515    *
516    * @covers ::generate
517    */
518   public function testGenerateWithAlterHook() {
519     $options = ['query' => [], 'language' => NULL, 'set_active_class' => FALSE, 'absolute' => FALSE];
520     $this->urlGenerator->expects($this->any())
521       ->method('generateFromRoute')
522       ->will($this->returnValueMap([
523         ['test_route_1', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-1')],
524         ['test_route_2', [], $options, TRUE, (new GeneratedUrl())->setGeneratedUrl('/test-route-2')],
525       ]));
526
527     $url = new Url('test_route_2');
528     $url->setUrlGenerator($this->urlGenerator);
529
530     $this->moduleHandler->expects($this->atLeastOnce())
531       ->method('alter')
532       ->willReturnCallback(function ($hook, &$options) {
533         $options['url'] = (new Url('test_route_1'))->setUrlGenerator($this->urlGenerator);
534       });
535
536     $expected_link_markup = '<a href="/test-route-1">Test</a>';
537     $this->assertEquals($expected_link_markup, (string) $this->linkGenerator->generate('Test', $url)->getGeneratedLink());
538   }
539
540   /**
541    * Tests whether rendering the same link twice works.
542    *
543    * This is a regression test for https://www.drupal.org/node/2842399.
544    */
545   public function testGenerateTwice() {
546     $this->urlGenerator->expects($this->any())
547       ->method('generateFromRoute')
548       ->will($this->returnValue((new GeneratedUrl())->setGeneratedUrl('/')));
549
550     $url = Url::fromRoute('<front>', [], ['attributes' => ['class' => ['foo', 'bar']]]);
551     $url->setUrlGenerator($this->urlGenerator);
552
553     $link = Link::fromTextAndUrl('text', $url);
554     $link->setLinkGenerator($this->linkGenerator);
555     $output = $link->toString() . $link->toString();
556     $this->assertEquals('<a href="/" class="foo bar">text</a><a href="/" class="foo bar">text</a>', $output);
557   }
558
559   /**
560    * Checks that a link with certain properties exists in a given HTML snippet.
561    *
562    * @param array $properties
563    *   An associative array of link properties, with the following keys:
564    *   - attributes: optional array of HTML attributes that should be present.
565    *   - content: optional link content.
566    * @param \Drupal\Component\Render\MarkupInterface $html
567    *   The HTML to check.
568    * @param int $count
569    *   How many times the link should be present in the HTML. Defaults to 1.
570    */
571   public static function assertLink(array $properties, MarkupInterface $html, $count = 1) {
572     // Provide default values.
573     $properties += ['attributes' => []];
574
575     // Create an XPath query that selects a link element.
576     $query = '//a';
577
578     // Append XPath predicates for the attributes and content text.
579     $predicates = [];
580     foreach ($properties['attributes'] as $attribute => $value) {
581       $predicates[] = "@$attribute='$value'";
582     }
583     if (!empty($properties['content'])) {
584       $predicates[] = "contains(.,'{$properties['content']}')";
585     }
586     if (!empty($predicates)) {
587       $query .= '[' . implode(' and ', $predicates) . ']';
588     }
589
590     // Execute the query.
591     $document = new \DOMDocument();
592     $document->loadHTML($html);
593     $xpath = new \DOMXPath($document);
594
595     self::assertEquals($count, $xpath->query($query)->length);
596   }
597
598   /**
599    * Checks that the given XPath query has no results in a given HTML snippet.
600    *
601    * @param string $query
602    *   The XPath query to execute.
603    * @param string $html
604    *   The HTML snippet to check.
605    *
606    * @return int
607    *   The number of results that are found.
608    */
609   protected function assertNoXPathResults($query, $html) {
610     $document = new \DOMDocument();
611     $document->loadHTML($html);
612     $xpath = new \DOMXPath($document);
613
614     self::assertFalse((bool) $xpath->query($query)->length);
615   }
616
617 }