[BUGFIX] Catch possible error due to wrong redirect regex
[Packages/TYPO3.CMS.git] / typo3 / sysext / redirects / Classes / Service / RedirectService.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Redirects\Service;
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
18 use Psr\Http\Message\UriInterface;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
21 use TYPO3\CMS\Core\Http\Uri;
22 use TYPO3\CMS\Core\LinkHandling\LinkService;
23 use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
24 use TYPO3\CMS\Core\Resource\File;
25 use TYPO3\CMS\Core\Resource\Folder;
26 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\HttpUtility;
29 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
30 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
31 use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
32 use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
33
34 /**
35 * Creates a proper URL to redirect from a matched redirect of a request
36 *
37 * @internal due to some possible refactorings in TYPO3 v9
38 */
39 class RedirectService implements LoggerAwareInterface
40 {
41 use LoggerAwareTrait;
42
43 /**
44 * Checks against all available redirects "flat" or "regexp", and against starttime/endtime
45 *
46 * @param string $domain
47 * @param string $path
48 * @param string $query
49 * @return array|null
50 */
51 public function matchRedirect(string $domain, string $path, string $query = '')
52 {
53 $allRedirects = $this->fetchRedirects();
54 // Check if the domain matches, or if there is a
55 // redirect fitting for any domain
56 foreach ([$domain, '*'] as $domainName) {
57 if (empty($allRedirects[$domainName])) {
58 continue;
59 }
60
61 $possibleRedirects = [];
62 // check if a flat redirect matches
63 if (!empty($allRedirects[$domainName]['flat'][rtrim($path, '/') . '/'])) {
64 $possibleRedirects = $allRedirects[$domainName]['flat'][rtrim($path, '/') . '/'];
65 }
66 // check if a flat redirect matches with the Query applied
67 if (!empty($query)) {
68 $pathWithQuery = rtrim($path, '/') . '?' . ltrim($query, '?');
69 if (!empty($allRedirects[$domainName]['respect_query_parameters'][$pathWithQuery])) {
70 $possibleRedirects = $allRedirects[$domainName]['respect_query_parameters'][$pathWithQuery];
71 } else {
72 $pathWithQueryAndSlash = rtrim($path, '/') . '/?' . ltrim($query, '?');
73 if (!empty($allRedirects[$domainName]['respect_query_parameters'][$pathWithQueryAndSlash])) {
74 $possibleRedirects = $allRedirects[$domainName]['respect_query_parameters'][$pathWithQueryAndSlash];
75 }
76 }
77 }
78 // check all redirects that are registered as regex
79 if (!empty($allRedirects[$domainName]['regexp'])) {
80 $allRegexps = array_keys($allRedirects[$domainName]['regexp']);
81 foreach ($allRegexps as $regexp) {
82 $matchResult = @preg_match($regexp, $path);
83 if ($matchResult) {
84 $possibleRedirects += $allRedirects[$domainName]['regexp'][$regexp];
85 } elseif ($matchResult === false) {
86 $this->logger->warning('Invalid regex in redirect', ['regex' => $regexp]);
87 }
88 }
89 }
90
91 foreach ($possibleRedirects as $possibleRedirect) {
92 // check starttime and endtime for all existing records
93 if ($this->isRedirectActive($possibleRedirect)) {
94 return $possibleRedirect;
95 }
96 }
97 }
98 }
99
100 /**
101 * Check if a redirect record matches the starttime and endtime and disable restrictions
102 *
103 * @param array $redirectRecord
104 *
105 * @return bool whether the redirect is active and should be used for redirecting the current request
106 */
107 protected function isRedirectActive(array $redirectRecord): bool
108 {
109 return !$redirectRecord['disabled'] && $redirectRecord['starttime'] <= $GLOBALS['SIM_ACCESS_TIME'] &&
110 (!$redirectRecord['endtime'] || $redirectRecord['endtime'] >= $GLOBALS['SIM_ACCESS_TIME']);
111 }
112
113 /**
114 * Fetches all redirects from the DB and caches them, grouped by the domain
115 * does NOT take starttime/endtime into account, as it is cached.
116 *
117 * @return array
118 */
119 protected function fetchRedirects(): array
120 {
121 return GeneralUtility::makeInstance(RedirectCacheService::class)->getRedirects();
122 }
123
124 /**
125 * Check if the current request is actually a redirect, and then process the redirect.
126 *
127 * @param string $redirectTarget
128 *
129 * @return array the link details from the linkService
130 */
131 protected function resolveLinkDetailsFromLinkTarget(string $redirectTarget): array
132 {
133 // build the target URL, take force SSL into account etc.
134 $linkService = GeneralUtility::makeInstance(LinkService::class);
135 try {
136 $linkDetails = $linkService->resolve($redirectTarget);
137 switch ($linkDetails['type']) {
138 case LinkService::TYPE_URL:
139 // all set up, nothing to do
140 break;
141 case LinkService::TYPE_FILE:
142 /** @var File $file */
143 $file = $linkDetails['file'];
144 if ($file instanceof File) {
145 $linkDetails['url'] = $file->getPublicUrl();
146 }
147 break;
148 case LinkService::TYPE_FOLDER:
149 /** @var Folder $folder */
150 $folder = $linkDetails['folder'];
151 if ($folder instanceof Folder) {
152 $linkDetails['url'] = $folder->getPublicUrl();
153 }
154 break;
155 default:
156 // we have to return the link details without having a "URL" parameter
157
158 }
159 } catch (InvalidPathException $e) {
160 return [];
161 }
162 return $linkDetails;
163 }
164
165 /**
166 * @param array $matchedRedirect
167 * @param array $queryParams
168 * @param SiteInterface|null $site
169 * @return UriInterface|null
170 */
171 public function getTargetUrl(array $matchedRedirect, array $queryParams, ?SiteInterface $site = null): ?UriInterface
172 {
173 $this->logger->debug('Found a redirect to process', $matchedRedirect);
174 $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode((string)$matchedRedirect['target']);
175 $redirectTarget = $linkParameterParts['url'];
176 $linkDetails = $this->resolveLinkDetailsFromLinkTarget($redirectTarget);
177 $this->logger->debug('Resolved link details for redirect', $linkDetails);
178 // Do this for files, folders, external URLs
179 if (!empty($linkDetails['url'])) {
180 $url = new Uri($linkDetails['url']);
181 if ($matchedRedirect['force_https']) {
182 $url = $url->withScheme('https');
183 }
184 if ($matchedRedirect['keep_query_parameters']) {
185 $url = $this->addQueryParams($queryParams, $url);
186 }
187 return $url;
188 }
189 // If it's a record or page, then boot up TSFE and use typolink
190 return $this->getUriFromCustomLinkDetails($matchedRedirect, $site, $linkDetails, $queryParams);
191 }
192
193 /**
194 * Adds query parameters to a Uri object
195 *
196 * @param array $queryParams
197 * @param Uri $url
198 * @return Uri
199 */
200 protected function addQueryParams(array $queryParams, Uri $url): Uri
201 {
202 // New query parameters overrule the ones that should be kept
203 $newQueryParamString = $url->getQuery();
204 if (!empty($newQueryParamString)) {
205 $newQueryParams = [];
206 parse_str($newQueryParamString, $newQueryParams);
207 $queryParams = array_replace_recursive($queryParams, $newQueryParams);
208 }
209 $query = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
210 if ($query) {
211 $url = $url->withQuery($query);
212 }
213 return $url;
214 }
215
216 /**
217 * Called when TypoScript/TSFE is available, so typolink is used to generate the URL
218 *
219 * @param array $redirectRecord
220 * @param SiteInterface|null $site
221 * @param array $linkDetails
222 * @param array $queryParams
223 * @return UriInterface|null
224 */
225 protected function getUriFromCustomLinkDetails(array $redirectRecord, ?SiteInterface $site, array $linkDetails, array $queryParams): ?UriInterface
226 {
227 if (!isset($linkDetails['type'], $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
228 return null;
229 }
230 $controller = $this->bootFrontendController($site, $queryParams);
231 /** @var AbstractTypolinkBuilder $linkBuilder */
232 $linkBuilder = GeneralUtility::makeInstance(
233 $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
234 $controller->cObj,
235 $controller
236 );
237 try {
238 $configuration = [
239 'parameter' => (string)$redirectRecord['target'],
240 'forceAbsoluteUrl' => true,
241 ];
242 if ($redirectRecord['force_https']) {
243 $configuration['forceAbsoluteUrl.']['scheme'] = 'https';
244 }
245 if ($redirectRecord['keep_query_parameters']) {
246 $configuration['additionalParams'] = HttpUtility::buildQueryString($queryParams, '&');
247 }
248 list($url) = $linkBuilder->build($linkDetails, '', '', $configuration);
249 return new Uri($url);
250 } catch (UnableToLinkException $e) {
251 return null;
252 }
253 }
254
255 /**
256 * Finishing booting up TSFE, after that the following properties are available.
257 *
258 * Instantiating is done by the middleware stack (see Configuration/RequestMiddlewares.php)
259 *
260 * - TSFE->fe_user
261 * - TSFE->sys_page
262 * - TSFE->tmpl
263 * - TSFE->config
264 * - TSFE->cObj
265 *
266 * So a link to a page can be generated.
267 *
268 * @param SiteInterface|null $site
269 * @param array $queryParams
270 * @return TypoScriptFrontendController
271 */
272 protected function bootFrontendController(?SiteInterface $site, array $queryParams): TypoScriptFrontendController
273 {
274 // disable page errors
275 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = false;
276 $controller = GeneralUtility::makeInstance(
277 TypoScriptFrontendController::class,
278 null,
279 $site ? $site->getRootPageId() : $GLOBALS['TSFE']->id,
280 0
281 );
282 $controller->fe_user = $GLOBALS['TSFE']->fe_user ?? null;
283 $controller->fetch_the_id();
284 $controller->calculateLinkVars($queryParams);
285 $controller->getConfigArray();
286 $controller->settingLanguage();
287 $controller->settingLocale();
288 $controller->newCObj();
289 return $controller;
290 }
291 }