RedirectResponseSubscriber.php 5.4 KB

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