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