[!!!][TASK] Remove [SYS][recursiveDomainSearch] option
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / SiteMatcher.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Routing;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Psr\Http\Message\ServerRequestInterface;
20 use Symfony\Component\Routing\Exception\NoConfigurationException;
21 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
22 use Symfony\Component\Routing\Matcher\UrlMatcher;
23 use Symfony\Component\Routing\RequestContext;
24 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
25 use TYPO3\CMS\Core\SingletonInterface;
26 use TYPO3\CMS\Core\Site\Entity\PseudoSite;
27 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
28 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
29 use TYPO3\CMS\Core\Site\PseudoSiteFinder;
30 use TYPO3\CMS\Core\Site\SiteFinder;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Returns a site or pseudo-site (with sys_domain records) based on a given request.
35 *
36 * The main usage is the ->matchRequest() functionality, which receives a request object and boots up
37 * Symfony Routing to find the proper route with its defaults / attributes.
38 *
39 * On top, this is also commonly used throughout TYPO3 to fetch a site by a given pageId.
40 * ->matchPageId().
41 *
42 * The concept of the SiteMatcher is to *resolve*, and not build URIs. On top, it is a facade to hide the
43 * dependency to symfony and to not expose its logic.
44 *
45 * @internal Please note that the site matcher will be probably cease to exist and adapted to the SiteFinder concept when Pseudo-Site handling will be removed.
46 */
47 class SiteMatcher implements SingletonInterface
48 {
49 /**
50 * @var SiteFinder
51 */
52 protected $finder;
53
54 /**
55 * @var PseudoSiteFinder
56 */
57 protected $pseudoSiteFinder;
58
59 /**
60 * Injects necessary objects. PseudoSiteFinder is not injectable as this will be become obsolete in the future.
61 *
62 * @param SiteFinder|null $finder
63 */
64 public function __construct(SiteFinder $finder = null)
65 {
66 $this->finder = $finder ?? GeneralUtility::makeInstance(SiteFinder::class);
67 $this->pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
68 }
69
70 /**
71 * First, it is checked, if a "id" GET/POST parameter is found.
72 * If it is, we check for a valid site mounted there.
73 *
74 * If it isn't the quest continues by validating the whole request URL and validating against
75 * all available site records (and their language prefixes).
76 *
77 * If none is found, the "legacy" handling is checked for - checking for all pseudo-sites with
78 * a sys_domain record, and match against them.
79 *
80 * @param ServerRequestInterface $request
81 * @return RouteResultInterface
82 */
83 public function matchRequest(ServerRequestInterface $request): RouteResultInterface
84 {
85 $site = null;
86 $language = null;
87 $defaultLanguage = null;
88
89 $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0;
90
91 // First, check if we have a _GET/_POST parameter for "id", then a site information can be resolved based.
92 if ($pageId > 0) {
93 // Loop over the whole rootline without permissions to get the actual site information
94 try {
95 $site = $this->finder->getSiteByPageId((int)$pageId);
96 // If a "L" parameter is given, we take that one into account.
97 $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null;
98 if ($languageId !== null) {
99 $language = $site->getLanguageById((int)$languageId);
100 } else {
101 // Use this later below
102 $defaultLanguage = $site->getDefaultLanguage();
103 }
104 } catch (SiteNotFoundException $e) {
105 // No site found by the given page
106 } catch (\InvalidArgumentException $e) {
107 // The language fetched by getLanguageById() was not available, now the PSR-15 middleware
108 // redirects to the default page.
109 }
110 }
111
112 // No language found at this point means that the URL was not used with a valid "?id=1&L=2" parameter
113 // which resulted in a site / language combination that was found. Now, the matching is done
114 // on the incoming URL.
115 if (!($language instanceof SiteLanguage)) {
116 $collection = $this->getRouteCollectionForAllSites();
117 $context = new RequestContext(
118 '',
119 $request->getMethod(),
120 $request->getUri()->getHost(),
121 $request->getUri()->getScheme(),
122 // Ports are only necessary for URL generation in Symfony which is not used by TYPO3
123 80,
124 443,
125 $request->getUri()->getPath()
126 );
127 $matcher = new UrlMatcher($collection, $context);
128 try {
129 $result = $matcher->match($request->getUri()->getPath());
130 return new SiteRouteResult(
131 $request->getUri(),
132 $result['site'],
133 // if no language is found, this usually results due to "/" called instead of "/fr/"
134 // but it could also be the reason that "/index.php?id=23" was called, so the default
135 // language is used as a fallback here then.
136 $result['language'] ?? $defaultLanguage,
137 $result['tail']
138 );
139 } catch (NoConfigurationException | ResourceNotFoundException $e) {
140 // No site+language combination found so far
141 }
142 // At this point we discard a possible found site via ?id=123
143 // Because ?id=123 _can_ only work if the actual domain/site base works
144 // so www.domain-without-site-configuration/index.php?id=123 (where 123 is a page referring
145 // to a page within a site configuration will never be resolved here) properly
146 $site = null;
147 }
148
149 // Check against any sys_domain records
150 $collection = $this->getRouteCollectionForVisibleSysDomains();
151 $context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost());
152 $matcher = new UrlMatcher($collection, $context);
153 try {
154 $result = $matcher->match($request->getUri()->getPath());
155 return new SiteRouteResult($request->getUri(), $result['site'], $result['language'], $result['tail']);
156 } catch (NoConfigurationException | ResourceNotFoundException $e) {
157 // No domain record found
158 }
159 // No domain record found, try resolving "pseudo-site" again
160 if ($site == null) {
161 try {
162 // use the matching "pseudo-site" for $pageId
163 $site = $this->pseudoSiteFinder->getSiteByPageId((int)$pageId);
164 } catch (SiteNotFoundException $exception) {
165 // use the first "pseudo-site" found
166 $allPseudoSites = $this->pseudoSiteFinder->findAll();
167 $site = reset($allPseudoSites);
168 }
169 }
170 return new SiteRouteResult($request->getUri(), $site, $language);
171 }
172
173 /**
174 * If a given page ID is handed in, a Site/PseudoSite/NullSite is returned.
175 *
176 * @param int $pageId uid of a page in default language
177 * @param array|null $rootLine an alternative root line, if already at and.
178 * @return SiteInterface
179 * @throws SiteNotFoundException
180 */
181 public function matchByPageId(int $pageId, array $rootLine = null): SiteInterface
182 {
183 try {
184 return $this->finder->getSiteByPageId($pageId, $rootLine);
185 } catch (SiteNotFoundException $e) {
186 // Check for a pseudo / null site
187 return $this->pseudoSiteFinder->getSiteByPageId($pageId, $rootLine);
188 }
189 }
190
191 /**
192 * Returns a Symfony RouteCollection containing all routes to all sites.
193 *
194 * @return RouteCollection
195 */
196 protected function getRouteCollectionForAllSites(): RouteCollection
197 {
198 $groupedRoutes = [];
199 foreach ($this->finder->getAllSites() as $site) {
200 // Add the site as entrypoint
201 $uri = $site->getBase();
202 $route = new Route(
203 ($uri->getPath() ?: '/') . '{tail}',
204 ['site' => $site, 'language' => null, 'tail' => ''],
205 array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
206 ['utf8' => true],
207 $uri->getHost() ?: '',
208 $uri->getScheme()
209 );
210 $identifier = 'site_' . $site->getIdentifier();
211 $groupedRoutes[($uri->getScheme() ?: '-') . ($uri->getHost() ?: '-')][$uri->getPath() ?: '/'][$identifier] = $route;
212 // Add all languages
213 foreach ($site->getAllLanguages() as $siteLanguage) {
214 $uri = $siteLanguage->getBase();
215 $route = new Route(
216 ($uri->getPath() ?: '/') . '{tail}',
217 ['site' => $site, 'language' => $siteLanguage, 'tail' => ''],
218 array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
219 ['utf8' => true],
220 $uri->getHost() ?: '',
221 $uri->getScheme()
222 );
223 $identifier = 'site_' . $site->getIdentifier() . '_' . $siteLanguage->getLanguageId();
224 $groupedRoutes[($uri->getScheme() ?: '-') . ($uri->getHost() ?: '-')][$uri->getPath() ?: '/'][$identifier] = $route;
225 }
226 }
227 return $this->createRouteCollectionFromGroupedRoutes($groupedRoutes);
228 }
229
230 /**
231 * Return the page ID (pid) of a sys_domain record, based on a request object, does the infamous
232 * "recursive domain search", to also detect if the domain is like "abc.def.example.com" even if the
233 * sys_domain entry is "example.com".
234 *
235 * @return RouteCollection
236 */
237 protected function getRouteCollectionForVisibleSysDomains(): RouteCollection
238 {
239 $sites = $this->pseudoSiteFinder->findAll();
240 $groupedRoutes = [];
241 foreach ($sites as $site) {
242 if (!$site instanceof PseudoSite) {
243 continue;
244 }
245 foreach ($site->getEntryPoints() as $uri) {
246 // Site has no sys_domain record, it is not valid for a routing entrypoint, but only available
247 // via "id" GET parameter which is handled separately
248 if (!$uri->getHost()) {
249 continue;
250 }
251 $route = new Route(
252 ($uri->getPath() ?: '/') . '{tail}',
253 ['site' => $site, 'language' => null, 'tail' => ''],
254 array_filter(['tail' => '.*', 'port' => (string)$uri->getPort()]),
255 ['utf8' => true],
256 $uri->getHost(),
257 $uri->getScheme()
258 );
259 $identifier = 'site_' . $site->getIdentifier() . '_' . (string)$uri;
260 $groupedRoutes[($uri->getScheme() ?: '-') . ($uri->getHost() ?: '-')][$uri->getPath() ?: '/'][$identifier] = $route;
261 }
262 }
263 return $this->createRouteCollectionFromGroupedRoutes($groupedRoutes);
264 }
265
266 /**
267 * As the {tail} parameter is greedy, it needs to be ensured that the one with the
268 * most specific part matches first.
269 *
270 * @param array $groupedRoutes
271 * @return RouteCollection
272 */
273 protected function createRouteCollectionFromGroupedRoutes(array $groupedRoutes): RouteCollection
274 {
275 $collection = new RouteCollection();
276 foreach ($groupedRoutes as $groupedRoutesPerHost) {
277 krsort($groupedRoutesPerHost);
278 foreach ($groupedRoutesPerHost as $groupedRoutesPerPath) {
279 krsort($groupedRoutesPerPath);
280 foreach ($groupedRoutesPerPath as $identifier => $route) {
281 $collection->add($identifier, $route);
282 }
283 }
284 }
285 return $collection;
286 }
287 }