[FEATURE] Add system extension "redirects"
[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\Controller\TypoScriptFrontendController;
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 $linkDetails = $this->resolveLinkDetailsFromLinkTarget((string)$matchedRedirect['target']);
155 $this->logger->debug('Resolved link details for redirect', $linkDetails);
156 // Do this for files, folders, external URLs
157 if ($linkDetails['url']) {
158 $url = new Uri($linkDetails['url']);
159 if ($matchedRedirect['force_https']) {
160 $url = $url->withScheme('https');
161 }
162 if ($matchedRedirect['keep_query_parameters']) {
163 $url = $this->addQueryParams($queryParams, $url);
164 }
165 } else {
166 // If it's a record or page, then boot up TSFE
167 $url = $this->getUriFromCustomLinkDetails($linkDetails, $matchedRedirect);
168 }
169 return $url;
170 }
171
172 /**
173 * Adds query parameters to a Uri object
174 *
175 * @param array $queryParams
176 * @param Uri $url
177 * @return Uri
178 */
179 protected function addQueryParams(array $queryParams, Uri $url): Uri
180 {
181 // New query parameters overrule the ones that should be kept
182 $newQueryParamString = $url->getQuery();
183 if (!empty($newQueryParamString)) {
184 $newQueryParams = GeneralUtility::explodeUrl2Array($newQueryParamString, true);
185 $queryParams = array_replace_recursive($queryParams, $newQueryParams);
186 }
187 $query = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
188 if ($query) {
189 $url = $url->withQuery($query);
190 }
191 return $url;
192 }
193
194 /**
195 * Called when TypoScript/TSFE is available, so typolink is used to generate the URL
196 *
197 * @param array $linkDetails
198 * @param array $redirectRecord
199 * @return UriInterface|null
200 */
201 protected function getUriFromCustomLinkDetails(array $linkDetails, array $redirectRecord)
202 {
203 if (!isset($linkDetails['type'], $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
204 return null;
205 }
206 $this->bootFrontendController();
207 /** @var AbstractTypolinkBuilder $linkBuilder */
208 $linkBuilder = GeneralUtility::makeInstance(
209 $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
210 $GLOBALS['TSFE']->cObj
211 );
212 try {
213 $configuration = [
214 'forceAbsoluteUrl' => true,
215 ];
216 if ($redirectRecord['force_https']) {
217 $configuration['forceAbsoluteUrl.']['scheme'] = 'https';
218 }
219 if ($redirectRecord['keep_query_parameters']) {
220 $configuration['useCacheHash'] = false;
221 $configuration['addQueryString'] = true;
222 }
223 list($url) = $linkBuilder->build($linkDetails, '', '', $configuration);
224 return new Uri($url);
225 } catch (UnableToLinkException $e) {
226 }
227 }
228
229 /**
230 * Instantiates a TSFE object, with the first valid page ID found, after that the following properties
231 * are available
232 * - TSFE->sys_page
233 * - TSFE->tmpl
234 * - TSFE->config
235 * - TSFE->cObj
236 *
237 * So a link to a page could be generated.
238 */
239 protected function bootFrontendController()
240 {
241 // disable page errors
242 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = false;
243 $GLOBALS['TSFE'] = GeneralUtility::makeInstance(
244 TypoScriptFrontendController::class,
245 null,
246 GeneralUtility::_GP('id'),
247 GeneralUtility::_GP('type')
248 );
249 $GLOBALS['TSFE']->initFEuser();
250 $GLOBALS['TSFE']->initializeBackendUser();
251 $GLOBALS['TSFE']->fetch_the_id();
252 $GLOBALS['TSFE']->initTemplate();
253 $GLOBALS['TSFE']->getConfigArray();
254 $GLOBALS['TSFE']->settingLanguage();
255 $GLOBALS['TSFE']->settingLocale();
256 $GLOBALS['TSFE']->newCObj();
257 }
258 }