[FEATURE] Use symfony/routing for Site 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\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27 use TYPO3\CMS\Core\Http\NormalizedParams;
28 use TYPO3\CMS\Core\Site\Entity\Site;
29 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
30 use TYPO3\CMS\Core\Site\SiteFinder;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Identifies if a site is configured for the request, based on "id" and "L" GET/POST parameters, or the requested
35 * string.
36 *
37 * If a site is found, the request is populated with the found language+site objects. If none is found, the main magic
38 * is handled by the PageResolver middleware.
39 *
40 * In addition to that, TSFE gets the $domainStartPage information resolved and added.
41 */
42 class SiteResolver implements MiddlewareInterface
43 {
44 /**
45 * @var SiteFinder
46 */
47 protected $finder;
48
49 /**
50 * Injects necessary objects
51 * @param SiteFinder|null $finder
52 */
53 public function __construct(SiteFinder $finder = null)
54 {
55 $this->finder = $finder ?? GeneralUtility::makeInstance(SiteFinder::class);
56 }
57
58 /**
59 * Resolve the site/language information by checking the page ID or the URL.
60 *
61 * @param ServerRequestInterface $request
62 * @param RequestHandlerInterface $handler
63 * @return ResponseInterface
64 */
65 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
66 {
67 $site = null;
68 $language = null;
69
70 $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0;
71 $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null;
72
73 // 1. Check if we have a _GET/_POST parameter for "id", then a site information can be resolved based.
74 if ($pageId > 0 && $languageId !== null) {
75 // Loop over the whole rootline without permissions to get the actual site information
76 try {
77 $site = $this->finder->getSiteByPageId((int)$pageId);
78 $language = $site->getLanguageById((int)$languageId);
79 } catch (SiteNotFoundException $e) {
80 // No site found by ID
81 }
82 }
83
84 // 2. Check if there is a site language, if not, do "Site Routing"
85 if (!($language instanceof SiteLanguage)) {
86 $collection = $this->finder->getRouteCollectionForAllSites();
87 // This part will likely be extracted into a separate class that builds a context out of a PSR-7 request
88 // something like $result = SiteRouter->matchRequest($psr7Request);
89 $context = new RequestContext(
90 '',
91 $request->getMethod(),
92 $request->getUri()->getHost(),
93 $request->getUri()->getScheme(),
94 // Ports are only necessary for URL generation in Symfony which is not used by TYPO3
95 80,
96 443,
97 $request->getUri()->getPath()
98 );
99 $matcher = new UrlMatcher($collection, $context);
100 try {
101 $result = $matcher->match($request->getUri()->getPath());
102 $site = $result['site'];
103 $language = $result['language'];
104 } catch (ResourceNotFoundException $e) {
105 // No site found
106 }
107 }
108
109 // Add language+site information to the PSR-7 request object.
110 if ($language instanceof SiteLanguage && $site instanceof Site) {
111 $request = $request->withAttribute('site', $site);
112 $request = $request->withAttribute('language', $language);
113 $queryParams = $request->getQueryParams();
114 // necessary to calculate the proper hash base
115 $queryParams['L'] = $language->getLanguageId();
116 $request = $request->withQueryParams($queryParams);
117 $_GET['L'] = $queryParams['L'];
118 // At this point, we later get further route modifiers
119 // for bw-compat we update $GLOBALS[TYPO3_REQUEST] to be used later in TSFE.
120 $GLOBALS['TYPO3_REQUEST'] = $request;
121 }
122
123 // Now resolve the root page of the site, the page_id of the current domain
124 if ($site instanceof Site) {
125 $GLOBALS['TSFE']->domainStartPage = $site->getRootPageId();
126 } else {
127 $GLOBALS['TSFE']->domainStartPage = $this->findDomainRecord($request->getAttribute('normalizedParams'), (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['recursiveDomainSearch']);
128 }
129
130 return $handler->handle($request);
131 }
132
133 /**
134 * Looking up a domain record based on server parameters HTTP_HOST
135 *
136 * @param NormalizedParams $requestParams used to get sanitized information of the current request
137 * @param bool $recursive If set, it looks "recursively" meaning that a domain like "123.456.typo3.com" would find a domain record like "typo3.com" if "123.456.typo3.com" or "456.typo3.com" did not exist.
138 * @return int|null Returns the page id of the page where the domain record was found or null if no sys_domain record found.
139 * previously found at \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::findDomainRecord()
140 */
141 protected function findDomainRecord(NormalizedParams $requestParams, $recursive = false): ?int
142 {
143 if ($recursive) {
144 $pageUid = 0;
145 $host = explode('.', $requestParams->getHttpHost());
146 while (count($host)) {
147 $pageUid = $this->getRootPageIdFromDomainRecord(implode('.', $host), $requestParams->getScriptName());
148 if ($pageUid) {
149 return $pageUid;
150 }
151 array_shift($host);
152 }
153 return $pageUid;
154 }
155 return $this->getRootPageIdFromDomainRecord($requestParams->getHttpHost(), $requestParams->getScriptName());
156 }
157
158 /**
159 * Will find the page ID carrying the domain record matching the input domain.
160 *
161 * @param string $domain Domain name to search for. Eg. "www.typo3.com". Typical the HTTP_HOST value.
162 * @param string $path Path for the current script in domain. Eg. "/somedir/subdir". Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('SCRIPT_NAME')
163 * @return int|null If found, returns integer with page UID where found. Otherwise null.
164 * previously found at PageRepository::getDomainStartPage
165 */
166 protected function getRootPageIdFromDomainRecord(string $domain, string $path = ''): ?int
167 {
168 list($domain) = explode(':', $domain);
169 $domain = strtolower(preg_replace('/\\.$/', '', $domain));
170 // Removing extra trailing slashes
171 $path = trim(preg_replace('/\\/[^\\/]*$/', '', $path));
172 // Appending to domain string
173 $domain .= $path;
174 $domain = preg_replace('/\\/*$/', '', $domain);
175 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain');
176 $queryBuilder->getRestrictions()->removeAll();
177 $row = $queryBuilder
178 ->select(
179 'pid'
180 )
181 ->from('sys_domain')
182 ->where(
183 $queryBuilder->expr()->eq(
184 'hidden',
185 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
186 ),
187 $queryBuilder->expr()->orX(
188 $queryBuilder->expr()->eq(
189 'domainName',
190 $queryBuilder->createNamedParameter($domain, \PDO::PARAM_STR)
191 ),
192 $queryBuilder->expr()->eq(
193 'domainName',
194 $queryBuilder->createNamedParameter($domain . '/', \PDO::PARAM_STR)
195 )
196 )
197 )
198 ->setMaxResults(1)
199 ->execute()
200 ->fetch();
201 return $row ? (int)$row['pid'] : null;
202 }
203 }