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