‪TYPO3CMS  ‪main
RedirectHandler.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\ResponseFactoryInterface;
22 use Psr\Http\Message\ResponseInterface;
23 use Psr\Http\Message\ServerRequestInterface;
24 use Psr\Http\Message\UriInterface;
25 use Psr\Http\Server\MiddlewareInterface;
26 use Psr\Http\Server\RequestHandlerInterface;
27 use Psr\Log\LoggerInterface;
31 
38 class ‪RedirectHandler implements MiddlewareInterface
39 {
41  protected EventDispatcherInterface ‪$eventDispatcher;
42  protected ResponseFactoryInterface ‪$responseFactory;
43  protected LoggerInterface ‪$logger;
44 
45  public function ‪__construct(
47  EventDispatcherInterface ‪$eventDispatcher,
48  ResponseFactoryInterface ‪$responseFactory,
49  LoggerInterface ‪$logger
50  ) {
51  $this->redirectService = ‪$redirectService;
52  $this->eventDispatcher = ‪$eventDispatcher;
53  $this->responseFactory = ‪$responseFactory;
54  $this->logger = ‪$logger;
55  }
56 
57  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
58  {
59  $port = $request->getUri()->getPort();
60  ‪$matchedRedirect = $this->redirectService->matchRedirect(
61  $request->getUri()->getHost() . ($port ? ':' . $port : ''),
62  $request->getUri()->getPath(),
63  $request->getUri()->getQuery()
64  );
65 
66  // If the matched redirect is found, resolve it, and check further
67  if (!is_array(‪$matchedRedirect)) {
68  return $handler->handle($request);
69  }
70  ‪$url = $this->redirectService->getTargetUrl(‪$matchedRedirect, $request);
71  if (‪$url === null) {
72  return $handler->handle($request);
73  }
74  if ($this->‪redirectUriWillRedirectToCurrentUri($request, ‪$url)) {
75  if ($this->‪isEmptyRedirectUri(‪$url)) {
76  // Empty uri leads to a redirect loop in Firefox, whereas Chrome would stop it but not displaying anything.
77  // @see https://forge.typo3.org/issues/100791
78  $this->logger->error('Empty redirect points to itself! Aborting.', ['record' => ‪$matchedRedirect, 'uri' => (string)‪$url]);
79  } elseif (‪$url->getFragment()) {
80  // Enrich error message for unsharp check with target url fragment.
81  $this->logger->error('Redirect ' . ‪$url->getPath() . ' eventually points to itself! Target with fragment can not be checked and we take the safe check to avoid redirect loops. Aborting.', ['record' => ‪$matchedRedirect, 'uri' => (string)‪$url]);
82  } else {
83  $this->logger->error('Redirect ' . ‪$url->getPath() . ' points to itself! Aborting.', ['record' => ‪$matchedRedirect, 'uri' => (string)‪$url]);
84  }
85  return $handler->handle($request);
86  }
87  $this->logger->debug('Redirecting', ['record' => ‪$matchedRedirect, 'uri' => (string)‪$url]);
88  $response = $this->‪buildRedirectResponse($url, ‪$matchedRedirect);
89  // Dispatch event, allowing listeners to execute further tasks and to adjust the PSR-7 response
90  return $this->eventDispatcher->dispatch(
91  new ‪RedirectWasHitEvent($request, $response, ‪$matchedRedirect, ‪$url)
92  )->getResponse();
93  }
94 
95  protected function ‪buildRedirectResponse(UriInterface $uri, array $redirectRecord): ResponseInterface
96  {
97  return $this->responseFactory
98  ->createResponse((int)$redirectRecord['target_statuscode'])
99  ->withHeader('location', (string)$uri)
100  ->withHeader('X-Redirect-By', 'TYPO3 Redirect ' . $redirectRecord['uid']);
101  }
102 
106  protected function ‪redirectUriWillRedirectToCurrentUri(ServerRequestInterface $request, UriInterface $redirectUri): bool
107  {
108  if ($this->‪isEmptyRedirectUri($redirectUri)) {
109  return true;
110  }
111  $requestUri = $request->getUri();
112  $redirectIsAbsolute = $redirectUri->getHost() && $redirectUri->getScheme();
113  $requestUri = $this->‪sanitizeUriForComparison($requestUri, !$redirectIsAbsolute);
114  $redirectUri = $this->‪sanitizeUriForComparison($redirectUri, !$redirectIsAbsolute);
115  return (string)$requestUri === (string)$redirectUri;
116  }
117 
122  protected function ‪sanitizeUriForComparison(UriInterface $uri, bool $relativeCheck): UriInterface
123  {
124  // Remove schema, host and port if we need to sanitize for relative check.
125  if ($relativeCheck) {
126  $uri = $uri->withScheme('')->withHost('')->withPort(null);
127  }
128 
129  // Remove default port by schema, as they are superfluous and not meaningful enough, and even not
130  // set in a request uri as this depends a lot on the used webserver setup and infrastructure.
131  $portDefaultSchemaMap = [
132  // we only need web ports here, as web request could not be done over another
133  // schema at all, ex. ftp or mailto.
134  80 => 'http',
135  443 => 'https',
136  ];
137  if (
138  !$relativeCheck
139  && $uri->getScheme()
140  && isset($portDefaultSchemaMap[$uri->getPort()])
141  && $uri->getScheme() === $portDefaultSchemaMap[$uri->getPort()]
142  ) {
143  $uri = $uri->withPort(null);
144  }
145 
146  // Remove userinfo, as request would not hold it and so comparing would lead to a false-positive result
147  if ($uri->getUserInfo()) {
148  $uri = $uri->withUserInfo('');
149  }
150 
151  // Browser should and do not hand over the fragment part in a request as this is defined to be handled
152  // by clients only in the protocol, thus we remove the fragment to be safe and do not end in redirect loop
153  // for targets with fragments because we do not get it in the request. Still not optimal but the best we
154  // can do in this case.
155  if ($uri->getFragment()) {
156  $uri = $uri->withFragment('');
157  }
158 
159  // Query arguments do not have to be in the same order to be the same outcome, thus sorting them will
160  // give us a valid comparison, and we can correctly determine if we would have a redirect to the same uri.
161  // Arguments with empty values are kept, because removing them might lead to false-positives in some cases.
162  if ($uri->getQuery()) {
163  $parts = [];
164  parse_str($uri->getQuery(), $parts);
165  ksort($parts);
166  $uri = $uri->withQuery(‪HttpUtility::buildQueryString($parts));
167  }
168 
169  return $uri;
170  }
171 
176  private function ‪isEmptyRedirectUri(UriInterface $uri): bool
177  {
178  return (string)$uri === '';
179  }
180 }
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: RedirectHandler.php:41
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\redirectUriWillRedirectToCurrentUri
‪redirectUriWillRedirectToCurrentUri(ServerRequestInterface $request, UriInterface $redirectUri)
Definition: RedirectHandler.php:106
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler
Definition: RedirectHandler.php:39
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\sanitizeUriForComparison
‪sanitizeUriForComparison(UriInterface $uri, bool $relativeCheck)
Definition: RedirectHandler.php:122
‪TYPO3\CMS\Redirects\Service\RedirectService
Definition: RedirectService.php:53
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\$responseFactory
‪ResponseFactoryInterface $responseFactory
Definition: RedirectHandler.php:42
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:124
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\$redirectService
‪RedirectService $redirectService
Definition: RedirectHandler.php:40
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: RedirectHandler.php:57
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Redirects\Http\Middleware
Definition: RedirectHandler.php:18
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\__construct
‪__construct(RedirectService $redirectService, EventDispatcherInterface $eventDispatcher, ResponseFactoryInterface $responseFactory, LoggerInterface $logger)
Definition: RedirectHandler.php:45
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\isEmptyRedirectUri
‪isEmptyRedirectUri(UriInterface $uri)
Definition: RedirectHandler.php:176
‪TYPO3\CMS\Redirects\Message\$matchedRedirect
‪identifier readonly UriInterface readonly int readonly array $matchedRedirect
Definition: RedirectWasHitMessage.php:35
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:24
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\buildRedirectResponse
‪buildRedirectResponse(UriInterface $uri, array $redirectRecord)
Definition: RedirectHandler.php:95
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\$logger
‪LoggerInterface $logger
Definition: RedirectHandler.php:43
‪TYPO3\CMS\Redirects\Event\RedirectWasHitEvent
Definition: RedirectWasHitEvent.php:33