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