‪TYPO3CMS  11.5
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\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use Psr\Http\Message\UriInterface;
23 use Psr\Http\Server\MiddlewareInterface;
24 use Psr\Http\Server\RequestHandlerInterface;
25 use Psr\Log\LoggerAwareInterface;
26 use Psr\Log\LoggerAwareTrait;
34 
41 class ‪RedirectHandler implements MiddlewareInterface, LoggerAwareInterface
42 {
43  use LoggerAwareTrait;
44 
48  protected ‪$redirectService;
49 
51  {
52  $this->redirectService = ‪$redirectService;
53  }
54 
55  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
56  {
57  $port = $request->getUri()->getPort();
58  $matchedRedirect = $this->redirectService->matchRedirect(
59  $request->getUri()->getHost() . ($port ? ':' . $port : ''),
60  $request->getUri()->getPath(),
61  $request->getUri()->getQuery()
62  );
63 
64  // If the matched redirect is found, resolve it, and check further
65  if (is_array($matchedRedirect)) {
66  $url = $this->redirectService->getTargetUrl($matchedRedirect, $request);
67  if ($url instanceof UriInterface) {
68  if ($this->‪redirectUriWillRedirectToCurrentUri($request, $url)) {
69  if ($this->‪isEmptyRedirectUri($url)) {
70  // Empty uri leads to a redirect loop in Firefox, whereas Chrome would stop it but not displaying anything.
71  // @see https://forge.typo3.org/issues/100791
72  $this->logger->error('Empty redirect points to itself! Aborting.', ['record' => $matchedRedirect, 'uri' => (string)$url]);
73  } elseif ($url->getFragment()) {
74  // Enrich error message for unsharp check with target url fragment.
75  $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]);
76  } else {
77  $this->logger->error('Redirect ' . $url->getPath() . ' points to itself! Aborting.', ['record' => $matchedRedirect, 'uri' => (string)$url]);
78  }
79  return $handler->handle($request);
80  }
81  $this->logger->debug('Redirecting', ['record' => $matchedRedirect, 'uri' => (string)$url]);
82  $response = $this->‪buildRedirectResponse($url, $matchedRedirect);
83  $this->‪incrementHitCount($matchedRedirect);
84 
85  return $response;
86  }
87  }
88 
89  return $handler->handle($request);
90  }
91 
92  protected function ‪buildRedirectResponse(UriInterface $uri, array $redirectRecord): ResponseInterface
93  {
94  return new ‪RedirectResponse(
95  $uri,
96  (int)$redirectRecord['target_statuscode'],
97  ['X-Redirect-By' => 'TYPO3 Redirect ' . $redirectRecord['uid']]
98  );
99  }
100 
104  protected function ‪incrementHitCount(array $redirectRecord): void
105  {
106  // Track the hit if not disabled
107  if (!GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('redirects.hitCount') || $redirectRecord['disable_hitcount']) {
108  return;
109  }
110  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
111  ->getQueryBuilderForTable('sys_redirect');
112  $queryBuilder
113  ->update('sys_redirect')
114  ->where(
115  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($redirectRecord['uid'], ‪Connection::PARAM_INT))
116  )
117  ->set('hitcount', $queryBuilder->quoteIdentifier('hitcount') . '+1', false)
118  ->set('lasthiton', ‪$GLOBALS['EXEC_TIME'])
119  ->execute();
120  }
121 
125  protected function ‪redirectUriWillRedirectToCurrentUri(ServerRequestInterface $request, UriInterface $redirectUri): bool
126  {
127  if ($this->‪isEmptyRedirectUri($redirectUri)) {
128  return true;
129  }
130  $requestUri = $request->getUri();
131  $redirectIsAbsolute = $redirectUri->getHost() && $redirectUri->getScheme();
132  $requestUri = $this->‪sanitizeUriForComparison($requestUri, !$redirectIsAbsolute);
133  $redirectUri = $this->‪sanitizeUriForComparison($redirectUri, !$redirectIsAbsolute);
134  return (string)$requestUri === (string)$redirectUri;
135  }
136 
141  protected function ‪sanitizeUriForComparison(UriInterface $uri, bool $relativeCheck): UriInterface
142  {
143  // Remove schema, host and port if we need to sanitize for relative check.
144  if ($relativeCheck) {
145  $uri = $uri->withScheme('')->withHost('')->withPort(null);
146  }
147 
148  // Remove default port by schema, as they are superfluous and not meaningful enough, and even not
149  // set in a request uri as this depends a lot on the used webserver setup and infrastructure.
150  $portDefaultSchemaMap = [
151  // we only need web ports here, as web request could not be done over another
152  // schema at all, ex. ftp or mailto.
153  80 => 'http',
154  443 => 'https',
155  ];
156  if (
157  !$relativeCheck
158  && $uri->getScheme()
159  && isset($portDefaultSchemaMap[$uri->getPort()])
160  && $uri->getScheme() === $portDefaultSchemaMap[$uri->getPort()]
161  ) {
162  $uri = $uri->withPort(null);
163  }
164 
165  // Remove userinfo, as request would not hold it and so comparing would lead to a false-positive result
166  if ($uri->getUserInfo()) {
167  $uri = $uri->withUserInfo('');
168  }
169 
170  // Browser should and do not hand over the fragment part in a request as this is defined to be handled
171  // by clients only in the protocol, thus we remove the fragment to be safe and do not end in redirect loop
172  // for targets with fragments because we do not get it in the request. Still not optimal but the best we
173  // can do in this case.
174  if ($uri->getFragment()) {
175  $uri = $uri->withFragment('');
176  }
177 
178  // Query arguments do not have to be in the same order to be the same outcome, thus sorting them will
179  // give us a valid comparison, and we can correctly determine if we would have a redirect to the same uri.
180  // Arguments with empty values are kept, because removing them might lead to false-positives in some cases.
181  if ($uri->getQuery()) {
182  $parts = [];
183  parse_str($uri->getQuery(), $parts);
184  ksort($parts);
185  $uri = $uri->withQuery(‪HttpUtility::buildQueryString($parts));
186  }
187 
188  return $uri;
189  }
190 
195  private function ‪isEmptyRedirectUri(UriInterface $uri): bool
196  {
197  return (string)$uri === '';
198  }
199 }
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\redirectUriWillRedirectToCurrentUri
‪redirectUriWillRedirectToCurrentUri(ServerRequestInterface $request, UriInterface $redirectUri)
Definition: RedirectHandler.php:124
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler
Definition: RedirectHandler.php:42
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\incrementHitCount
‪incrementHitCount(array $redirectRecord)
Definition: RedirectHandler.php:103
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\sanitizeUriForComparison
‪sanitizeUriForComparison(UriInterface $uri, bool $relativeCheck)
Definition: RedirectHandler.php:140
‪TYPO3\CMS\Redirects\Service\RedirectService
Definition: RedirectService.php:50
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:171
‪TYPO3\CMS\Core\Configuration\Features
Definition: Features.php:56
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\$redirectService
‪RedirectService $redirectService
Definition: RedirectHandler.php:47
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: RedirectHandler.php:54
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\__construct
‪__construct(RedirectService $redirectService)
Definition: RedirectHandler.php:49
‪TYPO3\CMS\Core\Http\RedirectResponse
Definition: RedirectResponse.php:28
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Redirects\Http\Middleware
Definition: RedirectHandler.php:18
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\isEmptyRedirectUri
‪isEmptyRedirectUri(UriInterface $uri)
Definition: RedirectHandler.php:194
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:22
‪TYPO3\CMS\Redirects\Http\Middleware\RedirectHandler\buildRedirectResponse
‪buildRedirectResponse(UriInterface $uri, array $redirectRecord)
Definition: RedirectHandler.php:91
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50