3 namespace Drupal\Tests\Core\EventSubscriber;
5 use Drupal\Core\EventSubscriber\RedirectResponseSubscriber;
6 use Drupal\Core\Routing\TrustedRedirectResponse;
7 use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
8 use Drupal\Tests\UnitTestCase;
9 use Symfony\Component\DependencyInjection\Container;
10 use Symfony\Component\EventDispatcher\EventDispatcher;
11 use Symfony\Component\HttpFoundation\RedirectResponse;
12 use Symfony\Component\HttpFoundation\Request;
13 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
14 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
15 use Symfony\Component\HttpKernel\HttpKernelInterface;
16 use Symfony\Component\HttpKernel\KernelEvents;
19 * @coversDefaultClass \Drupal\Core\EventSubscriber\RedirectResponseSubscriber
20 * @group EventSubscriber
22 class RedirectResponseSubscriberTest extends UnitTestCase {
25 * The mocked request context.
27 * @var \Drupal\Core\Routing\RequestContext|\PHPUnit_Framework_MockObject_MockObject
29 protected $requestContext;
32 * The mocked request context.
34 * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface|\PHPUnit_Framework_MockObject_MockObject
36 protected $urlAssembler;
41 protected function setUp() {
44 $this->requestContext = $this->getMockBuilder('Drupal\Core\Routing\RequestContext')
45 ->disableOriginalConstructor()
47 $this->requestContext->expects($this->any())
48 ->method('getCompleteBaseUrl')
49 ->willReturn('http://example.com/drupal');
51 $this->urlAssembler = $this->getMock(UnroutedUrlAssemblerInterface::class);
53 ->expects($this->any())
56 ['base:test', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/test'],
57 ['base:example.com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example.com'],
58 ['base:example:com', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/example:com'],
59 ['base:javascript:alert(0)', ['query' => [], 'fragment' => '', 'absolute' => TRUE], FALSE, 'http://example.com/drupal/javascript:alert(0)'],
62 $container = new Container();
63 $container->set('router.request_context', $this->requestContext);
64 \Drupal::setContainer($container);
68 * Test destination detection and redirection.
70 * @param \Symfony\Component\HttpFoundation\Request $request
71 * The request object with destination query set.
72 * @param string|bool $expected
73 * The expected target URL or FALSE.
75 * @covers ::checkRedirectUrl
76 * @dataProvider providerTestDestinationRedirect
78 public function testDestinationRedirect(Request $request, $expected) {
79 $dispatcher = new EventDispatcher();
80 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
81 $response = new RedirectResponse('http://example.com/drupal');
82 $request->headers->set('HOST', 'example.com');
84 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
85 $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
86 $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
87 $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
89 $target_url = $event->getResponse()->getTargetUrl();
91 $this->assertEquals($expected, $target_url);
94 $this->assertEquals('http://example.com/drupal', $target_url);
99 * Data provider for testDestinationRedirect().
101 * @see \Drupal\Tests\Core\EventSubscriber\RedirectResponseSubscriberTest::testDestinationRedirect()
103 public static function providerTestDestinationRedirect() {
105 [new Request(), FALSE],
106 [new Request(['destination' => 'test']), 'http://example.com/drupal/test'],
107 [new Request(['destination' => '/drupal/test']), 'http://example.com/drupal/test'],
108 [new Request(['destination' => 'example.com']), 'http://example.com/drupal/example.com'],
109 [new Request(['destination' => 'example:com']), 'http://example.com/drupal/example:com'],
110 [new Request(['destination' => 'javascript:alert(0)']), 'http://example.com/drupal/javascript:alert(0)'],
111 [new Request(['destination' => 'http://example.com/drupal/']), 'http://example.com/drupal/'],
112 [new Request(['destination' => 'http://example.com/drupal/test']), 'http://example.com/drupal/test'],
117 * @dataProvider providerTestDestinationRedirectToExternalUrl
119 public function testDestinationRedirectToExternalUrl($request, $expected) {
120 $dispatcher = new EventDispatcher();
121 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
122 $response = new RedirectResponse('http://other-example.com');
124 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
125 $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
126 $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
127 $this->setExpectedException(\PHPUnit_Framework_Error::class);
128 $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
132 * @covers ::checkRedirectUrl
134 public function testRedirectWithOptInExternalUrl() {
135 $dispatcher = new EventDispatcher();
136 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
137 $response = new TrustedRedirectResponse('http://external-url.com');
138 $request = Request::create('');
139 $request->headers->set('HOST', 'example.com');
141 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
142 $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
143 $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
144 $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
146 $target_url = $event->getResponse()->getTargetUrl();
147 $this->assertEquals('http://external-url.com', $target_url);
151 * Data provider for testDestinationRedirectToExternalUrl().
153 public function providerTestDestinationRedirectToExternalUrl() {
155 'absolute external url' => [new Request(['destination' => 'http://example.com']), 'http://example.com'],
156 'absolute external url with folder' => [new Request(['destination' => 'http://example.com/foobar']), 'http://example.com/foobar'],
157 'absolute external url with folder2' => [new Request(['destination' => 'http://example.ca/drupal']), 'http://example.ca/drupal'],
158 'path without drupal basepath' => [new Request(['destination' => '/test']), 'http://example.com/test'],
159 'path with URL' => [new Request(['destination' => '/example.com']), 'http://example.com/example.com'],
160 'path with URL and two slashes' => [new Request(['destination' => '//example.com']), 'http://example.com//example.com'],
165 * @dataProvider providerTestDestinationRedirectWithInvalidUrl
167 public function testDestinationRedirectWithInvalidUrl(Request $request) {
168 $dispatcher = new EventDispatcher();
169 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
170 $response = new RedirectResponse('http://example.com/drupal');
172 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
173 $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'checkRedirectUrl']);
174 $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);
175 $this->setExpectedException(\PHPUnit_Framework_Error::class);
176 $dispatcher->dispatch(KernelEvents::RESPONSE, $event);
180 * Data provider for testDestinationRedirectWithInvalidUrl().
182 public function providerTestDestinationRedirectWithInvalidUrl() {
184 $data[] = [new Request(['destination' => '//example:com'])];
185 $data[] = [new Request(['destination' => '//example:com/test'])];
186 $data['absolute external url'] = [new Request(['destination' => 'http://example.com'])];
187 $data['absolute external url with folder'] = [new Request(['destination' => 'http://example.ca/drupal'])];
188 $data['path without drupal basepath'] = [new Request(['destination' => '/test'])];
189 $data['path with URL'] = [new Request(['destination' => '/example.com'])];
190 $data['path with URL and two slashes'] = [new Request(['destination' => '//example.com'])];
196 * Tests that $_GET only contain internal URLs.
198 * @covers ::sanitizeDestination
200 * @dataProvider providerTestSanitizeDestination
202 * @see \Drupal\Component\Utility\UrlHelper::isExternal
204 public function testSanitizeDestinationForGet($input, $output) {
205 $request = new Request();
206 $request->query->set('destination', $input);
208 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
209 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
210 $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
212 $dispatcher = new EventDispatcher();
213 $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100);
214 $dispatcher->dispatch(KernelEvents::REQUEST, $event);
216 $this->assertEquals($output, $request->query->get('destination'));
220 * Tests that $_REQUEST['destination'] only contain internal URLs.
222 * @covers ::sanitizeDestination
224 * @dataProvider providerTestSanitizeDestination
226 * @see \Drupal\Component\Utility\UrlHelper::isExternal
228 public function testSanitizeDestinationForPost($input, $output) {
229 $request = new Request();
230 $request->request->set('destination', $input);
232 $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext);
233 $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
234 $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
236 $dispatcher = new EventDispatcher();
237 $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100);
238 $dispatcher->dispatch(KernelEvents::REQUEST, $event);
240 $this->assertEquals($output, $request->request->get('destination'));
244 * Data provider for testSanitizeDestination().
246 public function providerTestSanitizeDestination() {
248 // Standard internal example node path is present in the 'destination'
250 $data[] = ['node', 'node'];
251 // Internal path with one leading slash is allowed.
252 $data[] = ['/example.com', '/example.com'];
253 // External URL without scheme is not allowed.
254 $data[] = ['//example.com/test', ''];
255 // Internal URL using a colon is allowed.
256 $data[] = ['example:test', 'example:test'];
257 // External URL is not allowed.
258 $data[] = ['http://example.com', ''];
259 // Javascript URL is allowed because it is treated as an internal URL.
260 $data[] = ['javascript:alert(0)', 'javascript:alert(0)'];