‪TYPO3CMS  11.5
RedirectService.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\ServerRequestInterface;
21 use Psr\Http\Message\UriInterface;
22 use Psr\Log\LoggerAwareInterface;
23 use Psr\Log\LoggerAwareTrait;
39 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
43 
49 class ‪RedirectService implements LoggerAwareInterface
50 {
51  use LoggerAwareTrait;
52 
56  protected ‪$redirectCacheService;
57 
61  protected ‪$linkService;
62 
66  protected ‪$siteFinder;
67 
69  {
70  $this->redirectCacheService = ‪$redirectCacheService;
71  $this->linkService = ‪$linkService;
72  $this->siteFinder = ‪$siteFinder;
73  }
74 
78  public function ‪matchRedirect(string $domain, string $path, string $query = ''): ?array
79  {
80  $path = rawurldecode($path);
81  // Check if the domain matches, or if there is a
82  // redirect fitting for any domain
83  foreach ([$domain, '*'] as $domainName) {
84  $redirects = $this->‪fetchRedirects($domainName);
85  if (empty($redirects)) {
86  continue;
87  }
88 
89  // check if a flat redirect matches with the Query applied
90  if (!empty($query)) {
91  $pathWithQuery = rtrim($path, '/') . '?' . ltrim($query, '?');
92  if (!empty($redirects['respect_query_parameters'][$pathWithQuery])) {
93  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['respect_query_parameters'][$pathWithQuery])) {
94  return $matchedRedirect;
95  }
96  } else {
97  $pathWithQueryAndSlash = rtrim($path, '/') . '/?' . ltrim($query, '?');
98  if (!empty($redirects['respect_query_parameters'][$pathWithQueryAndSlash])) {
99  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['respect_query_parameters'][$pathWithQueryAndSlash])) {
100  return $matchedRedirect;
101  }
102  }
103  }
104  }
105 
106  // check if a flat redirect matches
107  if (!empty($redirects['flat'][rtrim($path, '/') . '/'])) {
108  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['flat'][rtrim($path, '/') . '/'])) {
109  return $matchedRedirect;
110  }
111  }
112 
113  // check all regex redirects respecting query arguments
114  if (!empty($redirects['regexp_query_parameters'])) {
115  $allRegexps = array_keys($redirects['regexp_query_parameters']);
116  $regExpPath = $path;
117  if (!empty($query)) {
118  $regExpPath .= '?' . ltrim($query, '?');
119  }
120  foreach ($allRegexps as $regexp) {
121  $matchResult = @preg_match((string)$regexp, $regExpPath);
122  if ($matchResult > 0) {
123  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_query_parameters'][$regexp])) {
124  return $matchedRedirect;
125  }
126  continue;
127  }
128 
129  // Log invalid regular expression
130  if ($matchResult === false) {
131  $this->logger->warning('Invalid regex in redirect', ['regex' => $regexp]);
132  }
133  }
134  }
135 
136  // check all redirects that are registered as regex
137  if (!empty($redirects['regexp_flat'])) {
138  $allRegexps = array_keys($redirects['regexp_flat']);
139  $regExpPath = $path;
140  if (!empty($query)) {
141  $regExpPath .= '?' . ltrim($query, '?');
142  }
143  foreach ($allRegexps as $regexp) {
144  $matchResult = @preg_match((string)$regexp, $regExpPath);
145  if ($matchResult > 0) {
146  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_flat'][$regexp])) {
147  return $matchedRedirect;
148  }
149  continue;
150  }
151 
152  // Log invalid regular expression
153  if ($matchResult === false) {
154  $this->logger->warning('Invalid regex in redirect', ['regex' => $regexp]);
155  }
156  }
157 
158  // We need a second match run to evaluate against path only, even when query parameters where
159  // provided to ensure regexp without query parameters in mind are still processed.
160  // We need to do this only if there are query parameters in the request, otherwise first
161  // preg_match would have found it.
162  if (!empty($query)) {
163  foreach ($allRegexps as $regexp) {
164  $matchResult = preg_match((string)$regexp, $path);
165  if ($matchResult > 0) {
166  if ($matchedRedirect = $this->‪getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_flat'][$regexp])) {
167  return $matchedRedirect;
168  }
169  }
170  }
171  }
172  }
173  }
174 
175  return null;
176  }
177 
184  protected function ‪isRedirectActive(array $redirectRecord): bool
185  {
186  return !$redirectRecord['disabled'] && $redirectRecord['starttime'] <= ‪$GLOBALS['SIM_ACCESS_TIME'] &&
187  (!$redirectRecord['endtime'] || $redirectRecord['endtime'] >= ‪$GLOBALS['SIM_ACCESS_TIME']);
188  }
189 
194  protected function ‪fetchRedirects(string $sourceHost): array
195  {
196  return $this->redirectCacheService->getRedirects($sourceHost);
197  }
198 
205  protected function ‪resolveLinkDetailsFromLinkTarget(string $redirectTarget): array
206  {
207  try {
208  $linkDetails = $this->linkService->resolve($redirectTarget);
209  // Having the `typoLinkParameter` in the linkDetails is required, if the linkDetails are used to generate
210  // an url out of it. Therefore, this should be set in `getUriFromCustomLinkDetails()` before calling the
211  // LinkBuilder->build() method. We have a really tight execution context here, so we can safely set it here
212  // for now.
213  // @todo This simply reflects the used value to resolve the details. Other places in core set this to the
214  // array before building an url. This looks kind of unfinished. We should check, if we should not set
215  // that linkDetail value directly in the LinkService()->resolve() method generally.
216  $linkDetails['typoLinkParameter'] = $redirectTarget;
217  switch ($linkDetails['type']) {
219  // all set up, nothing to do
220  break;
223  $file = $linkDetails['file'];
224  if ($file instanceof ‪File) {
225  $linkDetails['url'] = $file->getPublicUrl();
226  }
227  break;
230  $folder = $linkDetails['folder'];
231  if ($folder instanceof ‪Folder) {
232  $linkDetails['url'] = $folder->getPublicUrl();
233  }
234  break;
236  // If $redirectTarget could not be resolved, we can only assume $redirectTarget with leading '/'
237  // as relative redirect and try to resolve it with enriched information from current request.
238  // That ensures that regexp redirects ending in replaceRegExpCaptureGroup(), but also ensures
239  // that relative urls are not left as unknown file here.
240  if (str_starts_with($redirectTarget, '/')) {
241  $linkDetails = [
242  'type' => ‪LinkService::TYPE_URL,
243  'url' => $redirectTarget,
244  ];
245  }
246  break;
247  default:
248  // we have to return the link details without having a "URL" parameter
249  }
250  } catch (‪InvalidPathException $e) {
251  return [];
252  }
253 
254  return $linkDetails;
255  }
256 
257  public function ‪getTargetUrl(array $matchedRedirect, ServerRequestInterface $request): ?UriInterface
258  {
259  $site = $request->getAttribute('site');
260  $uri = $request->getUri();
261  $queryParams = $request->getQueryParams();
262  $this->logger->debug('Found a redirect to process', ['redirect' => $matchedRedirect]);
263  $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode((string)$matchedRedirect['target']);
264  $redirectTarget = $linkParameterParts['url'];
265  $linkDetails = $this->‪resolveLinkDetailsFromLinkTarget($redirectTarget);
266  $this->logger->debug('Resolved link details for redirect', ['details' => $linkDetails]);
267  if (!empty($linkParameterParts['additionalParams']) && $matchedRedirect['keep_query_parameters']) {
268  $params = GeneralUtility::explodeUrl2Array($linkParameterParts['additionalParams']);
269  foreach ($params as $key => $value) {
270  $queryParams[$key] = $value;
271  }
272  }
273  // Do this for files, folders, external URLs or relative urls
274  if (!empty($linkDetails['url'])) {
275  if ($matchedRedirect['is_regexp'] ?? false) {
276  $linkDetails = $this->‪replaceRegExpCaptureGroup($matchedRedirect, $uri, $linkDetails);
277  }
278 
279  $url = new Uri($linkDetails['url']);
280  if ($matchedRedirect['force_https']) {
281  $url = $url->withScheme('https');
282  }
283  if ($matchedRedirect['keep_query_parameters']) {
284  $url = $this->‪addQueryParams($queryParams, $url);
285  }
286  return $url;
287  }
288  $site = $this->‪resolveSite($linkDetails, $site);
289  // If it's a record or page, then boot up TSFE and use typolink
290  return $this->‪getUriFromCustomLinkDetails(
291  $matchedRedirect,
292  $site,
293  $linkDetails,
294  $queryParams,
295  $request
296  );
297  }
298 
302  protected function ‪resolveSite(array $linkDetails, ?SiteInterface $site): ?SiteInterface
303  {
304  if (($site === null || $site instanceof NullSite) && ($linkDetails['type'] ?? '') === 'page') {
305  try {
306  return $this->siteFinder->getSiteByPageId((int)$linkDetails['pageuid']);
307  } catch (SiteNotFoundException $e) {
308  return new NullSite();
309  }
310  }
311  return $site;
312  }
313 
317  protected function ‪addQueryParams(array $queryParams, Uri $url): Uri
318  {
319  // New query parameters overrule the ones that should be kept
320  $newQueryParamString = $url->getQuery();
321  if (!empty($newQueryParamString)) {
322  $newQueryParams = [];
323  parse_str($newQueryParamString, $newQueryParams);
324  $queryParams = array_replace_recursive($queryParams, $newQueryParams);
325  }
326  $query = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
327  if ($query) {
328  $url = $url->withQuery($query);
329  }
330  return $url;
331  }
332 
336  protected function ‪getUriFromCustomLinkDetails(array $redirectRecord, ?SiteInterface $site, array $linkDetails, array $queryParams, ServerRequestInterface $originalRequest): ?UriInterface
337  {
338  if (!isset($linkDetails['type'], ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
339  return null;
340  }
341  if ($site === null || $site instanceof NullSite) {
342  return null;
343  }
344  $controller = $this->‪bootFrontendController($site, $queryParams, $originalRequest);
346  $linkBuilder = GeneralUtility::makeInstance(
347  ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
348  $controller->cObj,
349  $controller
350  );
351  try {
352  $configuration = [
353  'parameter' => (string)$redirectRecord['target'],
354  'forceAbsoluteUrl' => true,
355  'linkAccessRestrictedPages' => true,
356  ];
357  if ($redirectRecord['force_https']) {
358  $configuration['forceAbsoluteUrl.']['scheme'] = 'https';
359  }
360  if ($redirectRecord['keep_query_parameters']) {
361  $configuration['additionalParams'] = ‪HttpUtility::buildQueryString($queryParams, '&');
362  }
363  $result = $linkBuilder->build($linkDetails, '', '', $configuration);
364  $this->‪cleanupTSFE();
365  if (is_array($result)) {
366  return new Uri($result[0] ?? '');
367  }
368  if ($result instanceof LinkResultInterface) {
369  return new Uri($result->getUrl());
370  }
371  return null;
372  } catch (UnableToLinkException $e) {
373  // This exception is also thrown by the DatabaseRecordTypolinkBuilder
374  $url = $controller->cObj->lastTypoLinkUrl;
375  $this->‪cleanupTSFE();
376  if (!empty($url)) {
377  return new Uri($url);
378  }
379  return null;
380  }
381  }
382 
396  protected function ‪bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): TypoScriptFrontendController
397  {
398  $controller = GeneralUtility::makeInstance(
399  TypoScriptFrontendController::class,
400  GeneralUtility::makeInstance(Context::class),
401  $site,
402  $site->getDefaultLanguage(),
403  new PageArguments($site->getRootPageId(), '0', []),
404  $originalRequest->getAttribute('frontend.user')
405  );
406  $controller->determineId($originalRequest);
407  $controller->calculateLinkVars($queryParams);
408  $controller->getConfigArray();
409  $controller->newCObj($originalRequest);
410  if (!isset(‪$GLOBALS['TSFE']) || !‪$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
411  ‪$GLOBALS['TSFE'] = $controller;
412  }
413  if (!‪$GLOBALS['TSFE']->sys_page instanceof PageRepository) {
414  ‪$GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class);
415  }
416  return $controller;
417  }
418 
419  protected function ‪replaceRegExpCaptureGroup(array $matchedRedirect, UriInterface $uri, array $linkDetails): array
420  {
421  $uriToCheck = rawurldecode($uri->getPath());
422  if (($matchedRedirect['respect_query_parameters'] ?? false) && $uri->getQuery()) {
423  $uriToCheck .= '?' . rawurldecode($uri->getQuery());
424  }
425  $matchResult = preg_match($matchedRedirect['source_path'], $uriToCheck, $matches);
426  if ($matchResult > 0) {
427  foreach ($matches as $key => $val) {
428  // Unsafe regexp captching group may lead to adding query parameters to result url, which we need
429  // to prevent here, thus throwing everything beginning with ? away
430  if (strpos($val, '?') !== false) {
431  $val = explode('?', $val, 2)[0] ?? '';
432  $this->logger->warning(
433  sprintf(
434  'Unsafe captching group regex in redirect #%s, including query parameters in matched group',
435  $matchedRedirect['uid'] ?? 0
436  ),
437  ['regex' => $matchedRedirect['source_path']]
438  );
439  }
440  $linkDetails['url'] = str_replace('$' . $key, $val, $linkDetails['url']);
441  }
442  }
443  return $linkDetails;
444  }
445 
449  protected function ‪getFirstActiveRedirectFromPossibleRedirects(array $possibleRedirects): ?array
450  {
451  foreach ($possibleRedirects as $possibleRedirect) {
452  if ($this->‪isRedirectActive($possibleRedirect)) {
453  return $possibleRedirect;
454  }
455  }
456 
457  return null;
458  }
459 
465  private function ‪cleanupTSFE(): void
466  {
467  $context = GeneralUtility::makeInstance(Context::class);
468  $context->unsetAspect('language');
469  $context->unsetAspect('typoscript');
470  $context->unsetAspect('frontend.preview');
471  unset(‪$GLOBALS['TSFE']);
472  }
473 }
‪TYPO3\CMS\Redirects\Service\RedirectService\getFirstActiveRedirectFromPossibleRedirects
‪getFirstActiveRedirectFromPossibleRedirects(array $possibleRedirects)
Definition: RedirectService.php:446
‪TYPO3\CMS\Redirects\Service\RedirectService\getUriFromCustomLinkDetails
‪getUriFromCustomLinkDetails(array $redirectRecord, ?SiteInterface $site, array $linkDetails, array $queryParams, ServerRequestInterface $originalRequest)
Definition: RedirectService.php:333
‪TYPO3\CMS\Core\Site\Entity\SiteInterface
Definition: SiteInterface.php:26
‪TYPO3\CMS\Core\Routing\PageArguments
Definition: PageArguments.php:26
‪TYPO3\CMS\Core\Site\Entity\SiteInterface\getRootPageId
‪int getRootPageId()
‪TYPO3\CMS\Redirects\Service\RedirectService\$redirectCacheService
‪RedirectCacheService $redirectCacheService
Definition: RedirectService.php:55
‪TYPO3\CMS\Redirects\Service\RedirectService\isRedirectActive
‪bool isRedirectActive(array $redirectRecord)
Definition: RedirectService.php:181
‪TYPO3\CMS\Core\Site\Entity\NullSite
Definition: NullSite.php:32
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Redirects\Service\RedirectService
Definition: RedirectService.php:50
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Redirects\Service\RedirectService\cleanupTSFE
‪cleanupTSFE()
Definition: RedirectService.php:462
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Redirects\Service\RedirectService\getTargetUrl
‪getTargetUrl(array $matchedRedirect, ServerRequestInterface $request)
Definition: RedirectService.php:254
‪TYPO3\CMS\Core\Http\Uri\getQuery
‪string getQuery()
Definition: Uri.php:312
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:29
‪TYPO3\CMS\Redirects\Service\RedirectCacheService
Definition: RedirectCacheService.php:34
‪TYPO3\CMS\Redirects\Service\RedirectService\$linkService
‪LinkService $linkService
Definition: RedirectService.php:59
‪TYPO3\CMS\Redirects\Service\RedirectService\$siteFinder
‪SiteFinder $siteFinder
Definition: RedirectService.php:63
‪TYPO3\CMS\Redirects\Service\RedirectService\resolveLinkDetailsFromLinkTarget
‪array resolveLinkDetailsFromLinkTarget(string $redirectTarget)
Definition: RedirectService.php:202
‪TYPO3\CMS\Redirects\Service\RedirectService\__construct
‪__construct(RedirectCacheService $redirectCacheService, LinkService $linkService, SiteFinder $siteFinder)
Definition: RedirectService.php:65
‪TYPO3\CMS\Core\Site\Entity\SiteInterface\getDefaultLanguage
‪SiteLanguage getDefaultLanguage()
‪TYPO3\CMS\Core\Resource\Folder
Definition: Folder.php:37
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:171
‪TYPO3\CMS\Core\Resource\File
Definition: File.php:24
‪TYPO3\CMS\Redirects\Service\RedirectService\addQueryParams
‪addQueryParams(array $queryParams, Uri $url)
Definition: RedirectService.php:314
‪TYPO3\CMS\Redirects\Service\RedirectService\fetchRedirects
‪fetchRedirects(string $sourceHost)
Definition: RedirectService.php:191
‪TYPO3\CMS\Redirects\Service\RedirectService\matchRedirect
‪matchRedirect(string $domain, string $path, string $query='')
Definition: RedirectService.php:75
‪TYPO3\CMS\Redirects\Service
Definition: IntegrityService.php:18
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:104
‪TYPO3\CMS\Core\Http\Uri\withQuery
‪static withQuery($query)
Definition: Uri.php:506
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Redirects\Service\RedirectService\replaceRegExpCaptureGroup
‪replaceRegExpCaptureGroup(array $matchedRedirect, UriInterface $uri, array $linkDetails)
Definition: RedirectService.php:416
‪TYPO3\CMS\Redirects\Service\RedirectService\resolveSite
‪resolveSite(array $linkDetails, ?SiteInterface $site)
Definition: RedirectService.php:299
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:22
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Redirects\Service\RedirectService\bootFrontendController
‪bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest)
Definition: RedirectService.php:393
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Resource\Exception\InvalidPathException
Definition: InvalidPathException.php:23