[FEATURE] Use symfony/routing for Site Resolving
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Site / SiteFinder.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Site;
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 Symfony\Component\Routing\Route;
20 use Symfony\Component\Routing\RouteCollection;
21 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
22 use TYPO3\CMS\Core\Core\Environment;
23 use TYPO3\CMS\Core\Exception\Page\PageNotFoundException;
24 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
25 use TYPO3\CMS\Core\Site\Entity\Site;
26 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\RootlineUtility;
29
30 /**
31 * Is used in backend and frontend for all places where to read / identify sites and site languages.
32 */
33 class SiteFinder
34 {
35 /**
36 * @var Site[]
37 */
38 protected $sites = [];
39
40 /**
41 * Short-hand to quickly fetch a site based on a rootPageId
42 *
43 * @var array
44 */
45 protected $mappingRootPageIdToIdentifier = [];
46
47 /**
48 * Fetches all existing configurations as Site objects
49 */
50 public function __construct()
51 {
52 $reader = GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites');
53 $sites = $reader->resolveAllExistingSites();
54 foreach ($sites as $identifier => $site) {
55 $this->sites[$identifier] = $site;
56 $this->mappingRootPageIdToIdentifier[$site->getRootPageId()] = $identifier;
57 }
58 }
59
60 /**
61 * Return a list of all configured sites
62 *
63 * @return Site[]
64 */
65 public function getAllSites(): array
66 {
67 return $this->sites;
68 }
69
70 /**
71 * Returns a Symfony RouteCollection containing all routes to all sites.
72 *
73 * {next} is not evaluated yet, but set as suffix and will change in the future.
74 *
75 * @return RouteCollection
76 * @internal this method will likely change due to further extraction into custom logic for Routing
77 */
78 public function getRouteCollectionForAllSites(): RouteCollection
79 {
80 $collection = new RouteCollection();
81 $groupedRoutes = [];
82 foreach ($this->sites as $site) {
83 foreach ($site->getLanguages() as $siteLanguage) {
84 $urlParts = parse_url($siteLanguage->getBase());
85 $route = new Route(
86 ($urlParts['path'] ?? '/') . '{next}',
87 ['next' => '', 'site' => $site, 'language' => $siteLanguage],
88 array_filter(['next' => '.*', 'port' => $urlParts['port'] ?? null]),
89 ['utf8' => true],
90 $urlParts['host'] ?? '',
91 !empty($urlParts['scheme']) ? [$urlParts['scheme']] : null
92 );
93 $identifier = 'site_' . $site->getIdentifier() . '_' . $siteLanguage->getLanguageId();
94 $groupedRoutes[$urlParts['host'] ?? 0][$urlParts['path'] ?? 0][$identifier] = $route;
95 }
96 }
97 // As the {next} parameter is greedy, it needs to be ensured that the one with the most specific part
98 // matches last
99 foreach ($groupedRoutes as $groupedRoutesPerHost) {
100 krsort($groupedRoutesPerHost);
101 foreach ($groupedRoutesPerHost as $groupedRoutesPerPath) {
102 krsort($groupedRoutesPerPath);
103 foreach ($groupedRoutesPerPath as $identifier => $route) {
104 $collection->add($identifier, $route);
105 }
106 }
107 }
108 return $collection;
109 }
110
111 /**
112 * Find a site by given root page id
113 *
114 * @param int $rootPageId
115 * @return SiteInterface
116 * @throws SiteNotFoundException
117 */
118 public function getSiteByRootPageId(int $rootPageId): SiteInterface
119 {
120 if (isset($this->mappingRootPageIdToIdentifier[$rootPageId])) {
121 return $this->sites[$this->mappingRootPageIdToIdentifier[$rootPageId]];
122 }
123 throw new SiteNotFoundException('No site found for root page id ' . $rootPageId, 1521668882);
124 }
125
126 /**
127 * Find a site by given identifier
128 *
129 * @param string $identifier
130 * @return Site
131 * @throws SiteNotFoundException
132 */
133 public function getSiteByIdentifier(string $identifier): Site
134 {
135 if (isset($this->sites[$identifier])) {
136 return $this->sites[$identifier];
137 }
138 throw new SiteNotFoundException('No site found for identifier ' . $identifier, 1521716628);
139 }
140
141 /**
142 * Traverses the rootline of a page up until a Site was found.
143 *
144 * @param int $pageId
145 * @param array $rootLine
146 * @return SiteInterface
147 * @throws SiteNotFoundException
148 */
149 public function getSiteByPageId(int $pageId, array $rootLine = null): SiteInterface
150 {
151 if (!is_array($rootLine)) {
152 try {
153 $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
154 } catch (PageNotFoundException $e) {
155 // Usually when a page was hidden or disconnected
156 // This could be improved by handing in a Context object and decide whether hidden pages
157 // Should be linkeable too
158 $rootLine = [];
159 }
160 }
161 foreach ($rootLine as $pageInRootLine) {
162 if ($pageInRootLine['uid'] > 0) {
163 try {
164 return $this->getSiteByRootPageId((int)$pageInRootLine['uid']);
165 } catch (SiteNotFoundException $e) {
166 // continue looping
167 }
168 }
169 }
170 throw new SiteNotFoundException('No site found in root line of page ' . $pageId, 1521716622);
171 }
172 }