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