123655d5e2b6c18e3907b8c57e7f890810a84f9d
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / SiteHandling / PlainRequestTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
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 TYPO3\CMS\Core\Core\Bootstrap;
19 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
20 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
21 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
22 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
23 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
24 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
25
26 /**
27 * Test case for frontend requests without having site handling configured
28 */
29 class PlainRequestTest extends AbstractTestCase
30 {
31 /**
32 * @var string
33 */
34 private $siteTitle = 'A Company that Manufactures Everything Inc';
35
36 /**
37 * @var InternalRequestContext
38 */
39 private $internalRequestContext;
40
41 public static function setUpBeforeClass()
42 {
43 parent::setUpBeforeClass();
44 static::initializeDatabaseSnapshot();
45 }
46
47 public static function tearDownAfterClass()
48 {
49 static::destroyDatabaseSnapshot();
50 parent::tearDownAfterClass();
51 }
52
53 protected function setUp()
54 {
55 parent::setUp();
56
57 // these settings are forwarded to the frontend sub-request as well
58 $this->internalRequestContext = (new InternalRequestContext())
59 ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
60
61 $this->withDatabaseSnapshot(function () {
62 $this->setUpDatabase();
63 });
64 }
65
66 protected function setUpDatabase()
67 {
68 $backendUser = $this->setUpBackendUserFromFixture(1);
69 Bootstrap::initializeLanguageObject();
70
71 $scenarioFile = __DIR__ . '/Fixtures/PlainScenario.yaml';
72 $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
73 $writer = DataHandlerWriter::withBackendUser($backendUser);
74 $writer->invokeFactory($factory);
75 static::failIfArrayIsNotEmpty(
76 $writer->getErrors()
77 );
78
79 $this->setUpFrontendRootPage(
80 1000,
81 [
82 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
83 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
84 ],
85 [
86 'title' => 'ACME Root',
87 'sitetitle' => $this->siteTitle,
88 ]
89 );
90 $this->setUpFrontendRootPage(
91 3000,
92 [
93 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
94 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
95 ],
96 [
97 'title' => 'ACME Archive',
98 'sitetitle' => $this->siteTitle,
99 ]
100 );
101 }
102
103 protected function tearDown()
104 {
105 unset($this->internalRequestContext);
106 parent::tearDown();
107 }
108
109 /**
110 * @return array
111 */
112 public function shortcutsAreRedirectedDataProvider(): array
113 {
114 $domainPaths = [
115 '/',
116 'https://localhost/',
117 'https://website.local/',
118 ];
119
120 $queries = [
121 '?',
122 '?id=1000',
123 ];
124
125 return $this->wrapInArray(
126 $this->keysFromValues(
127 $this->meltStrings([$domainPaths, $queries])
128 )
129 );
130 }
131
132 /**
133 * @param string $uri
134 *
135 * @test
136 * @dataProvider shortcutsAreRedirectedDataProvider
137 */
138 public function shortcutsAreRedirectedToFirstSubPage(string $uri)
139 {
140 $expectedStatusCode = 307;
141 $expectedHeaders = ['location' => ['index.php?id=1100']];
142
143 $response = $this->executeFrontendRequest(
144 new InternalRequest($uri),
145 $this->internalRequestContext
146 );
147 static::assertSame($expectedStatusCode, $response->getStatusCode());
148 static::assertSame($expectedHeaders, $response->getHeaders());
149 }
150
151 /**
152 * @param string $uri
153 *
154 * @test
155 * @dataProvider shortcutsAreRedirectedDataProvider
156 */
157 public function shortcutsAreRedirectedAndRenderFirstSubPage(string $uri)
158 {
159 $expectedStatusCode = 200;
160 $expectedPageTitle = 'EN: Welcome';
161
162 $response = $this->executeFrontendRequest(
163 new InternalRequest($uri),
164 $this->internalRequestContext,
165 true
166 );
167 $responseStructure = ResponseContent::fromString(
168 (string)$response->getBody()
169 );
170
171 static::assertSame(
172 $expectedStatusCode,
173 $response->getStatusCode()
174 );
175 static::assertSame(
176 $this->siteTitle,
177 $responseStructure->getScopePath('template/sitetitle')
178 );
179 static::assertSame(
180 $expectedPageTitle,
181 $responseStructure->getScopePath('page/title')
182 );
183 }
184
185 /**
186 * @return array
187 */
188 public function pageIsRenderedDataProvider(): array
189 {
190 $domainPaths = [
191 '/',
192 'https://localhost/',
193 'https://website.local/',
194 ];
195
196 $queries = [
197 '?id=1100',
198 ];
199
200 $languageQueries = [
201 '',
202 '&L=0',
203 '&L=1',
204 '&L=2',
205 ];
206
207 return array_map(
208 function (string $uri) {
209 if (strpos($uri, '&L=1') !== false) {
210 $expectedPageTitle = 'FR: Welcome';
211 } elseif (strpos($uri, '&L=2') !== false) {
212 $expectedPageTitle = 'FR-CA: Welcome';
213 } else {
214 $expectedPageTitle = 'EN: Welcome';
215 }
216 return [$uri, $expectedPageTitle];
217 },
218 $this->keysFromValues(
219 $this->meltStrings([$domainPaths, $queries, $languageQueries])
220 )
221 );
222 }
223
224 /**
225 * @param string $uri
226 * @param string $expectedPageTitle
227 *
228 * @test
229 * @dataProvider pageIsRenderedDataProvider
230 */
231 public function pageIsRendered(string $uri, string $expectedPageTitle)
232 {
233 $response = $this->executeFrontendRequest(
234 new InternalRequest($uri),
235 $this->internalRequestContext
236 );
237 $responseStructure = ResponseContent::fromString(
238 (string)$response->getBody()
239 );
240
241 static::assertSame(
242 200,
243 $response->getStatusCode()
244 );
245 static::assertSame(
246 $this->siteTitle,
247 $responseStructure->getScopePath('template/sitetitle')
248 );
249 static::assertSame(
250 $expectedPageTitle,
251 $responseStructure->getScopePath('page/title')
252 );
253 }
254
255 /**
256 * @return array
257 */
258 public function pageIsRenderedWithDomainsDataProvider(): array
259 {
260 $instructions = [
261 ['https://archive.acme.com/?id=3100', 'EN: Statistics'],
262 ['https://archive.acme.com/?id=3110', 'EN: Markets'],
263 ['https://archive.acme.com/?id=3120', 'EN: Products'],
264 ['https://archive.acme.com/?id=3130', 'EN: Partners'],
265 ];
266
267 return $this->keysFromTemplate($instructions, '%1$s');
268 }
269
270 /**
271 * @param string $uri
272 * @param string $expectedPageTitle
273 *
274 * @test
275 * @dataProvider pageIsRenderedWithDomainsDataProvider
276 */
277 public function pageIsRenderedWithDomains(string $uri, string $expectedPageTitle)
278 {
279 $response = $this->executeFrontendRequest(
280 new InternalRequest($uri),
281 $this->internalRequestContext
282 );
283 $responseStructure = ResponseContent::fromString(
284 (string)$response->getBody()
285 );
286
287 static::assertSame(
288 200,
289 $response->getStatusCode()
290 );
291 static::assertSame(
292 $this->siteTitle,
293 $responseStructure->getScopePath('template/sitetitle')
294 );
295 static::assertSame(
296 $expectedPageTitle,
297 $responseStructure->getScopePath('page/title')
298 );
299 }
300
301 /**
302 * @return array
303 */
304 public function restrictedPageIsRenderedDataProvider(): array
305 {
306 $instructions = [
307 // frontend user 1
308 ['https://website.local/?id=1510', 1, 'Whitepapers'],
309 ['https://website.local/?id=1511', 1, 'Products'],
310 ['https://website.local/?id=1512', 1, 'Solutions'],
311 // frontend user 2
312 ['https://website.local/?id=1510', 2, 'Whitepapers'],
313 ['https://website.local/?id=1511', 2, 'Products'],
314 ['https://website.local/?id=1515', 2, 'Research'],
315 ['https://website.local/?id=1520', 2, 'Forecasts'],
316 ['https://website.local/?id=1521', 2, 'Current Year'],
317 // frontend user 3
318 ['https://website.local/?id=1510', 3, 'Whitepapers'],
319 ['https://website.local/?id=1511', 3, 'Products'],
320 ['https://website.local/?id=1512', 3, 'Solutions'],
321 ['https://website.local/?id=1515', 3, 'Research'],
322 ['https://website.local/?id=1520', 3, 'Forecasts'],
323 ['https://website.local/?id=1521', 3, 'Current Year'],
324 ];
325
326 return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
327 }
328
329 /**
330 * @param string $uri
331 * @param int $frontendUserId
332 * @param string $expectedPageTitle
333 *
334 * @test
335 * @dataProvider restrictedPageIsRenderedDataProvider
336 */
337 public function restrictedPageIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle)
338 {
339 $response = $this->executeFrontendRequest(
340 new InternalRequest($uri),
341 $this->internalRequestContext
342 ->withFrontendUserId($frontendUserId)
343 );
344 $responseStructure = ResponseContent::fromString(
345 (string)$response->getBody()
346 );
347
348 static::assertSame(
349 200,
350 $response->getStatusCode()
351 );
352 static::assertSame(
353 $this->siteTitle,
354 $responseStructure->getScopePath('template/sitetitle')
355 );
356 static::assertSame(
357 $expectedPageTitle,
358 $responseStructure->getScopePath('page/title')
359 );
360 }
361
362 /**
363 * @return array
364 */
365 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
366 {
367 $instructions = [
368 // no frontend user given
369 ['https://website.local/?id=1510', 0],
370 ['https://website.local/?id=1511', 0],
371 ['https://website.local/?id=1512', 0],
372 ['https://website.local/?id=1515', 0],
373 ['https://website.local/?id=1520', 0],
374 ['https://website.local/?id=1521', 0],
375 // frontend user 1
376 ['https://website.local/?id=1515', 1],
377 ['https://website.local/?id=1520', 1],
378 ['https://website.local/?id=1521', 1],
379 // frontend user 2
380 ['https://website.local/?id=1512', 2],
381 ];
382
383 return $this->keysFromTemplate($instructions, '%1$s (user:%2$s)');
384 }
385
386 /**
387 * @param string $uri
388 * @param int $frontendUserId
389 *
390 * @test
391 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
392 */
393 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorUsingDefaultErrorHandling(string $uri, int $frontendUserId)
394 {
395 $response = $this->executeFrontendRequest(
396 new InternalRequest($uri),
397 $this->internalRequestContext
398 ->withFrontendUserId($frontendUserId)
399 );
400
401 static::assertSame(
402 403,
403 $response->getStatusCode()
404 );
405 static::assertThat(
406 (string)$response->getBody(),
407 static::logicalOr(
408 static::stringContains('Reason: ID was not an accessible page'),
409 static::stringContains('Reason: Subsection was found and not accessible')
410 )
411 );
412 }
413
414 /**
415 * @param string $uri
416 * @param int $frontendUserId
417 *
418 * @test
419 * @dataProvider restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider
420 */
421 public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorUsingCustomErrorHandling(string $uri, int $frontendUserId)
422 {
423 $response = $this->executeFrontendRequest(
424 new InternalRequest($uri),
425 $this->internalRequestContext
426 ->withFrontendUserId($frontendUserId)
427 ->withMergedGlobalSettings([
428 'TYPO3_CONF_VARS' => [
429 'FE' => [
430 'pageNotFound_handling' => 'READFILE:typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/PageError.txt',
431 ]
432 ]
433 ])
434 );
435
436 static::assertSame(
437 403,
438 $response->getStatusCode()
439 );
440 static::assertThat(
441 (string)$response->getBody(),
442 static::logicalOr(
443 static::stringContains('reason: ID was not an accessible page'),
444 static::stringContains('reason: Subsection was found and not accessible')
445 )
446 );
447 }
448
449 /**
450 * @return array
451 */
452 public function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
453 {
454 $domainPaths = [
455 '/',
456 'https://localhost/',
457 'https://website.local/',
458 ];
459
460 $queries = [
461 '?',
462 '?id=1000',
463 '?id=1100',
464 ];
465
466 $customQueries = [
467 '&testing[value]=1',
468 '&testing[value]=1&cHash=',
469 '&testing[value]=1&cHash=WRONG',
470 ];
471
472 return $this->wrapInArray(
473 $this->keysFromValues(
474 $this->meltStrings([$domainPaths, $queries, $customQueries])
475 )
476 );
477 }
478
479 /**
480 * @param string $uri
481 *
482 * @test
483 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
484 * @todo In TYPO3 v8 this seemed to be rendered, without throwing that exception
485 */
486 public function pageRequestThrowsExceptionWithInvalidCacheHash(string $uri)
487 {
488 $this->expectExceptionCode(1518472189);
489 $this->expectException(PageNotFoundException::class);
490
491 $this->executeFrontendRequest(
492 new InternalRequest($uri),
493 $this->internalRequestContext
494 );
495 }
496
497 /**
498 * @param string $uri
499 *
500 * @test
501 * @dataProvider pageRenderingStopsWithInvalidCacheHashDataProvider
502 */
503 public function pageRequestSendsNotFoundResponseWithInvalidCacheHash(string $uri)
504 {
505 $response = $this->executeFrontendRequest(
506 new InternalRequest($uri),
507 $this->internalRequestContext->withMergedGlobalSettings([
508 'TYPO3_CONF_VARS' => [
509 'FE' => [
510 'pageNotFound_handling' => 'READFILE:typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/PageError.txt',
511 ]
512 ]
513 ])
514 );
515
516 static::assertSame(
517 404,
518 $response->getStatusCode()
519 );
520 static::assertThat(
521 (string)$response->getBody(),
522 static::logicalOr(
523 static::stringContains('reason: Request parameters could not be validated (&amp;cHash empty)'),
524 static::stringContains('reason: Request parameters could not be validated (&amp;cHash comparison failed)')
525 )
526 );
527 }
528
529 /**
530 * @return array
531 */
532 public function pageIsRenderedWithValidCacheHashDataProvider(): array
533 {
534 $domainPaths = [
535 '/',
536 'https://localhost/',
537 'https://website.local/',
538 ];
539
540 // cHash has been calculated with encryption key set to
541 // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
542 $queries = [
543 // @todo Currently fails since cHash is verified after(!) redirect to page 1100
544 // '?&cHash=7d1f13fa91159dac7feb3c824936b39d&id=1000',
545 '?&cHash=f42b850e435f0cedd366f5db749fc1af&id=1100',
546 ];
547
548 $customQueries = [
549 '&testing[value]=1',
550 ];
551
552 $dataSet = $this->wrapInArray(
553 $this->keysFromValues(
554 $this->meltStrings([$domainPaths, $queries, $customQueries])
555 )
556 );
557
558 return $dataSet;
559 }
560
561 /**
562 * @param string $uri
563 *
564 * @test
565 * @dataProvider pageIsRenderedWithValidCacheHashDataProvider
566 */
567 public function pageIsRenderedWithValidCacheHash($uri)
568 {
569 $response = $this->executeFrontendRequest(
570 new InternalRequest($uri),
571 $this->internalRequestContext
572 );
573 $responseStructure = ResponseContent::fromString(
574 (string)$response->getBody()
575 );
576 static::assertSame(
577 '1',
578 $responseStructure->getScopePath('getpost/testing.value')
579 );
580 }
581 }