diff --git a/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php b/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php index 7649c3f..221788e 100644 --- a/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php +++ b/core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php @@ -10,12 +10,26 @@ use \Symfony\Component\HttpFoundation\RedirectResponse; /** - * @todo Document. + * Provides a common base class for safe redirects. + * + * In case you want to redirect to external URLs use + * \Drupal\Core\Routing\TrustedRedirectResponse. + * + * For local URLs we use \Drupal\Core\Routing\LocalRedirectResponse which opts + * out of external redirects. */ abstract class SecuredRedirectResponse extends RedirectResponse { /** - * @todo Document. + * Copies an existing redirect response into a safe one. + * + * The safe one cannot accidentally redirect to an external URL, unless + * actively wanted (see \Drupal\Core\Routing\TrustedRedirectResponse). + * + * @param \Symfony\Component\HttpFoundation\RedirectResponse $response + * The original redirect. + * + * @return static */ public static function createFromRedirectResponse(RedirectResponse $response) { $safe_response = new static($response->getTargetUrl(), $response->getStatusCode(), $response->headers->allPreserveCase()); diff --git a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php index 6392903..26241a7 100644 --- a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php @@ -7,7 +7,7 @@ namespace Drupal\Core\EventSubscriber; -use Drupal\Component\HttpFoundation\SafeRedirectResponse; +use Drupal\Component\HttpFoundation\SecuredRedirectResponse; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Routing\LocalRedirectResponse; use Drupal\Core\Routing\RequestContext; @@ -56,7 +56,7 @@ public function checkRedirectUrl(FilterResponseEvent $event) { $request = $event->getRequest(); // Let the 'destination' query parameter override the redirect target. - // If $response is already a SafeRedirectResponse, it might reject the + // If $response is already a SecuredRedirectResponse, it might reject the // new target as invalid, in which case proceed with the old target. $destination = $request->query->get('destination'); if ($destination) { @@ -71,12 +71,13 @@ public function checkRedirectUrl(FilterResponseEvent $event) { // Regardless of whether the target is the original one or the overridden // destination, ensure that all redirects are safe. - if (!($response instanceOf SafeRedirectResponse)) { + if (!($response instanceOf SecuredRedirectResponse)) { try { - // SafeRedirectResponse is an abstract class that requires a + // SecuredRedirectResponse is an abstract class that requires a // concrete implementation. Default to LocalRedirectResponse, which // considers only redirects to within the same site as safe. $safe_response = LocalRedirectResponse::createFromRedirectResponse($response); + $safe_response->setRequestContext($this->requestContext); } catch (\InvalidArgumentException $e) { // If the above failed, it's because the redirect target wasn't diff --git a/core/lib/Drupal/Core/Routing/LocalRedirectResponse.php b/core/lib/Drupal/Core/Routing/LocalRedirectResponse.php index 9b06e74..06e3214 100644 --- a/core/lib/Drupal/Core/Routing/LocalRedirectResponse.php +++ b/core/lib/Drupal/Core/Routing/LocalRedirectResponse.php @@ -7,13 +7,13 @@ namespace Drupal\Core\Routing; -use Drupal\Component\HttpFoundation\SafeRedirectResponse; +use Drupal\Component\HttpFoundation\SecuredRedirectResponse; use Drupal\Component\Utility\UrlHelper; /** - * @todo Document + * Provides a redirect response which cannot redirect to an external URL. */ -class LocalRedirectResponse extends SafeRedirectResponse { +class LocalRedirectResponse extends SecuredRedirectResponse { /** * The request context. @@ -26,7 +26,7 @@ class LocalRedirectResponse extends SafeRedirectResponse { * {@inheritdoc} */ protected function isSafe($url) { - return UrlHelper::isExternal($url) && UrlHelper::externalIsLocal($url, $this->getRequestContext()->getCompleteBaseUrl()); + return !UrlHelper::isExternal($url) || UrlHelper::externalIsLocal($url, $this->getRequestContext()->getCompleteBaseUrl()); } /** @@ -41,4 +41,18 @@ protected function getRequestContext() { return $this->requestContext; } + /** + * Sets the request context. + * + * @param \Drupal\Core\Routing\RequestContext $request_context + * The request context. + * + * @return $this + */ + public function setRequestContext(RequestContext $request_context) { + $this->requestContext = $request_context; + + return $this; + } + } diff --git a/core/lib/Drupal/Core/Routing/TrustedRedirectResponse.php b/core/lib/Drupal/Core/Routing/TrustedRedirectResponse.php index c316e1e..5ce1c50 100644 --- a/core/lib/Drupal/Core/Routing/TrustedRedirectResponse.php +++ b/core/lib/Drupal/Core/Routing/TrustedRedirectResponse.php @@ -7,10 +7,15 @@ namespace Drupal\Core\Routing; +use Drupal\Component\HttpFoundation\SecuredRedirectResponse; + /** - * @todo Document + * Provides a redirect response which contains trusted URLs. + * + * Use this class in case you know that you want to redirect to an external + * URL. */ -class TrustedRedirectResponse extends LocalRedirectResponse { +class TrustedRedirectResponse extends SecuredRedirectResponse { /** * @todo Document. diff --git a/core/modules/field_ui/src/Tests/ManageFieldsTest.php b/core/modules/field_ui/src/Tests/ManageFieldsTest.php index fb4a5e6..24821e5 100644 --- a/core/modules/field_ui/src/Tests/ManageFieldsTest.php +++ b/core/modules/field_ui/src/Tests/ManageFieldsTest.php @@ -632,7 +632,7 @@ public function testExternalDestinations() { if ($assertion['status'] === 'exception') { // Ensure that the right error is thrown. $this->assertEqual('User error', $assertion['message_group']); - $this->assertEqual('Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\RedirectResponseAllowExternalUrl for it', $assertion['message']); + $this->assertEqual('Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.', $assertion['message']); // Finally remove that exception entry in order to not fail the test // itself. unset($this->assertions[$id]); diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php index 51568ca..ecc58d8 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Routing\RequestContext; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Tests\UnitTestCase; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -26,6 +27,31 @@ class RedirectResponseSubscriberTest extends UnitTestCase { /** + * The mocked request context. + * + * @var \Drupal\Core\Routing\RequestContext|\PHPUnit_Framework_MockObject_MockObject + */ + protected $requestContext; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->requestContext = $this->getMockBuilder('Drupal\Core\Routing\RequestContext') + ->disableOriginalConstructor() + ->getMock(); + $this->requestContext->expects($this->any()) + ->method('getCompleteBaseUrl') + ->willReturn('http://example.com/drupal'); + + $container = new Container(); + $container->set('router.request_context', $this->requestContext); + \Drupal::setContainer($container); + } + + /** * Test destination detection and redirection. * * @param Request $request @@ -58,15 +84,9 @@ public function testDestinationRedirect(Request $request, $expected) { ]); } - $request_context = $this->getMockBuilder('Drupal\Core\Routing\RequestContext') - ->disableOriginalConstructor() - ->getMock(); - $request_context->expects($this->any()) - ->method('getCompleteBaseUrl') - ->willReturn('http://example.com/drupal'); $request->headers->set('HOST', 'example.com'); - $listener = new RedirectResponseSubscriber($url_generator, $request_context); + $listener = new RedirectResponseSubscriber($url_generator, $this->requestContext); $dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'checkRedirectUrl')); $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response); $dispatcher->dispatch(KernelEvents::RESPONSE, $event); @@ -88,7 +108,6 @@ public function testDestinationRedirect(Request $request, $expected) { public static function providerTestDestinationRedirect() { return array( array(new Request(), FALSE), - array(new Request(array('destination' => 'http://example.ca/drupal')), FALSE), array(new Request(array('destination' => 'test')), 'http://example.com/drupal/test'), array(new Request(array('destination' => '/drupal/test')), 'http://example.com/drupal/test'), array(new Request(array('destination' => 'example.com')), 'http://example.com/drupal/example.com'), @@ -126,14 +145,6 @@ public function testDestinationRedirectToExternalUrl($request, $expected) { ]); } - $request_context = $this->getMockBuilder('Drupal\Core\Routing\RequestContext') - ->disableOriginalConstructor() - ->getMock(); - $request_context->expects($this->any()) - ->method('getCompleteBaseUrl') - ->willReturn('http://example.com/drupal'); - $request->headers->set('HOST', 'example.com'); - $listener = new RedirectResponseSubscriber($url_generator, $request_context); $dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'checkRedirectUrl')); $event = new FilterResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $response);