3 namespace Drupal\Core\EventSubscriber;
5 use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
6 use Drupal\Component\Utility\UrlHelper;
7 use Drupal\Core\Routing\LocalRedirectResponse;
8 use Drupal\Core\Routing\RequestContext;
9 use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
10 use Symfony\Component\HttpFoundation\Response;
11 use Symfony\Component\HttpKernel\KernelEvents;
12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
13 use Symfony\Component\HttpFoundation\RedirectResponse;
14 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17 * Allows manipulation of the response object when performing a redirect.
19 class RedirectResponseSubscriber implements EventSubscriberInterface {
22 * The unrouted URL assembler service.
24 * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
26 protected $unroutedUrlAssembler;
29 * Constructs a RedirectResponseSubscriber object.
31 * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface $url_assembler
32 * The unrouted URL assembler service.
33 * @param \Drupal\Core\Routing\RequestContext $request_context
34 * The request context.
36 public function __construct(UnroutedUrlAssemblerInterface $url_assembler, RequestContext $request_context) {
37 $this->unroutedUrlAssembler = $url_assembler;
38 $this->requestContext = $request_context;
42 * Allows manipulation of the response object when performing a redirect.
44 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
45 * The Event to process.
47 public function checkRedirectUrl(FilterResponseEvent $event) {
48 $response = $event->getResponse();
49 if ($response instanceof RedirectResponse) {
50 $request = $event->getRequest();
52 // Let the 'destination' query parameter override the redirect target.
53 // If $response is already a SecuredRedirectResponse, it might reject the
54 // new target as invalid, in which case proceed with the old target.
55 $destination = $request->query->get('destination');
57 // The 'Location' HTTP header must always be absolute.
58 $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost());
60 $response->setTargetUrl($destination);
62 catch (\InvalidArgumentException $e) {
66 // Regardless of whether the target is the original one or the overridden
67 // destination, ensure that all redirects are safe.
68 if (!($response instanceof SecuredRedirectResponse)) {
70 // SecuredRedirectResponse is an abstract class that requires a
71 // concrete implementation. Default to LocalRedirectResponse, which
72 // considers only redirects to within the same site as safe.
73 $safe_response = LocalRedirectResponse::createFromRedirectResponse($response);
74 $safe_response->setRequestContext($this->requestContext);
76 catch (\InvalidArgumentException $e) {
77 // If the above failed, it's because the redirect target wasn't
78 // local. Do not follow that redirect. Display an error message
79 // instead. We're already catching one exception, so trigger_error()
80 // rather than throw another one.
81 // We don't throw an exception, because this is a client error rather than a
83 $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.';
84 trigger_error($message, E_USER_ERROR);
85 $safe_response = new Response($message, 400);
87 $event->setResponse($safe_response);
93 * Converts the passed in destination into an absolute URL.
95 * @param string $destination
96 * The path for the destination. In case it starts with a slash it should
97 * have the base path included already.
98 * @param string $scheme_and_host
99 * The scheme and host string of the current request.
102 * The destination as absolute URL.
104 protected function getDestinationAsAbsoluteUrl($destination, $scheme_and_host) {
105 if (!UrlHelper::isExternal($destination)) {
106 // The destination query parameter can be a relative URL in the sense of
107 // not including the scheme and host, but its path is expected to be
108 // absolute (start with a '/'). For such a case, prepend the scheme and
109 // host, because the 'Location' header must be absolute.
110 if (strpos($destination, '/') === 0) {
111 $destination = $scheme_and_host . $destination;
114 // Legacy destination query parameters can be internal paths that have
115 // not yet been converted to URLs.
116 $destination = UrlHelper::parse($destination);
117 $uri = 'base:' . $destination['path'];
119 'query' => $destination['query'],
120 'fragment' => $destination['fragment'],
123 // Treat this as if it's user input of a path relative to the site's
125 $destination = $this->unroutedUrlAssembler->assemble($uri, $options);
132 * Registers the methods in this class that should be listeners.
135 * An array of event listener definitions.
137 public static function getSubscribedEvents() {
138 $events[KernelEvents::RESPONSE][] = ['checkRedirectUrl'];