bc129d78b36c6acb0d7d7ae29664da2fdc858d64
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / SiteHandling / SiteRequestTest.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
18 use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
19 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\ActionService;
20 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataMapFactory;
21 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
22 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
23 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
24
25 /**
26 * Test case for frontend requests having site handling configured
27 */
28 class SiteRequestTest extends AbstractRequestTest
29 {
30 protected const LANGUAGE_PRESETS = [
31 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
32 'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
33 'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
34 ];
35
36 /**
37 * @var array
38 */
39 protected $coreExtensionsToLoad = ['frontend'];
40
41 /**
42 * @var ActionService
43 */
44 private $actionService;
45
46 /**
47 * @var string
48 */
49 private $siteTitle = 'A Company that Manufactures Everything Inc';
50
51 /**
52 * @var InternalRequestContext
53 */
54 private $internalRequestContext;
55
56 protected function setUp()
57 {
58 parent::setUp();
59
60 // these settings are forwarded to the frontend sub-request as well
61 $this->internalRequestContext = (new InternalRequestContext())
62 ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
63
64 $this->setUpBackendUserFromFixture(1);
65 $this->actionService = new ActionService();
66
67 $scenarioFile = __DIR__ . '/Fixtures/scenario.yaml';
68 $factory = DataMapFactory::fromYamlFile($scenarioFile);
69 $this->actionService->invoke(
70 $factory->getDataMap(),
71 [],
72 $factory->getSuggestedIds()
73 );
74
75 $this->setUpFrontendRootPage(
76 101,
77 [
78 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
79 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
80 ],
81 [
82 'title' => 'ACME Root',
83 'sitetitle' => $this->siteTitle,
84 ]
85 );
86 }
87
88 protected function tearDown()
89 {
90 unset(
91 $this->actionService,
92 $this->internalRequestContext
93 );
94 parent::tearDown();
95 }
96
97 /**
98 * @return array
99 */
100 public function requestsAreRedirectedDataProvider(): array
101 {
102 $domainPaths = [
103 '/',
104 'https://localhost/',
105 'https://website.local/',
106 ];
107
108 $queries = [
109 '?',
110 '?id=101',
111 '?id=acme-root'
112 ];
113
114 return $this->wrapInArray(
115 $this->keysFromValues(
116 $this->meltStrings([$domainPaths, $queries])
117 )
118 );
119 }
120
121 /**
122 * @param string $uri
123 *
124 * @test
125 * @dataProvider requestsAreRedirectedDataProvider
126 */
127 public function requestsAreRedirected(string $uri)
128 {
129 $this->writeSiteConfiguration(
130 'website-local',
131 $this->buildSiteConfiguration(101, 'https://website.local/')
132 );
133
134 $expectedStatusCode = 307;
135 $expectedHeaders = ['location' => ['/?id=acme-first']];
136
137 $response = $this->executeFrontendRequest(
138 new InternalRequest($uri),
139 $this->internalRequestContext
140 );
141 static::assertSame($expectedStatusCode, $response->getStatusCode());
142 static::assertSame($expectedHeaders, $response->getHeaders());
143 }
144
145 /**
146 * @return array
147 */
148 public function pageIsRenderedWithPathsDataProvider(): array
149 {
150 $domainPaths = [
151 // @todo currently base needs to be defined with domain
152 // '/',
153 'https://website.local/',
154 ];
155
156 $languagePaths = [
157 '',
158 'en-en/',
159 'fr-fr/',
160 'fr-ca/',
161 ];
162
163 $queries = [
164 '?id=102',
165 '?id=acme-first',
166 ];
167
168 return array_map(
169 function (string $uri) {
170 if (strpos($uri, '/fr-fr/') !== false) {
171 $expectedPageTitle = 'FR: Welcome';
172 } elseif (strpos($uri, '/fr-ca/') !== false) {
173 $expectedPageTitle = 'FR-CA: Welcome';
174 } else {
175 $expectedPageTitle = 'EN: Welcome';
176 }
177 return [$uri, $expectedPageTitle];
178 },
179 $this->keysFromValues(
180 $this->meltStrings([$domainPaths, $languagePaths, $queries])
181 )
182 );
183 }
184
185 /**
186 * @param string $uri
187 * @param string $expectedPageTitle
188 *
189 * @test
190 * @dataProvider pageIsRenderedWithPathsDataProvider
191 */
192 public function pageIsRenderedWithPaths(string $uri, string $expectedPageTitle)
193 {
194 $this->writeSiteConfiguration(
195 'website-local',
196 $this->buildSiteConfiguration(101, 'https://website.local/'),
197 [
198 $this->buildDefaultLanguageConfiguration('EN', '/en-en/'),
199 $this->buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
200 $this->buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
201 ]
202 );
203
204 $response = $this->executeFrontendRequest(
205 new InternalRequest($uri),
206 $this->internalRequestContext
207 );
208 $responseStructure = ResponseContent::fromString(
209 (string)$response->getBody()
210 );
211
212 static::assertSame(
213 200,
214 $response->getStatusCode()
215 );
216 static::assertSame(
217 $this->siteTitle,
218 $responseStructure->getScopePath('template/sitetitle')
219 );
220 static::assertSame(
221 $expectedPageTitle,
222 $responseStructure->getScopePath('page/title')
223 );
224 }
225
226 /**
227 * @return array
228 */
229 public function pageIsRenderedWithDomainsDataProvider(): array
230 {
231 $domainPaths = [
232 'https://website.local/',
233 'https://website.us/',
234 'https://website.fr/',
235 'https://website.ca/',
236 'https://website.other/',
237 ];
238
239 $queries = [
240 '?id=102',
241 '?id=acme-first',
242 ];
243
244 return array_map(
245 function (string $uri) {
246 if (strpos($uri, '.fr/') !== false) {
247 $expectedPageTitle = 'FR: Welcome';
248 } elseif (strpos($uri, '.ca/') !== false) {
249 $expectedPageTitle = 'FR-CA: Welcome';
250 } else {
251 $expectedPageTitle = 'EN: Welcome';
252 }
253 return [$uri, $expectedPageTitle];
254 },
255 $this->keysFromValues(
256 $this->meltStrings([$domainPaths, $queries])
257 )
258 );
259 }
260
261 /**
262 * @param string $uri
263 * @param string $expectedPageTitle
264 *
265 * @test
266 * @dataProvider pageIsRenderedWithDomainsDataProvider
267 */
268 public function pageIsRenderedWithDomains(string $uri, string $expectedPageTitle)
269 {
270 $this->writeSiteConfiguration(
271 'website-local',
272 $this->buildSiteConfiguration(101, 'https://website.local/'),
273 [
274 $this->buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
275 $this->buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
276 $this->buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
277 ]
278 );
279
280 $response = $this->executeFrontendRequest(
281 new InternalRequest($uri),
282 $this->internalRequestContext
283 );
284 $responseStructure = ResponseContent::fromString(
285 (string)$response->getBody()
286 );
287
288 static::assertSame(
289 200,
290 $response->getStatusCode()
291 );
292 static::assertSame(
293 $this->siteTitle,
294 $responseStructure->getScopePath('template/sitetitle')
295 );
296 static::assertSame(
297 $expectedPageTitle,
298 $responseStructure->getScopePath('page/title')
299 );
300 }
301
302 /**
303 * @return array
304 */
305 public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
306 {
307 $domainPaths = [
308 'https://website.local/',
309 ];
310
311 $queries = [
312 '?',
313 '?id=101',
314 '?id=acme-root',
315 '?id=102',
316 '?id=acme-first',
317 ];
318
319 $customQueries = [
320 '&testing[value]=1',
321 '&testing[value]=1&cHash=',
322 '&testing[value]=1&cHash=WRONG',
323 ];
324
325 return $this->wrapInArray(
326 $this->keysFromValues(
327 $this->meltStrings([$domainPaths, $queries, $customQueries])
328 )
329 );
330 }
331
332 /**
333 * @param string $uri
334 *
335 * @test
336 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
337 * @todo In case no error handler is defined, default handler should be used
338 * @see PlainRequestTest::pageRequestSendsNotFoundResponseWithInvalidCacheHash
339 */
340 public function pageRequestThrowsExceptionWithInvalidCacheHashWithoutHavingErrorHandling(string $uri)
341 {
342 $this->writeSiteConfiguration(
343 'website-local',
344 $this->buildSiteConfiguration(101, 'https://website.local/')
345 );
346
347 $this->expectExceptionCode(1522495914);
348 $this->expectException(\RuntimeException::class);
349
350 $this->executeFrontendRequest(
351 new InternalRequest($uri),
352 $this->internalRequestContext
353 );
354 }
355
356 /**
357 * @param string $uri
358 *
359 * @test
360 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
361 */
362 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingFluidErrorHandling(string $uri)
363 {
364 $this->writeSiteConfiguration(
365 'website-local',
366 $this->buildSiteConfiguration(101, 'https://website.local/'),
367 [],
368 $this->buildErrorHandlingConfiguration('Fluid', [404])
369 );
370
371 $response = $this->executeFrontendRequest(
372 new InternalRequest($uri),
373 $this->internalRequestContext
374 );
375
376 static::assertSame(
377 404,
378 $response->getStatusCode()
379 );
380 static::assertThat(
381 (string)$response->getBody(),
382 static::logicalOr(
383 static::stringContains('message: Request parameters could not be validated (&amp;cHash empty)'),
384 static::stringContains('message: Request parameters could not be validated (&amp;cHash comparison failed)')
385 )
386 );
387 }
388
389 /**
390 * @param string $uri
391 *
392 * @test
393 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
394 * @todo Response body cannot be asserted since PageContentErrorHandler::handlePageError executes request via HTTP (not internally)
395 */
396 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPageErrorHandling(string $uri)
397 {
398 $this->writeSiteConfiguration(
399 'website-local',
400 $this->buildSiteConfiguration(101, 'https://website.local/'),
401 [],
402 $this->buildErrorHandlingConfiguration('Page', [404])
403 );
404
405 $response = $this->executeFrontendRequest(
406 new InternalRequest($uri),
407 $this->internalRequestContext
408 );
409
410 static::assertSame(
411 404,
412 $response->getStatusCode()
413 );
414 }
415
416 /**
417 * @param string $uri
418 *
419 * @test
420 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
421 */
422 public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPhpErrorHandling(string $uri)
423 {
424 $this->writeSiteConfiguration(
425 'website-local',
426 $this->buildSiteConfiguration(101, 'https://website.local/'),
427 [],
428 $this->buildErrorHandlingConfiguration('PHP', [404])
429 );
430
431 $response = $this->executeFrontendRequest(
432 new InternalRequest($uri),
433 $this->internalRequestContext
434 );
435 $json = json_decode((string)$response->getBody(), true);
436
437 static::assertSame(
438 404,
439 $response->getStatusCode()
440 );
441 static::assertThat(
442 $json['message'] ?? null,
443 static::logicalOr(
444 static::identicalTo('Request parameters could not be validated (&cHash empty)'),
445 static::identicalTo('Request parameters could not be validated (&cHash comparison failed)')
446 )
447 );
448 }
449
450 /**
451 * @return array
452 */
453 public function pageIsRenderedWithValidCacheHashDataProvider(): array
454 {
455 $domainPaths = [
456 '/',
457 'https://localhost/',
458 'https://website.local/',
459 ];
460
461 // cHash has been calculated with encryption key set to
462 // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
463 $queries = [
464 // @todo Currently fails since cHash is verified after(!) redirect to page 102
465 // '?&cHash=814ea11ad629c7e24cfd031cea2779f4&id=101',
466 // '?&cHash=814ea11ad629c7e24cfd031cea2779f4id=acme-root',
467 '?&cHash=126d2980c12f4759fed1bb7429db2dff&id=102',
468 '?&cHash=126d2980c12f4759fed1bb7429db2dff&id=acme-first',
469 ];
470
471 $customQueries = [
472 '&testing[value]=1',
473 ];
474
475 $dataSet = $this->wrapInArray(
476 $this->keysFromValues(
477 $this->meltStrings([$domainPaths, $queries, $customQueries])
478 )
479 );
480
481 return $dataSet;
482 }
483
484 /**
485 * @param string $uri
486 *
487 * @test
488 * @dataProvider pageIsRenderedWithValidCacheHashDataProvider
489 */
490 public function pageIsRenderedWithValidCacheHash($uri)
491 {
492 $this->writeSiteConfiguration(
493 'website-local',
494 $this->buildSiteConfiguration(101, 'https://website.local/')
495 );
496
497 $response = $this->executeFrontendRequest(
498 new InternalRequest($uri),
499 $this->internalRequestContext
500 );
501 $responseStructure = ResponseContent::fromString(
502 (string)$response->getBody()
503 );
504 static::assertSame(
505 '1',
506 $responseStructure->getScopePath('getpost/testing.value')
507 );
508 }
509
510 /**
511 * @param string $identifier
512 * @param array $site
513 * @param array $languages
514 * @param array $errorHandling
515 */
516 private function writeSiteConfiguration(
517 string $identifier,
518 array $site = [],
519 array $languages = [],
520 array $errorHandling = []
521 ) {
522 $configuration = [
523 'site' => $site,
524 ];
525 if (!empty($languages)) {
526 $configuration['site']['languages'] = $languages;
527 }
528 if (!empty($errorHandling)) {
529 $configuration['site']['errorHandling'] = $errorHandling;
530 }
531
532 $siteConfiguration = new SiteConfiguration(
533 $this->instancePath . '/typo3conf/sites/'
534 );
535
536 try {
537 $siteConfiguration->write($identifier, $configuration);
538 } catch (\Exception $exception) {
539 $this->markTestSkipped($exception->getMessage());
540 }
541 }
542
543 /**
544 * @param int $rootPageId
545 * @param string $base
546 * @return array
547 */
548 private function buildSiteConfiguration(
549 int $rootPageId,
550 string $base = ''
551 ): array {
552 return [
553 'rootPageId' => $rootPageId,
554 'base' => $base,
555 ];
556 }
557
558 /**
559 * @param string $identifier
560 * @param string $base
561 * @return array
562 */
563 private function buildDefaultLanguageConfiguration(
564 string $identifier,
565 string $base
566 ): array {
567 $configuration = $this->buildLanguageConfiguration($identifier, $base);
568 $configuration['typo3Language'] = 'default';
569 $configuration['flag'] = 'global';
570 unset($configuration['fallbackType']);
571 return $configuration;
572 }
573
574 /**
575 * @param string $identifier
576 * @param string $base
577 * @param array $fallbackIdentifiers
578 * @return array
579 */
580 private function buildLanguageConfiguration(
581 string $identifier,
582 string $base,
583 array $fallbackIdentifiers = []
584 ): array {
585 $preset = $this->resolveLanguagePreset($identifier);
586
587 $configuration = [
588 'languageId' => $preset['id'],
589 'title' => $preset['title'],
590 'navigationTitle' => $preset['title'],
591 'base' => $base,
592 'locale' => $preset['locale'],
593 'iso-639-1' => $preset['iso'],
594 'hreflang' => $preset['hrefLang'],
595 'direction' => $preset['direction'],
596 'typo3Language' => $preset['iso'],
597 'flag' => $preset['iso'],
598 'fallbackType' => 'strict',
599 ];
600
601 if (!empty($fallbackIdentifiers)) {
602 $fallbackIds = array_map(
603 function (string $fallbackIdentifier) {
604 $preset = $this->resolveLanguagePreset($fallbackIdentifier);
605 return $preset['id'];
606 },
607 $fallbackIdentifiers
608 );
609 $configuration['fallbackType'] = 'fallback';
610 $configuration['fallbackType'] = implode(',', $fallbackIds);
611 }
612
613 return $configuration;
614 }
615
616 /**
617 * @param string $handler
618 * @param array $codes
619 * @return array
620 */
621 private function buildErrorHandlingConfiguration(
622 string $handler,
623 array $codes
624 ): array {
625 if ($handler === 'Page') {
626 $baseConfiguration = [
627 'errorContentSource' => '404',
628 ];
629 } elseif ($handler === 'Fluid') {
630 $baseConfiguration = [
631 'errorFluidTemplate' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html',
632 'errorFluidTemplatesRootPath' => '',
633 'errorFluidLayoutsRootPath' => '',
634 'errorFluidPartialsRootPath' => '',
635 ];
636 } elseif ($handler === 'PHP') {
637 $baseConfiguration = [
638 'errorPhpClassFQCN' => PhpError::class,
639 ];
640 } else {
641 throw new \LogicException(
642 sprintf('Invalid handler "%s"', $handler),
643 1533894782
644 );
645 }
646
647 $baseConfiguration['errorHandler'] = $handler;
648
649 return array_map(
650 function (int $code) use ($baseConfiguration) {
651 $baseConfiguration['errorCode'] = $code;
652 return $baseConfiguration;
653 },
654 $codes
655 );
656 }
657
658 /**
659 * @param string $identifier
660 * @return mixed
661 */
662 private function resolveLanguagePreset(string $identifier)
663 {
664 if (!isset(static::LANGUAGE_PRESETS[$identifier])) {
665 throw new \LogicException(
666 sprintf('Undefined preset identifier "%s"', $identifier),
667 1533893665
668 );
669 }
670 return static::LANGUAGE_PRESETS[$identifier];
671 }
672 }