[TASK] Centralize sys_domain resolving
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Middleware / SiteResolver.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Middleware;
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\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use Psr\Http\Server\MiddlewareInterface;
21 use Psr\Http\Server\RequestHandlerInterface;
22 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
23 use Symfony\Component\Routing\Matcher\UrlMatcher;
24 use Symfony\Component\Routing\RequestContext;
25 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27 use TYPO3\CMS\Core\Site\Entity\Site;
28 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
29 use TYPO3\CMS\Core\Site\SiteFinder;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
32 use TYPO3\CMS\Frontend\Controller\ErrorController;
33 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
34
35 /**
36 * Identifies if a site is configured for the request, based on "id" and "L" GET/POST parameters, or the requested
37 * string.
38 *
39 * If a site is found, the request is populated with the found language+site objects. If none is found, the main magic
40 * is handled by the PageResolver middleware.
41 *
42 * In addition to that, TSFE gets the $domainStartPage information resolved and added.
43 */
44 class SiteResolver implements MiddlewareInterface
45 {
46 /**
47 * @var SiteFinder
48 */
49 protected $finder;
50
51 /**
52 * Injects necessary objects
53 * @param SiteFinder|null $finder
54 */
55 public function __construct(SiteFinder $finder = null)
56 {
57 $this->finder = $finder ?? GeneralUtility::makeInstance(SiteFinder::class);
58 }
59
60 /**
61 * Resolve the site/language information by checking the page ID or the URL.
62 *
63 * @param ServerRequestInterface $request
64 * @param RequestHandlerInterface $handler
65 * @return ResponseInterface
66 */
67 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
68 {
69 $site = null;
70 $language = null;
71
72 $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0;
73 $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null;
74
75 // 1. Check if we have a _GET/_POST parameter for "id", then a site information can be resolved based.
76 if ($pageId > 0 && $languageId !== null) {
77 // Loop over the whole rootline without permissions to get the actual site information
78 try {
79 $site = $this->finder->getSiteByPageId((int)$pageId);
80 $language = $site->getLanguageById((int)$languageId);
81 // language is hidden but also not visible to the BE user, this needs to fail
82 if ($language && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
83 $request = $request->withAttribute('site', $site);
84 return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
85 $request,
86 'Page is not available in the requested language.',
87 ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
88 );
89 }
90 } catch (SiteNotFoundException $e) {
91 // No site found by ID
92 }
93 }
94
95 // 2. Check if there is a site language, if not, do "Site Routing"
96 if (!($language instanceof SiteLanguage)) {
97 $collection = $this->finder->getRouteCollectionForAllSites();
98 // This part will likely be extracted into a separate class that builds a context out of a PSR-7 request
99 // something like $result = SiteRouter->matchRequest($psr7Request);
100 $context = new RequestContext(
101 '',
102 $request->getMethod(),
103 $request->getUri()->getHost(),
104 $request->getUri()->getScheme(),
105 // Ports are only necessary for URL generation in Symfony which is not used by TYPO3
106 80,
107 443,
108 $request->getUri()->getPath()
109 );
110 $matcher = new UrlMatcher($collection, $context);
111 try {
112 $result = $matcher->match($request->getUri()->getPath());
113 $site = $result['site'];
114 $language = $result['language'];
115 // language is found, and hidden but also not visible to the BE user, this needs to fail
116 if ($language && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
117 $request = $request->withAttribute('site', $site);
118 return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
119 $request,
120 'Page is not available in the requested language.',
121 ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
122 );
123 }
124 } catch (ResourceNotFoundException $e) {
125 // No site found
126 }
127 }
128
129 // Add language+site information to the PSR-7 request object.
130 if ($language instanceof SiteLanguage && $site instanceof Site) {
131 $request = $request->withAttribute('site', $site);
132 $request = $request->withAttribute('language', $language);
133 $queryParams = $request->getQueryParams();
134 // necessary to calculate the proper hash base
135 $queryParams['L'] = $language->getLanguageId();
136 $request = $request->withQueryParams($queryParams);
137 $_GET['L'] = $queryParams['L'];
138 // At this point, we later get further route modifiers
139 // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
140 $GLOBALS['TYPO3_REQUEST'] = $request;
141 }
142
143 // Now resolve the root page of the site, the page_id of the current domain
144 if ($site instanceof Site) {
145 $GLOBALS['TSFE']->domainStartPage = $site->getRootPageId();
146 } else {
147 $GLOBALS['TSFE']->domainStartPage = GeneralUtility::makeInstance(LegacyDomainResolver::class)
148 ->matchRequest($request);
149 }
150
151 return $handler->handle($request);
152 }
153
154 /**
155 * Checks if the language is allowed in Frontend, if not, check if there is valid BE user
156 *
157 * @param SiteLanguage|null $language
158 * @param BackendUserAuthentication|null $user
159 * @return bool
160 */
161 protected function isLanguageEnabled(SiteLanguage $language, BackendUserAuthentication $user = null): bool
162 {
163 // language is hidden, check if a possible backend user is allowed to access the language
164 if ($language->enabled() || ($user instanceof BackendUserAuthentication && $user->checkLanguageAccess($language->getLanguageId()))) {
165 return true;
166 }
167 return false;
168 }
169 }