[FEATURE] Add "Pseudo" Site functionality
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Unit / Middleware / SiteResolverTest.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Frontend\Tests\Unit\Middleware;
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\ResponseInterface;
20 use Psr\Http\Message\ServerRequestInterface;
21 use Psr\Http\Server\RequestHandlerInterface;
22 use TYPO3\CMS\Core\Http\JsonResponse;
23 use TYPO3\CMS\Core\Http\NullResponse;
24 use TYPO3\CMS\Core\Http\ServerRequest;
25 use TYPO3\CMS\Core\Routing\SiteMatcher;
26 use TYPO3\CMS\Core\Site\Entity\Site;
27 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
28 use TYPO3\CMS\Core\Site\SiteFinder;
29 use TYPO3\CMS\Frontend\Middleware\SiteResolver;
30 use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
31 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
32 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
33
34 class SiteResolverTest extends UnitTestCase
35 {
36 /**
37 * @var bool Reset singletons created by subject
38 */
39 protected $resetSingletonInstances = true;
40
41 /**
42 * @var SiteFinder|AccessibleObjectInterface
43 */
44 protected $siteFinder;
45
46 protected $siteFoundRequestHandler;
47
48 /**
49 * Set up
50 */
51 protected function setUp(): void
52 {
53 // Make global object available, however it is not actively used
54 $GLOBALS['TSFE'] = new \stdClass();
55 $this->siteFinder = $this->getAccessibleMock(SiteFinder::class, ['dummy'], [], '', false);
56
57 // A request handler which expects a site to be found.
58 $this->siteFoundRequestHandler = new class implements RequestHandlerInterface {
59 public function handle(ServerRequestInterface $request): ResponseInterface
60 {
61 /** @var Site $site */
62 /** @var SiteLanguage $language */
63 $site = $request->getAttribute('site', false);
64 $language = $request->getAttribute('language', false);
65 if ($site && $language) {
66 return new JsonResponse(
67 [
68 'site' => $site->getIdentifier(),
69 'language-id' => $language->getLanguageId(),
70 'language-base' => $language->getBase(),
71 'rootpage' => $GLOBALS['TSFE']->domainStartPage
72 ]
73 );
74 }
75 return new NullResponse();
76 }
77 };
78 }
79
80 /**
81 * Expect a URL handed in, as a request. This URL does not have a GET parameter "id"
82 * Then the site handling gets triggered, and the URL is taken to resolve a site.
83 *
84 * This case tests against a site with no domain or scheme, and successfully finds it.
85 *
86 * @test
87 */
88 public function detectASingleSiteWhenProperRequestIsGiven()
89 {
90 $incomingUrl = 'https://a-random-domain.com/mysite/';
91 $siteIdentifier = 'full-site';
92 $this->siteFinder->_set('sites', [
93 $siteIdentifier => new Site($siteIdentifier, 13, [
94 'base' => '/mysite/',
95 'languages' => [
96 0 => [
97 'languageId' => 0,
98 'locale' => 'fr_FR.UTF-8',
99 'base' => '/'
100 ]
101 ]
102 ])
103 ]);
104
105 $request = new ServerRequest($incomingUrl, 'GET');
106 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
107 $response = $subject->process($request, $this->siteFoundRequestHandler);
108 if ($response instanceof NullResponse) {
109 $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
110 } else {
111 $result = $response->getBody()->getContents();
112 $result = json_decode($result, true);
113 $this->assertEquals($siteIdentifier, $result['site']);
114 $this->assertEquals(0, $result['language-id']);
115 $this->assertEquals('/mysite/', $result['language-base']);
116 }
117 }
118
119 /**
120 * Scenario with two sites
121 * Site 1: /
122 * Site 2: /mysubsite/
123 *
124 * The result should be that site 2 is resolved by the router when calling
125 *
126 * www.random-result.com/mysubsite/you-know-why/
127 *
128 * @test
129 */
130 public function detectSubsiteInsideNestedUrlStructure()
131 {
132 $incomingUrl = 'https://www.random-result.com/mysubsite/you-know-why/';
133 $this->siteFinder->_set('sites', [
134 'outside-site' => new Site('outside-site', 13, [
135 'base' => '/',
136 'languages' => [
137 0 => [
138 'languageId' => 0,
139 'locale' => 'fr_FR.UTF-8',
140 'base' => '/'
141 ]
142 ]
143 ]),
144 'sub-site' => new Site('sub-site', 15, [
145 'base' => '/mysubsite/',
146 'languages' => [
147 0 => [
148 'languageId' => 0,
149 'locale' => 'fr_FR.UTF-8',
150 'base' => '/'
151 ]
152 ]
153 ]),
154 ]);
155
156 $request = new ServerRequest($incomingUrl, 'GET');
157 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
158 $response = $subject->process($request, $this->siteFoundRequestHandler);
159 if ($response instanceof NullResponse) {
160 $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
161 } else {
162 $result = $response->getBody()->getContents();
163 $result = json_decode($result, true);
164 $this->assertEquals('sub-site', $result['site']);
165 $this->assertEquals(0, $result['language-id']);
166 $this->assertEquals('/mysubsite/', $result['language-base']);
167 }
168 }
169
170 public function detectSubSubsiteInsideNestedUrlStructureDataProvider()
171 {
172 return [
173 'matches second site' => [
174 'https://www.random-result.com/mysubsite/you-know-why/',
175 'sub-site',
176 14,
177 '/mysubsite/'
178 ],
179 'matches third site' => [
180 'https://www.random-result.com/mysubsite/micro-site/oh-yes-you-do/',
181 'subsub-site',
182 15,
183 '/mysubsite/micro-site/'
184 ],
185 'matches a subsite in first site' => [
186 'https://www.random-result.com/products/pampers/',
187 'outside-site',
188 13,
189 '/'
190 ],
191 ];
192 }
193
194 /**
195 * Scenario with three sites
196 * Site 1: /
197 * Site 2: /mysubsite/
198 * Site 3: /mysubsite/micro-site/
199 *
200 * The result should be that site 2 is resolved by the router when calling
201 *
202 * www.random-result.com/mysubsite/you-know-why/
203 *
204 * and site 3 when calling
205 * www.random-result.com/mysubsite/micro-site/oh-yes-you-do/
206 *
207 * @test
208 * @dataProvider detectSubSubsiteInsideNestedUrlStructureDataProvider
209 */
210 public function detectSubSubsiteInsideNestedUrlStructure($incomingUrl, $expectedSiteIdentifier, $expectedRootPageId, $expectedBase)
211 {
212 $this->siteFinder->_set('sites', [
213 'outside-site' => new Site('outside-site', 13, [
214 'base' => '/',
215 'languages' => [
216 0 => [
217 'languageId' => 0,
218 'locale' => 'fr_FR.UTF-8',
219 'base' => '/'
220 ]
221 ]
222 ]),
223 'sub-site' => new Site('sub-site', 14, [
224 'base' => '/mysubsite/',
225 'languages' => [
226 0 => [
227 'languageId' => 0,
228 'locale' => 'fr_FR.UTF-8',
229 'base' => '/'
230 ]
231 ]
232 ]),
233 'subsub-site' => new Site('subsub-site', 15, [
234 'base' => '/mysubsite/micro-site/',
235 'languages' => [
236 0 => [
237 'languageId' => 0,
238 'locale' => 'fr_FR.UTF-8',
239 'base' => '/'
240 ]
241 ]
242 ]),
243 ]);
244
245 $request = new ServerRequest($incomingUrl, 'GET');
246 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
247 $response = $subject->process($request, $this->siteFoundRequestHandler);
248 if ($response instanceof NullResponse) {
249 $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
250 } else {
251 $result = $response->getBody()->getContents();
252 $result = json_decode($result, true);
253 $this->assertEquals($expectedSiteIdentifier, $result['site']);
254 $this->assertEquals($expectedRootPageId, $result['rootpage']);
255 $this->assertEquals($expectedBase, $result['language-base']);
256 }
257 }
258
259 public function detectProperLanguageByIncomingUrlDataProvider()
260 {
261 return [
262 'matches second site' => [
263 'https://www.random-result.com/mysubsite/you-know-why/',
264 'sub-site',
265 14,
266 2,
267 '/mysubsite/'
268 ],
269 'matches second site in other language' => [
270 'https://www.random-result.com/mysubsite/it/you-know-why/',
271 'sub-site',
272 14,
273 2,
274 '/mysubsite/'
275 ],
276 'matches second site because third site language prefix did not match' => [
277 'https://www.random-result.com/mysubsite/micro-site/oh-yes-you-do/',
278 'sub-site',
279 14,
280 2,
281 '/mysubsite/'
282 ],
283 'matches third site' => [
284 'https://www.random-result.com/mysubsite/micro-site/ru/oh-yes-you-do/',
285 'subsub-site',
286 15,
287 13,
288 '/mysubsite/micro-site/ru/'
289 ],
290 /**
291 * This case does not work, as no language prefix is defined.
292 'matches a subsite in first site' => [
293 'https://www.random-result.com/products/pampers/',
294 'outside-site',
295 13,
296 0,
297 '/'
298 ],
299 */
300 'matches a subsite with translation in first site' => [
301 'https://www.random-result.com/fr/products/pampers/',
302 'outside-site',
303 13,
304 1,
305 '/fr/'
306 ],
307 ];
308 }
309
310 /**
311 * Scenario with three one site and three languages
312 * Site 1: /
313 * Language 0: /en/
314 * Language 1: /fr/
315 * Site 2: /mysubsite/
316 * Language: 2: /
317 * Site 3: /mysubsite/micro-site/
318 * Language: 13: /ru/
319 *
320 * @test
321 * @dataProvider detectProperLanguageByIncomingUrlDataProvider
322 */
323 public function detectProperLanguageByIncomingUrl($incomingUrl, $expectedSiteIdentifier, $expectedRootPageId, $expectedLanguageId, $expectedBase)
324 {
325 $this->siteFinder->_set('sites', [
326 'outside-site' => new Site('outside-site', 13, [
327 'base' => '/',
328 'languages' => [
329 0 => [
330 'languageId' => 0,
331 'locale' => 'en_US.UTF-8',
332 'base' => '/en/'
333 ],
334 1 => [
335 'languageId' => 1,
336 'locale' => 'fr_CA.UTF-8',
337 'base' => '/fr/'
338 ]
339 ]
340 ]),
341 'sub-site' => new Site('sub-site', 14, [
342 'base' => '/mysubsite/',
343 'languages' => [
344 2 => [
345 'languageId' => 2,
346 'locale' => 'it_IT.UTF-8',
347 'base' => '/'
348 ]
349 ]
350 ]),
351 'subsub-site' => new Site('subsub-site', 15, [
352 'base' => '/mysubsite/micro-site/',
353 'languages' => [
354 13 => [
355 'languageId' => 13,
356 'locale' => 'ru_RU.UTF-8',
357 'base' => '/ru/'
358 ]
359 ]
360 ]),
361 ]);
362
363 $request = new ServerRequest($incomingUrl, 'GET');
364 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
365 $response = $subject->process($request, $this->siteFoundRequestHandler);
366 if ($response instanceof NullResponse) {
367 $this->fail('No site configuration found in URL ' . $incomingUrl . '.');
368 } else {
369 $result = $response->getBody()->getContents();
370 $result = json_decode($result, true);
371 $this->assertEquals($expectedSiteIdentifier, $result['site']);
372 $this->assertEquals($expectedRootPageId, $result['rootpage']);
373 $this->assertEquals($expectedLanguageId, $result['language-id']);
374 $this->assertEquals($expectedBase, $result['language-base']);
375 }
376 }
377
378 /**
379 * @test
380 */
381 public function checkIf404IsSiteLanguageIsDisabledInFrontend()
382 {
383 $this->siteFinder->_set('sites', [
384 'mixed-site' => new Site('mixed-site', 13, [
385 'base' => '/',
386 'errorHandling' => [
387 [
388 'errorCode' => 404,
389 'errorHandler' => 'PHP',
390 'errorPhpClassFQCN' => PhpError::class
391 ]
392 ],
393 'languages' => [
394 0 => [
395 'languageId' => 0,
396 'locale' => 'en_US.UTF-8',
397 'base' => '/en/',
398 'enabled' => false
399 ],
400 1 => [
401 'languageId' => 1,
402 'locale' => 'fr_CA.UTF-8',
403 'base' => '/fr/',
404 'enabled' => true
405 ]
406 ]
407 ]),
408 ]);
409
410 // Reqest to default page
411 $request = new ServerRequest('https://twenty.one/en/pilots/', 'GET');
412 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
413 $response = $subject->process($request, $this->siteFoundRequestHandler);
414 $this->assertEquals(404, $response->getStatusCode());
415
416 $request = new ServerRequest('https://twenty.one/fr/pilots/', 'GET');
417 $subject = new SiteResolver(new SiteMatcher($this->siteFinder));
418 $response = $subject->process($request, $this->siteFoundRequestHandler);
419 $this->assertEquals(200, $response->getStatusCode());
420 }
421 }