Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Routing / UrlGeneratorTest.php
1 <?php
2
3 namespace Drupal\Tests\Core\Routing;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\DependencyInjection\ContainerBuilder;
7 use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
8 use Drupal\Core\PathProcessor\PathProcessorAlias;
9 use Drupal\Core\PathProcessor\PathProcessorManager;
10 use Drupal\Core\Render\BubbleableMetadata;
11 use Drupal\Core\Routing\RequestContext;
12 use Drupal\Core\Routing\RouteProviderInterface;
13 use Drupal\Core\Routing\UrlGenerator;
14 use Drupal\Tests\UnitTestCase;
15 use Prophecy\Argument;
16 use Symfony\Component\HttpFoundation\Request;
17 use Symfony\Component\HttpFoundation\RequestStack;
18 use Symfony\Component\Routing\Route;
19 use Symfony\Component\Routing\RouteCollection;
20
21 /**
22  * Confirm that the UrlGenerator is functioning properly.
23  *
24  * @coversDefaultClass \Drupal\Core\Routing\UrlGenerator
25  * @group Routing
26  */
27 class UrlGeneratorTest extends UnitTestCase {
28
29   /**
30    * The route provider.
31    *
32    * @var \Drupal\Core\Routing\RouteProviderInterface
33    */
34   protected $provider;
35
36   /**
37    * The url generator to test.
38    *
39    * @var \Drupal\Core\Routing\UrlGenerator
40    */
41   protected $generator;
42
43   /**
44    * The alias manager.
45    *
46    * @var \Drupal\Core\Path\AliasManager|\PHPUnit_Framework_MockObject_MockObject
47    */
48   protected $aliasManager;
49
50   /**
51    * The mock route processor manager.
52    *
53    * @var \Drupal\Core\RouteProcessor\RouteProcessorManager|\PHPUnit_Framework_MockObject_MockObject
54    */
55   protected $routeProcessorManager;
56
57   /**
58    * The request stack.
59    *
60    * @var \Symfony\Component\HttpFoundation\RequestStack
61    */
62   protected $requestStack;
63
64   /**
65    * The request context.
66    *
67    * @var \Drupal\Core\Routing\RequestContext
68    */
69   protected $context;
70
71   /**
72    * The path processor.
73    *
74    * @var \Drupal\Core\PathProcessor\PathProcessorManager
75    */
76   protected $processorManager;
77
78   /**
79    * {@inheritdoc}
80    */
81   protected function setUp() {
82     $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
83       ->disableOriginalConstructor()
84       ->getMock();
85     $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
86     $container = new ContainerBuilder();
87     $container->set('cache_contexts_manager', $cache_contexts_manager);
88     \Drupal::setContainer($container);
89
90     $routes = new RouteCollection();
91     $first_route = new Route('/test/one');
92     $second_route = new Route('/test/two/{narf}');
93     $third_route = new Route('/test/two/');
94     $fourth_route = new Route('/test/four', [], [], [], '', ['https']);
95     $none_route = new Route('', [], [], ['_no_path' => TRUE]);
96
97     $routes->add('test_1', $first_route);
98     $routes->add('test_2', $second_route);
99     $routes->add('test_3', $third_route);
100     $routes->add('test_4', $fourth_route);
101     $routes->add('<none>', $none_route);
102
103     // Create a route provider stub.
104     $provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider')
105       ->disableOriginalConstructor()
106       ->getMock();
107     // We need to set up return value maps for both the getRouteByName() and the
108     // getRoutesByNames() method calls on the route provider. The parameters
109     // are not passed in and default to an empty array.
110     $route_name_return_map = $routes_names_return_map = [];
111     $return_map_values = [
112       [
113         'route_name' => 'test_1',
114         'return' => $first_route,
115       ],
116       [
117         'route_name' => 'test_2',
118         'return' => $second_route,
119       ],
120       [
121         'route_name' => 'test_3',
122         'return' => $third_route,
123       ],
124       [
125         'route_name' => 'test_4',
126         'return' => $fourth_route,
127       ],
128       [
129         'route_name' => '<none>',
130         'return' => $none_route,
131       ],
132     ];
133     foreach ($return_map_values as $values) {
134       $route_name_return_map[] = [$values['route_name'], $values['return']];
135       $routes_names_return_map[] = [[$values['route_name']], $values['return']];
136     }
137     $this->provider = $provider;
138     $this->provider->expects($this->any())
139       ->method('getRouteByName')
140       ->will($this->returnValueMap($route_name_return_map));
141     $provider->expects($this->any())
142       ->method('getRoutesByNames')
143       ->will($this->returnValueMap($routes_names_return_map));
144
145     // Create an alias manager stub.
146     $alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager')
147       ->disableOriginalConstructor()
148       ->getMock();
149
150     $alias_manager->expects($this->any())
151       ->method('getAliasByPath')
152       ->will($this->returnCallback([$this, 'aliasManagerCallback']));
153
154     $this->aliasManager = $alias_manager;
155
156     $this->requestStack = new RequestStack();
157     $request = Request::create('/some/path');
158     $this->requestStack->push($request);
159
160     $this->context = new RequestContext();
161     $this->context->fromRequestStack($this->requestStack);
162
163     $processor = new PathProcessorAlias($this->aliasManager);
164     $processor_manager = new PathProcessorManager();
165     $processor_manager->addOutbound($processor, 1000);
166     $this->processorManager = $processor_manager;
167
168     $this->routeProcessorManager = $this->getMockBuilder('Drupal\Core\RouteProcessor\RouteProcessorManager')
169       ->disableOriginalConstructor()
170       ->getMock();
171
172     $generator = new UrlGenerator($this->provider, $processor_manager, $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
173     $generator->setContext($this->context);
174     $this->generator = $generator;
175   }
176
177   /**
178    * Return value callback for the getAliasByPath() method on the mock alias
179    * manager.
180    *
181    * Ensures that by default the call to getAliasByPath() will return the first
182    * argument that was passed in. We special-case the paths for which we wish it
183    * to return an actual alias.
184    *
185    * @return string
186    */
187   public function aliasManagerCallback() {
188     $args = func_get_args();
189     switch ($args[0]) {
190       case '/test/one':
191         return '/hello/world';
192       case '/test/two/5':
193         return '/goodbye/cruel/world';
194       case '/<front>':
195         return '/';
196       default:
197         return $args[0];
198     }
199   }
200
201   /**
202    * Confirms that generated routes will have aliased paths.
203    */
204   public function testAliasGeneration() {
205     $url = $this->generator->generate('test_1');
206     $this->assertEquals('/hello/world', $url);
207     // No cacheability to test; UrlGenerator::generate() doesn't support
208     // collecting cacheability metadata.
209
210     $this->routeProcessorManager->expects($this->exactly(3))
211       ->method('processOutbound')
212       ->with($this->anything());
213
214     // Check that the two generate methods return the same result.
215     $this->assertGenerateFromRoute('test_1', [], [], $url, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
216
217     $path = $this->generator->getPathFromRoute('test_1');
218     $this->assertEquals('test/one', $path);
219   }
220
221   /**
222    * Confirms that generated routes will have aliased paths using interface constants.
223    */
224   public function testAliasGenerationUsingInterfaceConstants() {
225     $url = $this->generator->generate('test_1', [], UrlGenerator::ABSOLUTE_PATH);
226     $this->assertEquals('/hello/world', $url);
227     // No cacheability to test; UrlGenerator::generate() doesn't support
228     // collecting cacheability metadata.
229
230     $this->routeProcessorManager->expects($this->exactly(3))
231       ->method('processOutbound')
232       ->with($this->anything());
233
234     // Check that the two generate methods return the same result.
235     $this->assertGenerateFromRoute('test_1', [], [], $url, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
236
237     $path = $this->generator->getPathFromRoute('test_1');
238     $this->assertEquals('test/one', $path);
239   }
240
241   /**
242    * @covers ::generateFromRoute
243    */
244   public function testUrlGenerationWithDisabledPathProcessing() {
245     $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
246     $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();
247
248     $generator = new UrlGenerator($this->provider, $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
249     $generator->setContext($this->context);
250
251     $url = $this->generator->generateFromRoute('test_1', [], ['path_processing' => FALSE]);
252     $this->assertEquals('/test/one', $url);
253   }
254
255   /**
256    * @covers ::generateFromRoute
257    */
258   public function testUrlGenerationWithDisabledPathProcessingByRoute() {
259     $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
260     $path_processor->processOutbound(Argument::cetera())->shouldNotBeCalled();
261
262     $provider = $this->prophesize(RouteProviderInterface::class);
263     $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));
264
265     $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
266     $generator->setContext($this->context);
267
268     $url = $generator->generateFromRoute('test_1', []);
269     $this->assertEquals('/test/one', $url);
270   }
271
272   /**
273    * @covers ::generateFromRoute
274    */
275   public function testUrlGenerationWithDisabledPathProcessingByRouteAndOptedInPathProcessing() {
276     $path_processor = $this->prophesize(OutboundPathProcessorInterface::class);
277     $path_processor->processOutbound('/test/one', Argument::cetera())->willReturn('/hello/world')->shouldBeCalled();
278
279     $provider = $this->prophesize(RouteProviderInterface::class);
280     $provider->getRouteByName('test_1')->willReturn(new Route('/test/one', [], [], ['default_url_options' => ['path_processing' => FALSE]]));
281
282     $generator = new UrlGenerator($provider->reveal(), $path_processor->reveal(), $this->routeProcessorManager, $this->requestStack, ['http', 'https']);
283     $generator->setContext($this->context);
284
285     $url = $generator->generateFromRoute('test_1', [], ['path_processing' => TRUE]);
286     $this->assertEquals('/hello/world', $url);
287   }
288
289   /**
290    * Tests URL generation in a subdirectory.
291    */
292   public function testGetPathFromRouteWithSubdirectory() {
293     $this->routeProcessorManager->expects($this->once())
294       ->method('processOutbound');
295
296     $path = $this->generator->getPathFromRoute('test_1');
297     $this->assertEquals('test/one', $path);
298   }
299
300   /**
301    * Confirms that generated routes will have aliased paths.
302    */
303   public function testAliasGenerationWithParameters() {
304     $url = $this->generator->generate('test_2', ['narf' => '5']);
305     $this->assertEquals('/goodbye/cruel/world', $url);
306     // No cacheability to test; UrlGenerator::generate() doesn't support
307     // collecting cacheability metadata.
308
309     $this->routeProcessorManager->expects($this->any())
310       ->method('processOutbound')
311       ->with($this->anything());
312
313     $options = ['fragment' => 'top'];
314     // Extra parameters should appear in the query string.
315     $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, '/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
316
317     $options = ['query' => ['page' => '1'], 'fragment' => 'bottom'];
318     $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
319
320     // Changing the parameters, the route still matches but there is no alias.
321     $this->assertGenerateFromRoute('test_2', ['narf' => 7], $options, '/test/two/7?page=1#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
322
323     $path = $this->generator->getPathFromRoute('test_2', ['narf' => '5']);
324     $this->assertEquals('test/two/5', $path);
325
326     // Specify a query parameter with NULL.
327     $options = ['query' => ['page' => NULL], 'fragment' => 'bottom'];
328     $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?page#bottom', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
329   }
330
331   /**
332    * Confirms that generated routes will have aliased paths with options.
333    *
334    * @dataProvider providerTestAliasGenerationWithOptions
335    */
336   public function testAliasGenerationWithOptions($route_name, $route_parameters, $options, $expected) {
337     $this->assertGenerateFromRoute($route_name, $route_parameters, $options, $expected, (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
338   }
339
340   /**
341    * Provides test data for testAliasGenerationWithOptions.
342    */
343   public function providerTestAliasGenerationWithOptions() {
344     $data = [];
345     // Extra parameters should appear in the query string.
346     $data[] = [
347       'test_1',
348       ['zoo' => '5'],
349       ['fragment' => 'top'],
350       '/hello/world?zoo=5#top',
351     ];
352     $data[] = [
353       'test_2',
354       ['narf' => '5'],
355       ['query' => ['page' => '1'], 'fragment' => 'bottom'],
356       '/goodbye/cruel/world?page=1#bottom',
357     ];
358     // Changing the parameters, the route still matches but there is no alias.
359     $data[] = [
360       'test_2',
361       ['narf' => '7'],
362       ['query' => ['page' => '1'], 'fragment' => 'bottom'],
363       '/test/two/7?page=1#bottom',
364     ];
365     // Query string values containing '/' should be decoded.
366     $data[] = [
367       'test_2',
368       ['narf' => '7'],
369       ['query' => ['page' => '1/2'], 'fragment' => 'bottom'],
370       '/test/two/7?page=1/2#bottom',
371     ];
372     // A NULL query string.
373     $data['query-with-NULL'] = [
374       'test_2',
375       ['narf' => '7'],
376       ['query' => NULL, 'fragment' => 'bottom'],
377       '/test/two/7#bottom',
378     ];
379     return $data;
380   }
381
382   /**
383    * Tests URL generation from route with trailing start and end slashes.
384    */
385   public function testGetPathFromRouteTrailing() {
386     $this->routeProcessorManager->expects($this->once())
387       ->method('processOutbound');
388
389     $path = $this->generator->getPathFromRoute('test_3');
390     $this->assertEquals($path, 'test/two');
391   }
392
393   /**
394    * Confirms that absolute URLs work with generated routes.
395    */
396   public function testAbsoluteURLGeneration() {
397     $url = $this->generator->generate('test_1', [], TRUE);
398     $this->assertEquals('http://localhost/hello/world', $url);
399     // No cacheability to test; UrlGenerator::generate() doesn't support
400     // collecting cacheability metadata.
401
402     $this->routeProcessorManager->expects($this->exactly(2))
403       ->method('processOutbound')
404       ->with($this->anything());
405
406     $options = ['absolute' => TRUE, 'fragment' => 'top'];
407     // Extra parameters should appear in the query string.
408     $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
409   }
410
411   /**
412    * Confirms that absolute URLs work with generated routes using interface constants.
413    */
414   public function testAbsoluteURLGenerationUsingInterfaceConstants() {
415     $url = $this->generator->generate('test_1', [], UrlGenerator::ABSOLUTE_URL);
416     $this->assertEquals('http://localhost/hello/world', $url);
417     // No cacheability to test; UrlGenerator::generate() doesn't support
418     // collecting cacheability metadata.
419
420     $this->routeProcessorManager->expects($this->exactly(2))
421       ->method('processOutbound')
422       ->with($this->anything());
423
424     $options = ['absolute' => TRUE, 'fragment' => 'top'];
425     // Extra parameters should appear in the query string.
426     $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://localhost/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
427   }
428
429   /**
430    * Confirms that explicitly setting the base_url works with generated routes
431    */
432   public function testBaseURLGeneration() {
433     $options = ['base_url' => 'http://www.example.com:8888'];
434     $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
435
436     $options = ['base_url' => 'http://www.example.com:8888', 'https' => TRUE];
437     $this->assertGenerateFromRoute('test_1', [], $options, 'https://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
438
439     $options = ['base_url' => 'https://www.example.com:8888', 'https' => FALSE];
440     $this->assertGenerateFromRoute('test_1', [], $options, 'http://www.example.com:8888/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
441
442     $this->routeProcessorManager->expects($this->exactly(2))
443       ->method('processOutbound')
444       ->with($this->anything());
445
446     $options = ['base_url' => 'http://www.example.com:8888', 'fragment' => 'top'];
447     // Extra parameters should appear in the query string.
448     $this->assertGenerateFromRoute('test_1', ['zoo' => 5], $options, 'http://www.example.com:8888/hello/world?zoo=5#top', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
449   }
450
451   /**
452    * Test that the 'scheme' route requirement is respected during url generation.
453    */
454   public function testUrlGenerationWithHttpsRequirement() {
455     $url = $this->generator->generate('test_4', [], TRUE);
456     $this->assertEquals('https://localhost/test/four', $url);
457     // No cacheability to test; UrlGenerator::generate() doesn't support
458     // collecting cacheability metadata.
459
460     $this->routeProcessorManager->expects($this->exactly(2))
461       ->method('processOutbound')
462       ->with($this->anything());
463
464     $options = ['absolute' => TRUE, 'https' => TRUE];
465     $this->assertGenerateFromRoute('test_1', [], $options, 'https://localhost/hello/world', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT)->setCacheContexts(['url.site']));
466   }
467
468   /**
469    * Tests generating a relative URL with no path.
470    *
471    * @param array $options
472    *   An array of URL options.
473    * @param string $expected_url
474    *   The expected relative URL.
475    *
476    * @covers ::generateFromRoute
477    *
478    * @dataProvider providerTestNoPath
479    */
480   public function testNoPath($options, $expected_url) {
481     $url = $this->generator->generateFromRoute('<none>', [], $options);
482     $this->assertEquals($expected_url, $url);
483   }
484
485   /**
486    * Data provider for ::testNoPath().
487    */
488   public function providerTestNoPath() {
489     return [
490       // Empty options.
491       [[], ''],
492       // Query parameters only.
493       [['query' => ['foo' => 'bar']], '?foo=bar'],
494       // Multiple query parameters.
495       [['query' => ['foo' => 'bar', 'baz' => '']], '?foo=bar&baz='],
496       // Fragment only.
497       [['fragment' => 'foo'], '#foo'],
498       // Query parameters and fragment.
499       [['query' => ['bar' => 'baz'], 'fragment' => 'foo'], '?bar=baz#foo'],
500       // Multiple query parameters and fragment.
501       [['query' => ['bar' => 'baz', 'foo' => 'bar'], 'fragment' => 'foo'], '?bar=baz&foo=bar#foo'],
502     ];
503   }
504
505   /**
506    * @covers \Drupal\Core\Routing\UrlGenerator::generateFromRoute
507    *
508    * Note: We use absolute covers to let
509    * \Drupal\Tests\Core\Render\MetadataBubblingUrlGeneratorTest work.
510    */
511   public function testGenerateWithPathProcessorChangingQueryParameter() {
512     $path_processor = $this->getMock(OutboundPathProcessorInterface::CLASS);
513     $path_processor->expects($this->atLeastOnce())
514       ->method('processOutbound')
515       ->willReturnCallback(function ($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
516         $options['query'] = ['zoo' => 5];
517         return $path;
518       });
519     $this->processorManager->addOutbound($path_processor);
520
521     $options = [];
522     $this->assertGenerateFromRoute('test_2', ['narf' => 5], $options, '/goodbye/cruel/world?zoo=5', (new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT));
523   }
524
525   /**
526    * Asserts \Drupal\Core\Routing\UrlGenerator::generateFromRoute()'s output.
527    *
528    * @param $route_name
529    *   The route name to test.
530    * @param array $route_parameters
531    *   The route parameters to test.
532    * @param array $options
533    *   The options to test.
534    * @param $expected_url
535    *   The expected generated URL string.
536    * @param \Drupal\Core\Render\BubbleableMetadata $expected_bubbleable_metadata
537    *   The expected generated bubbleable metadata.
538    */
539   protected function assertGenerateFromRoute($route_name, array $route_parameters, array $options, $expected_url, BubbleableMetadata $expected_bubbleable_metadata) {
540     // First, test with $collect_cacheability_metadata set to the default value.
541     $url = $this->generator->generateFromRoute($route_name, $route_parameters, $options);
542     $this->assertSame($expected_url, $url);
543
544     // Second, test with it set to TRUE.
545     $generated_url = $this->generator->generateFromRoute($route_name, $route_parameters, $options, TRUE);
546     $this->assertSame($expected_url, $generated_url->getGeneratedUrl());
547     $this->assertEquals($expected_bubbleable_metadata, BubbleableMetadata::createFromObject($generated_url));
548   }
549
550 }