‪TYPO3CMS  ‪main
SlugSiteRequestTest.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
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 
19 
20 use PHPUnit\Framework\Attributes\DataProvider;
21 use PHPUnit\Framework\Attributes\Test;
22 use Psr\Http\Message\UriInterface;
27 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
28 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
29 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
30 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
31 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
32 
34 {
35  protected array $configurationToUseInTestInstance = [
36  'SYS' => [
37  'devIPmask' => '123.123.123.123',
38  'encryptionKey' => '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6',
39  ],
40  'FE' => [
41  'cacheHash' => [
42  'requireCacheHashPresenceParameters' => ['value', 'testing[value]', 'tx_testing_link[value]'],
43  'excludedParameters' => ['L', 'tx_testing_link[excludedValue]'],
44  'enforceValidation' => true,
45  ],
46  'debug' => false,
47  ],
48  ];
49 
50  protected function setUp(): void
51  {
52  parent::setUp();
53  $this->withDatabaseSnapshot(function () {
54  $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
55  $backendUser = $this->setUpBackendUser(1);
56  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
57  $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml';
58  $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
59  $writer = DataHandlerWriter::withBackendUser($backendUser);
60  $writer->invokeFactory($factory);
61  static::failIfArrayIsNotEmpty($writer->getErrors());
62  $this->setUpFrontendRootPage(
63  1000,
64  [
65  'EXT:core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
66  'EXT:frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
67  ],
68  [
69  'title' => 'ACME Root',
70  ]
71  );
72  });
73  }
74 
75  public static function requestsAreRedirectedWithoutHavingDefaultSiteLanguageDataProvider(): array
76  {
77  $domainPaths = [
78  'https://website.local/',
79  'https://website.local/?',
80  // @todo: See how core should act here and activate this or have an own test for this scenario
81  // 'https://website.local//',
82  ];
83  return self::wrapInArray(
84  self::keysFromValues($domainPaths)
85  );
86  }
87 
88  #[DataProvider('requestsAreRedirectedWithoutHavingDefaultSiteLanguageDataProvider')]
89  #[Test]
90  public function requestsAreRedirectedWithoutHavingDefaultSiteLanguage(string $uri): void
91  {
93  'website-local',
94  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
95  );
96 
97  $expectedStatusCode = 307;
98  $expectedHeaders = [
99  'X-Redirect-By' => ['TYPO3 Shortcut/Mountpoint'],
100  'location' => ['https://website.local/welcome'],
101  ];
102 
103  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
104  self::assertSame($expectedStatusCode, $response->getStatusCode());
105  self::assertSame($expectedHeaders, $response->getHeaders());
106  }
107 
108  public static function shortcutsAreRedirectedDataProvider(): array
109  {
110  $domainPaths = [
111  'https://website.local/',
112  'https://website.local/?',
113  // @todo: See how core should act here and activate this or have an own test for this scenario
114  // 'https://website.local//',
115  ];
116  return self::wrapInArray(
117  self::keysFromValues($domainPaths)
118  );
119  }
120 
121  #[DataProvider('shortcutsAreRedirectedDataProvider')]
122  #[Test]
123  public function shortcutsAreRedirectedToDefaultSiteLanguage(string $uri): void
124  {
126  'website-local',
127  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
128  [
129  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
130  ]
131  );
132 
133  $expectedStatusCode = 307;
134  $expectedHeaders = [
135  'location' => ['https://website.local/en-en/'],
136  ];
137 
138  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
139  self::assertSame($expectedStatusCode, $response->getStatusCode());
140  self::assertSame($expectedHeaders, $response->getHeaders());
141  }
142 
143  #[DataProvider('shortcutsAreRedirectedDataProvider')]
144  #[Test]
145  public function shortcutsAreRedirectedAndRenderFirstSubPage(string $uri): void
146  {
148  'website-local',
149  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
150  [
151  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
152  ]
153  );
154 
155  $expectedStatusCode = 200;
156  $expectedPageTitle = 'EN: Welcome';
157 
158  $response = $this->executeFrontendSubRequest(
159  new InternalRequest($uri),
160  null,
161  true
162  );
163  $responseStructure = ResponseContent::fromString(
164  (string)$response->getBody()
165  );
166 
167  self::assertSame(
168  $expectedStatusCode,
169  $response->getStatusCode()
170  );
171  self::assertSame(
172  $expectedPageTitle,
173  $responseStructure->getScopePath('page/title')
174  );
175  }
176 
177  public static function shortcutsAreRedirectedDataProviderWithChineseCharacterInBase(): array
178  {
179  $domainPaths = [
180  'https://website.local/简',
181  'https://website.local/简?',
182  'https://website.local/简/',
183  'https://website.local/简/?',
184  ];
185  return self::wrapInArray(
186  self::keysFromValues($domainPaths)
187  );
188  }
189 
190  #[DataProvider('shortcutsAreRedirectedDataProviderWithChineseCharacterInBase')]
191  #[Test]
192  public function shortcutsAreRedirectedToDefaultSiteLanguageWithChineseCharacterInBase(string $uri): void
193  {
195  'website-local',
196  $this->‪buildSiteConfiguration(1000, 'https://website.local/简/'),
197  [
198  $this->‪buildDefaultLanguageConfiguration('ZH-CN', '/'),
199  ]
200  );
201 
202  $expectedStatusCode = 307;
203  $expectedHeaders = [
204  'X-Redirect-By' => ['TYPO3 Shortcut/Mountpoint'],
205  // We cannot expect 简 here directly, as they are rawurlencoded() in the used Symfony UrlGenerator.
206  'location' => ['https://website.local/%E7%AE%80/welcome'],
207  ];
208 
209  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
210  self::assertSame($expectedStatusCode, $response->getStatusCode());
211  self::assertSame($expectedHeaders, $response->getHeaders());
212  }
213 
214  #[DataProvider('shortcutsAreRedirectedDataProviderWithChineseCharacterInBase')]
215  #[Test]
216  public function shortcutsAreRedirectedAndRenderFirstSubPageWithChineseCharacterInBase(string $uri): void
217  {
219  'website-local',
220  $this->‪buildSiteConfiguration(1000, 'https://website.local/简/'),
221  [
222  $this->‪buildDefaultLanguageConfiguration('ZH-CN', '/'),
223  ]
224  );
225 
226  $expectedStatusCode = 200;
227  $expectedPageTitle = 'EN: Welcome';
228 
229  $response = $this->executeFrontendSubRequest(
230  new InternalRequest($uri),
231  null,
232  true
233  );
234  $responseStructure = ResponseContent::fromString(
235  (string)$response->getBody()
236  );
237 
238  self::assertSame(
239  $expectedStatusCode,
240  $response->getStatusCode()
241  );
242  self::assertSame(
243  $expectedPageTitle,
244  $responseStructure->getScopePath('page/title')
245  );
246  }
247 
248  #[Test]
249  public function invalidSiteResultsInNotFoundResponse(): void
250  {
252  'website-local',
253  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
254  [
255  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
256  ],
257  $this->‪buildErrorHandlingConfiguration('Fluid', [404])
258  );
259 
260  $uri = 'https://website.other/any/invalid/slug';
261  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
262  self::assertSame(404, $response->getStatusCode());
263  }
264 
265  public static function siteWithPageIdRequestsAreCorrectlyHandledDataProvider(): \Generator
266  {
267  yield 'valid same-site request is redirected' => ['https://website.local/?id=1000&L=0', 307];
268  yield 'valid same-site request is processed' => ['https://website.local/?id=1100&L=0', 200];
269  yield 'invalid off-site request with unknown domain is denied' => ['https://otherdomain.website.local/?id=3000&L=0', 404];
270  yield 'invalid off-site request with unknown domain and without L parameter is denied' => ['https://otherdomain.website.local/?id=3000', 404];
271  }
272 
278  #[DataProvider('siteWithPageIdRequestsAreCorrectlyHandledDataProvider')]
279  #[Test]
280  public function siteWithPageIdRequestsAreCorrectlyHandled(string $uri, int $expectation): void
281  {
283  'website-local',
284  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
285  [
286  $this->‪buildDefaultLanguageConfiguration('EN', '/'),
287  ],
288  $this->‪buildErrorHandlingConfiguration('Fluid', [404])
289  );
290 
291  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
292  self::assertSame($expectation, $response->getStatusCode());
293  }
294 
295  #[Test]
296  public function invalidSlugOutsideSiteLanguageResultsInNotFoundResponse(): void
297  {
299  'website-local',
300  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
301  [
302  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
303  ],
304  $this->‪buildErrorHandlingConfiguration('Fluid', [404])
305  );
306 
307  $uri = 'https://website.local/any/invalid/slug';
308  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
309 
310  self::assertSame(
311  404,
312  $response->getStatusCode()
313  );
314  self::assertStringContainsString(
315  'message: The requested page does not exist',
316  (string)$response->getBody()
317  );
318  }
319 
320  #[Test]
321  public function invalidSlugInsideSiteLanguageResultsInNotFoundResponse(): void
322  {
324  'website-local',
325  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
326  [
327  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
328  ],
329  $this->‪buildErrorHandlingConfiguration('Fluid', [404])
330  );
331 
332  $uri = 'https://website.local/en-en/any/invalid/slug';
333  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
334 
335  self::assertSame(
336  404,
337  $response->getStatusCode()
338  );
339  self::assertStringContainsString(
340  'message: The requested page does not exist',
341  (string)$response->getBody()
342  );
343  }
344 
345  #[Test]
346  public function unconfiguredTypeNumResultsIn500Error(): void
347  {
349  'website-local',
350  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
351  [
352  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
353  ],
354  $this->‪buildErrorHandlingConfiguration('Fluid', [500])
355  );
356 
357  $uri = 'https://website.local/en-en/?type=13';
358  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
359 
360  self::assertSame(
361  500,
362  $response->getStatusCode()
363  );
364  self::assertStringContainsString(
365  'message: No page configured for type=13.',
366  (string)$response->getBody()
367  );
368  }
369 
370  public static function pageIsRenderedWithPathsDataProvider(): array
371  {
372  $domainPaths = [
373  'https://website.local/en-en/welcome',
374  'https://website.local/fr-fr/bienvenue',
375  'https://website.local/fr-ca/bienvenue',
376  'https://website.local/简/简-bienvenue',
377  ];
378  return array_map(
379  static function (string $uri) {
380  if (str_contains($uri, '/fr-fr/')) {
381  $expectedPageTitle = 'FR: Welcome';
382  } elseif (str_contains($uri, '/fr-ca/')) {
383  $expectedPageTitle = 'FR-CA: Welcome';
384  } elseif (str_contains($uri, '/简/')) {
385  $expectedPageTitle = 'ZH-CN: Welcome';
386  } else {
387  $expectedPageTitle = 'EN: Welcome';
388  }
389  return [$uri, $expectedPageTitle];
390  },
391  self::keysFromValues($domainPaths)
392  );
393  }
394 
395  #[DataProvider('pageIsRenderedWithPathsDataProvider')]
396  #[Test]
397  public function pageIsRenderedWithPaths(string $uri, string $expectedPageTitle): void
398  {
400  'website-local',
401  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
402  [
403  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
404  $this->‪buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
405  $this->‪buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
406  $this->‪buildLanguageConfiguration('ZH', '/简/', ['EN']),
407  ]
408  );
409 
410  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
411  $responseStructure = ResponseContent::fromString(
412  (string)$response->getBody()
413  );
414 
415  self::assertSame(
416  200,
417  $response->getStatusCode()
418  );
419  self::assertSame(
420  $expectedPageTitle,
421  $responseStructure->getScopePath('page/title')
422  );
423  }
424 
425  public static function pageIsRenderedWithPathsAndChineseDefaultLanguageDataProvider(): array
426  {
427  $domainPaths = [
428  'https://website.local/简/简-bienvenue',
429  'https://website.local/fr-fr/zh-bienvenue',
430  'https://website.local/fr-ca/zh-bienvenue',
431  ];
432  return array_map(
433  static function (string $uri) {
434  if (str_contains($uri, '/fr-fr/')) {
435  $expectedPageTitle = 'FR: Welcome ZH Default';
436  } elseif (str_contains($uri, '/fr-ca/')) {
437  $expectedPageTitle = 'FR-CA: Welcome ZH Default';
438  } else {
439  $expectedPageTitle = 'ZH-CN: Welcome Default';
440  }
441  return [$uri, $expectedPageTitle];
442  },
443  self::keysFromValues($domainPaths)
444  );
445  }
446 
447  #[DataProvider('pageIsRenderedWithPathsAndChineseDefaultLanguageDataProvider')]
448  #[Test]
449  public function pageIsRenderedWithPathsAndChineseDefaultLanguage(string $uri, string $expectedPageTitle): void
450  {
452  'website-local',
453  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
454  [
455  $this->‪buildDefaultLanguageConfiguration('ZH-CN', '/简/'),
456  $this->‪buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
457  $this->‪buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
458  ]
459  );
460 
461  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
462  $responseStructure = ResponseContent::fromString(
463  (string)$response->getBody()
464  );
465 
466  self::assertSame(
467  200,
468  $response->getStatusCode()
469  );
470  self::assertSame(
471  $expectedPageTitle,
472  $responseStructure->getScopePath('page/title')
473  );
474  }
475 
476  public static function pageIsRenderedWithDomainsDataProvider(): array
477  {
478  $domainPaths = [
479  'https://website.us/welcome',
480  'https://website.fr/bienvenue',
481  'https://website.ca/bienvenue',
482  // Explicitly testing chinese character domains
483  'https://website.简/简-bienvenue',
484  ];
485  return array_map(
486  static function (string $uri) {
487  if (str_contains($uri, '.fr/')) {
488  $expectedPageTitle = 'FR: Welcome';
489  } elseif (str_contains($uri, '.ca/')) {
490  $expectedPageTitle = 'FR-CA: Welcome';
491  } elseif (str_contains($uri, '.简/')) {
492  $expectedPageTitle = 'ZH-CN: Welcome';
493  } else {
494  $expectedPageTitle = 'EN: Welcome';
495  }
496  return [$uri, $expectedPageTitle];
497  },
498  self::keysFromValues($domainPaths)
499  );
500  }
501 
502  #[DataProvider('pageIsRenderedWithDomainsDataProvider')]
503  #[Test]
504  public function pageIsRenderedWithDomains(string $uri, string $expectedPageTitle): void
505  {
507  'website-local',
508  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
509  [
510  $this->‪buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
511  $this->‪buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
512  $this->‪buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
513  $this->‪buildLanguageConfiguration('ZH', 'https://website.简/', ['EN']),
514  ]
515  );
516 
517  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
518  $responseStructure = ResponseContent::fromString(
519  (string)$response->getBody()
520  );
521 
522  self::assertSame(
523  200,
524  $response->getStatusCode()
525  );
526  self::assertSame(
527  $expectedPageTitle,
528  $responseStructure->getScopePath('page/title')
529  );
530  }
531 
532  #[Test]
533  public function pageWithTrailingSlashSlugIsRenderedIfRequestedWithSlash(): void
534  {
535  $uri = 'https://website.us/features/frontend-editing/';
536 
538  'website-local',
539  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
540  [
541  $this->‪buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
542  $this->‪buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
543  $this->‪buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
544  ]
545  );
546 
547  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
548  $responseStructure = ResponseContent::fromString((string)$response->getBody());
549  self::assertSame(200, $response->getStatusCode());
550  self::assertSame('EN: Frontend Editing', $responseStructure->getScopePath('page/title'));
551  }
552 
553  #[Test]
554  public function pageWithTrailingSlashSlugIsRenderedIfRequestedWithoutSlash(): void
555  {
556  $uri = 'https://website.us/features/frontend-editing';
557 
559  'website-local',
560  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
561  [
562  $this->‪buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
563  $this->‪buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
564  $this->‪buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
565  ]
566  );
567 
568  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
569  $responseStructure = ResponseContent::fromString((string)$response->getBody());
570  self::assertSame(200, $response->getStatusCode());
571  self::assertSame('EN: Frontend Editing', $responseStructure->getScopePath('page/title'));
572  }
573 
574  #[Test]
575  public function pageWithoutTrailingSlashSlugIsRenderedIfRequestedWithSlash(): void
576  {
577  $uri = 'https://website.us/features/';
578 
580  'website-local',
581  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
582  [
583  $this->‪buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
584  $this->‪buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
585  $this->‪buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
586  ]
587  );
588 
589  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
590  $responseStructure = ResponseContent::fromString((string)$response->getBody());
591  self::assertSame(200, $response->getStatusCode());
592  self::assertSame('EN: Features', $responseStructure->getScopePath('page/title'));
593  }
594 
595  #[Test]
596  public function pageWithoutTrailingSlashSlugIsRenderedIfRequestedWithoutSlash(): void
597  {
598  $uri = 'https://website.us/features';
599 
601  'website-local',
602  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
603  [
604  $this->‪buildDefaultLanguageConfiguration('EN', 'https://website.us/'),
605  $this->‪buildLanguageConfiguration('FR', 'https://website.fr/', ['EN']),
606  $this->‪buildLanguageConfiguration('FR-CA', 'https://website.ca/', ['FR', 'EN']),
607  ]
608  );
609 
610  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
611  $responseStructure = ResponseContent::fromString((string)$response->getBody());
612  self::assertSame(200, $response->getStatusCode());
613  self::assertSame('EN: Features', $responseStructure->getScopePath('page/title'));
614  }
615 
616  public static function restrictedPageIsRenderedDataProvider(): array
617  {
618  $instructions = [
619  // frontend user 1
620  ['https://website.local/my-acme/whitepapers', 1, 'Whitepapers'],
621  ['https://website.local/my-acme/whitepapers/products', 1, 'Products'],
622  ['https://website.local/my-acme/whitepapers/solutions', 1, 'Solutions'],
623  // frontend user 2
624  ['https://website.local/my-acme/whitepapers', 2, 'Whitepapers'],
625  ['https://website.local/my-acme/whitepapers/products', 2, 'Products'],
626  ['https://website.local/my-acme/whitepapers/research', 2, 'Research'],
627  ['https://website.local/my-acme/forecasts', 2, 'Forecasts'],
628  ['https://website.local/my-acme/forecasts/current-year', 2, 'Current Year'],
629  // frontend user 3
630  ['https://website.local/my-acme/whitepapers', 3, 'Whitepapers'],
631  ['https://website.local/my-acme/whitepapers/products', 3, 'Products'],
632  ['https://website.local/my-acme/whitepapers/solutions', 3, 'Solutions'],
633  ['https://website.local/my-acme/whitepapers/research', 3, 'Research'],
634  ['https://website.local/my-acme/forecasts', 3, 'Forecasts'],
635  ['https://website.local/my-acme/forecasts/current-year', 3, 'Current Year'],
636  ];
637  return self::keysFromTemplate($instructions, '%1$s (user:%2$s)');
638  }
639 
640  #[DataProvider('restrictedPageIsRenderedDataProvider')]
641  #[Test]
642  public function restrictedPageIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle): void
643  {
645  'website-local',
646  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
647  );
648 
649  $response = $this->executeFrontendSubRequest(
650  new InternalRequest($uri),
651  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
652  );
653  $responseStructure = ResponseContent::fromString(
654  (string)$response->getBody()
655  );
656 
657  self::assertSame(
658  200,
659  $response->getStatusCode()
660  );
661  self::assertSame(
662  $expectedPageTitle,
663  $responseStructure->getScopePath('page/title')
664  );
665  }
666 
667  public static function restrictedPageWithParentSysFolderIsRenderedDataProvider(): array
668  {
669  $instructions = [
670  // frontend user 4
671  ['https://website.local/sysfolder-restricted', 4, 'FEGroups Restricted'],
672  ];
673  return self::keysFromTemplate($instructions, '%1$s (user:%2$s)');
674  }
675 
676  #[DataProvider('restrictedPageWithParentSysFolderIsRenderedDataProvider')]
677  #[Test]
678  public function restrictedPageWithParentSysFolderIsRendered(string $uri, int $frontendUserId, string $expectedPageTitle): void
679  {
681  'website-local',
682  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
683  );
684 
685  $response = $this->executeFrontendSubRequest(
686  new InternalRequest($uri),
687  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
688  );
689  $responseStructure = ResponseContent::fromString(
690  (string)$response->getBody()
691  );
692 
693  self::assertSame(
694  200,
695  $response->getStatusCode()
696  );
697  self::assertSame(
698  $expectedPageTitle,
699  $responseStructure->getScopePath('page/title')
700  );
701  }
702 
703  public static function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
704  {
705  $instructions = [
706  // no frontend user given
707  ['https://website.local/my-acme/whitepapers', 0],
708  // ['https://website.local/my-acme/whitepapers/products', 0], // @todo extendToSubpages currently missing
709  ['https://website.local/my-acme/whitepapers/solutions', 0],
710  ['https://website.local/my-acme/whitepapers/research', 0],
711  ['https://website.local/my-acme/forecasts', 0],
712  // ['https://website.local/my-acme/forecasts/current-year', 0], // @todo extendToSubpages currently missing
713  // frontend user 1
714  ['https://website.local/my-acme/whitepapers/research', 1],
715  ['https://website.local/my-acme/forecasts', 1],
716  // ['https://website.local/my-acme/forecasts/current-year', 1], // @todo extendToSubpages currently missing
717  // frontend user 2
718  ['https://website.local/my-acme/whitepapers/solutions', 2],
719  ];
720  return self::keysFromTemplate($instructions, '%1$s (user:%2$s)');
721  }
722 
723  #[DataProvider('restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
724  #[Test]
725  public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithoutHavingErrorHandling(string $uri, int $frontendUserId): void
726  {
728  'website-local',
729  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
730  );
731 
732  $response = $this->executeFrontendSubRequest(
733  new InternalRequest($uri),
734  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
735  );
736 
737  self::assertSame(
738  403,
739  $response->getStatusCode()
740  );
741  self::assertThat(
742  (string)$response->getBody(),
743  self::logicalOr(
744  self::stringContains('Reason: ID was not an accessible page'),
745  self::stringContains('Reason: Subsection was found and not accessible')
746  )
747  );
748  }
749 
750  #[DataProvider('restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
751  #[Test]
752  public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingFluidErrorHandling(string $uri, int $frontendUserId): void
753  {
755  'website-local',
756  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
757  [],
758  $this->‪buildErrorHandlingConfiguration('Fluid', [403])
759  );
760 
761  $response = $this->executeFrontendSubRequest(
762  new InternalRequest($uri),
763  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
764  );
765 
766  self::assertSame(
767  403,
768  $response->getStatusCode()
769  );
770  self::assertStringContainsString(
771  'reasons: code',
772  (string)$response->getBody()
773  );
774  self::assertThat(
775  (string)$response->getBody(),
776  self::logicalOr(
777  self::stringContains('message: ID was not an accessible page'),
778  self::stringContains('message: Subsection was found and not accessible')
779  )
780  );
781  }
782 
783  #[DataProvider('restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
784  #[Test]
785  public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPageErrorHandling(string $uri, int $frontendUserId): void
786  {
788  'website-local',
789  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
790  [],
791  $this->‪buildErrorHandlingConfiguration('Page', [403])
792  );
793 
794  $response = $this->executeFrontendSubRequest(
795  new InternalRequest($uri),
796  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
797  );
798 
799  self::assertSame(
800  403,
801  $response->getStatusCode()
802  );
803  self::assertThat(
804  (string)$response->getBody(),
805  self::logicalOr(
806  self::stringContains('That page is forbidden to you'),
807  self::stringContains('ID was not an accessible page'),
808  self::stringContains('Subsection was found and not accessible')
809  )
810  );
811  }
812 
813  #[DataProvider('restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
814  #[Test]
815  public function restrictedPageSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPhpErrorHandling(string $uri, int $frontendUserId): void
816  {
818  'website-local',
819  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
820  [],
821  $this->‪buildErrorHandlingConfiguration('PHP', [403])
822  );
823 
824  $response = $this->executeFrontendSubRequest(
825  new InternalRequest($uri),
826  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
827  );
828  $json = json_decode((string)$response->getBody(), true);
829 
830  self::assertSame(
831  403,
832  $response->getStatusCode()
833  );
834  self::assertThat(
835  $json['message'] ?? null,
836  self::logicalOr(
837  self::identicalTo('ID was not an accessible page'),
838  self::identicalTo('Subsection was found and not accessible')
839  )
840  );
841  }
842 
843  public static function restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorDataProvider(): array
844  {
845  $instructions = [
846  // no frontend user given
847  ['https://website.local/sysfolder-restricted', 0],
848  // frontend user 1
849  ['https://website.local/sysfolder-restricted', 1],
850  // frontend user 2
851  ['https://website.local/sysfolder-restricted', 2],
852  // frontend user 3
853  ['https://website.local/sysfolder-restricted', 3],
854  ];
855  return self::keysFromTemplate($instructions, '%1$s (user:%2$s)');
856  }
857 
858  #[DataProvider('restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
859  #[Test]
860  public function restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorWithoutHavingErrorHandling(string $uri, int $frontendUserId): void
861  {
863  'website-local',
864  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
865  );
866 
867  $response = $this->executeFrontendSubRequest(
868  new InternalRequest($uri),
869  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
870  );
871 
872  self::assertSame(
873  403,
874  $response->getStatusCode()
875  );
876  self::assertThat(
877  (string)$response->getBody(),
878  self::logicalOr(
879  self::stringContains('Reason: ID was not an accessible page'),
880  self::stringContains('Reason: Subsection was found and not accessible')
881  )
882  );
883  }
884 
885  #[DataProvider('restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
886  #[Test]
887  public function restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorWithHavingFluidErrorHandling(string $uri, int $frontendUserId): void
888  {
890  'website-local',
891  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
892  [],
893  $this->‪buildErrorHandlingConfiguration('Fluid', [403])
894  );
895 
896  $response = $this->executeFrontendSubRequest(
897  new InternalRequest($uri),
898  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
899  );
900 
901  self::assertSame(
902  403,
903  $response->getStatusCode()
904  );
905  self::assertStringContainsString(
906  'reasons: code',
907  (string)$response->getBody()
908  );
909  self::assertThat(
910  (string)$response->getBody(),
911  self::logicalOr(
912  self::stringContains('message: ID was not an accessible page'),
913  self::stringContains('message: Subsection was found and not accessible')
914  )
915  );
916  }
917 
918  #[DataProvider('restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
919  #[Test]
920  public function restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPageErrorHandling(string $uri, int $frontendUserId): void
921  {
923  'website-local',
924  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
925  [],
926  $this->‪buildErrorHandlingConfiguration('Page', [403])
927  );
928 
929  $response = $this->executeFrontendSubRequest(
930  new InternalRequest($uri),
931  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
932  );
933 
934  self::assertSame(
935  403,
936  $response->getStatusCode()
937  );
938  self::assertThat(
939  (string)$response->getBody(),
940  self::logicalOr(
941  self::stringContains('That page is forbidden to you'),
942  self::stringContains('ID was not an accessible page'),
943  self::stringContains('Subsection was found and not accessible')
944  )
945  );
946  }
947 
948  #[DataProvider('restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorDataProvider')]
949  #[Test]
950  public function restrictedPageWithParentSysFolderSendsForbiddenResponseWithUnauthorizedVisitorWithHavingPhpErrorHandling(string $uri, int $frontendUserId): void
951  {
953  'website-local',
954  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
955  [],
956  $this->‪buildErrorHandlingConfiguration('PHP', [403])
957  );
958 
959  $response = $this->executeFrontendSubRequest(
960  new InternalRequest($uri),
961  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
962  );
963  $json = json_decode((string)$response->getBody(), true);
964 
965  self::assertSame(
966  403,
967  $response->getStatusCode()
968  );
969  self::assertThat(
970  $json['message'] ?? null,
971  self::logicalOr(
972  self::identicalTo('ID was not an accessible page'),
973  self::identicalTo('Subsection was found and not accessible')
974  )
975  );
976  }
977 
978  public static function hiddenPageSends404ResponseRegardlessOfVisitorGroupDataProvider(): array
979  {
980  $instructions = [
981  // hidden page, always 404
982  ['https://website.local/never-visible-working-on-it', 0],
983  ['https://website.local/never-visible-working-on-it', 1],
984  // hidden fe group restricted and fegroup generally okay
985  ['https://website.local/sysfolder-restricted-hidden', 4],
986  ];
987  return self::keysFromTemplate($instructions, '%1$s (user:%2$s)');
988  }
989 
990  #[DataProvider('hiddenPageSends404ResponseRegardlessOfVisitorGroupDataProvider')]
991  #[Test]
992  public function hiddenPageSends404ResponseRegardlessOfVisitorGroup(string $uri, int $frontendUserId): void
993  {
995  'website-local',
996  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
997  [],
998  $this->‪buildErrorHandlingConfiguration('PHP', [404])
999  );
1000 
1001  $response = $this->executeFrontendSubRequest(
1002  new InternalRequest($uri),
1003  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
1004  );
1005  $json = json_decode((string)$response->getBody(), true);
1006 
1007  self::assertSame(
1008  404,
1009  $response->getStatusCode()
1010  );
1011  self::assertThat(
1012  $json['message'] ?? null,
1013  self::identicalTo('The requested page does not exist!')
1014  );
1015  }
1016 
1017  public static function pageRenderingStopsWithInvalidCacheHashDataProvider(): array
1018  {
1019  $domainPaths = [
1020  'https://website.local/',
1021  ];
1022  $queries = [
1023  '',
1024  'welcome',
1025  ];
1026  $customQueries = [
1027  '?testing[value]=1',
1028  '?testing[value]=1&cHash=',
1029  '?testing[value]=1&cHash=WRONG',
1030  ];
1031  return self::wrapInArray(
1032  self::keysFromValues(
1033  ‪PermutationUtility::meltStringItems([$domainPaths, $queries, $customQueries])
1034  )
1035  );
1036  }
1037 
1038  #[DataProvider('pageRenderingStopsWithInvalidCacheHashDataProvider')]
1039  #[Test]
1040  public function pageRequestNotFoundInvalidCacheHashWithoutHavingErrorHandling(string $uri): void
1041  {
1043  'website-local',
1044  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
1045  );
1046 
1047  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1048  self::assertSame(404, $response->getStatusCode());
1049  }
1050 
1051  #[DataProvider('pageRenderingStopsWithInvalidCacheHashDataProvider')]
1052  #[Test]
1053  public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingFluidErrorHandling(string $uri): void
1054  {
1056  'website-local',
1057  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1058  [],
1059  $this->‪buildErrorHandlingConfiguration('Fluid', [404])
1060  );
1061 
1062  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1063 
1064  self::assertSame(
1065  404,
1066  $response->getStatusCode()
1067  );
1068  self::assertThat(
1069  (string)$response->getBody(),
1070  self::logicalOr(
1071  self::stringContains('message: Request parameters could not be validated (&amp;cHash empty)'),
1072  self::stringContains('message: Request parameters could not be validated (&amp;cHash comparison failed)')
1073  )
1074  );
1075  }
1076 
1077  #[DataProvider('pageRenderingStopsWithInvalidCacheHashDataProvider')]
1078  #[Test]
1079  public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPageErrorHandling(string $uri): void
1080  {
1082  'website-local',
1083  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1084  [],
1085  $this->‪buildErrorHandlingConfiguration('Page', [404, 500])
1086  );
1087 
1088  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1089 
1090  self::assertSame(
1091  404,
1092  $response->getStatusCode()
1093  );
1094  self::assertThat(
1095  (string)$response->getBody(),
1096  self::stringContains('That page was not found')
1097  );
1098  }
1099 
1100  #[DataProvider('pageRenderingStopsWithInvalidCacheHashDataProvider')]
1101  #[Test]
1102  public function pageRequestSendsNotFoundResponseWithInvalidCacheHashWithHavingPhpErrorHandling(string $uri): void
1103  {
1105  'website-local',
1106  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1107  [],
1108  $this->‪buildErrorHandlingConfiguration('PHP', [404])
1109  );
1110 
1111  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1112  $json = json_decode((string)$response->getBody(), true);
1113 
1114  self::assertSame(
1115  404,
1116  $response->getStatusCode()
1117  );
1118  self::assertThat(
1119  $json['message'] ?? null,
1120  self::logicalOr(
1121  self::identicalTo('Request parameters could not be validated (&cHash empty)'),
1122  self::identicalTo('Request parameters could not be validated (&cHash comparison failed)')
1123  )
1124  );
1125  }
1126 
1127  public static function pageIsRenderedWithValidCacheHashDataProvider(): array
1128  {
1129  $domainPaths = [
1130  'https://website.local/',
1131  ];
1132  // cHash has been calculated with encryption key set to
1133  // '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'
1134  $queries = [
1135  // @todo Currently fails since cHash is verified after(!) redirect to page 1100
1136  // '?cHash=7d1f13fa91159dac7feb3c824936b39d',
1137  // '?cHash=7d1f13fa91159dac7feb3c824936b39d',
1138  'welcome?cHash=f42b850e435f0cedd366f5db749fc1af',
1139  ];
1140  $customQueries = [
1141  '&testing[value]=1',
1142  ];
1143  return self::wrapInArray(
1144  self::keysFromValues(
1145  ‪PermutationUtility::meltStringItems([$domainPaths, $queries, $customQueries])
1146  )
1147  );
1148  }
1149 
1150  #[DataProvider('pageIsRenderedWithValidCacheHashDataProvider')]
1151  #[Test]
1152  public function pageIsRenderedWithValidCacheHash($uri): void
1153  {
1155  'website-local',
1156  $this->‪buildSiteConfiguration(1000, 'https://website.local/')
1157  );
1158 
1159  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1160  $responseStructure = ResponseContent::fromString(
1161  (string)$response->getBody()
1162  );
1163  self::assertSame(
1164  '1',
1165  $responseStructure->getScopePath('getpost/testing.value')
1166  );
1167  }
1168 
1169  public static function crossSiteShortcutsAreRedirectedDataProvider(): \Generator
1170  {
1171  yield 'shortcut is redirected' => [
1172  'https://website.local/cross-site-shortcut',
1173  307,
1174  [
1175  'X-Redirect-By' => ['TYPO3 Shortcut/Mountpoint'],
1176  'location' => ['https://blog.local/authors'],
1177  ],
1178  ];
1179  yield 'shortcut of translated page is redirected to a different page than the original page' => [
1180  'https://website.local/fr/other-cross-site-shortcut',
1181  307,
1182  [
1183  'X-Redirect-By' => ['TYPO3 Shortcut/Mountpoint'],
1184  'location' => ['https://website.local/fr/acme-dans-votre-region'],
1185  ],
1186  ];
1187  }
1188 
1189  #[DataProvider('crossSiteShortcutsAreRedirectedDataProvider')]
1190  #[Test]
1191  public function crossSiteShortcutsAreRedirected(string $uri, int $expectedStatusCode, array $expectedHeaders): void
1192  {
1194  'website-local',
1195  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1196  [
1197  $this->‪buildDefaultLanguageConfiguration('EN', '/'),
1198  $this->‪buildLanguageConfiguration('FR', '/fr/', ['EN']),
1199  ]
1200  );
1202  'blog-local',
1203  $this->‪buildSiteConfiguration(2000, 'https://blog.local/'),
1204  [
1205  $this->‪buildDefaultLanguageConfiguration('EN', '/'),
1206  $this->‪buildLanguageConfiguration('FR', '/fr/', ['EN']),
1207  ]
1208  );
1209  $this->setUpFrontendRootPage(
1210  2000,
1211  [
1212  'EXT:core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
1213  'EXT:frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript',
1214  ],
1215  [
1216  'title' => 'ACME Blog',
1217  ]
1218  );
1219 
1220  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1221  self::assertSame($expectedStatusCode, $response->getStatusCode());
1222  self::assertSame($expectedHeaders, $response->getHeaders());
1223  }
1224 
1225  public static function pageIsRenderedForVersionedPageDataProvider(): \Generator
1226  {
1227  yield 'Live page with logged-in user' => [
1228  'url' => 'https://website.local/en-en/welcome',
1229  'expectedPageTitle' => 'EN: Welcome',
1230  'expectedPageId' => '1100',
1231  'workspaceId' => 0,
1232  'backendUserId' => 1,
1233  'expectedStatusCode' => 200,
1234  ];
1235  yield 'Live page with logged-in user accessed even though versioned page slug was changed' => [
1236  'url' => 'https://website.local/en-en/welcome',
1237  'expectedPageTitle' => 'EN: Welcome to ACME Inc',
1238  'expectedPageId' => '1100',
1239  'workspaceId' => 1,
1240  'backendUserId' => 1,
1241  'expectedStatusCode' => 200,
1242  ];
1243  yield 'Versioned page with logged-in user and modified slug' => [
1244  'url' => 'https://website.local/en-en/welcome-modified',
1245  'expectedPageTitle' => 'EN: Welcome to ACME Inc',
1246  'expectedPageId' => '1100',
1247  'workspaceId' => 1,
1248  'backendUserId' => 1,
1249  'expectedStatusCode' => 200,
1250  ];
1251  yield 'Versioned page without logged-in user renders 404' => [
1252  'url' => 'https://website.local/en-en/welcome-modified',
1253  'expectedPageTitle' => null,
1254  'expectedPageId' => null,
1255  'workspaceId' => 1,
1256  'backendUserId' => 0,
1257  'expectedStatusCode' => 404,
1258  ];
1259  }
1260 
1261  #[DataProvider('pageIsRenderedForVersionedPageDataProvider')]
1262  #[Test]
1263  public function pageIsRenderedForVersionedPage(string ‪$url, ?string $expectedPageTitle, ?string $expectedPageId, int $workspaceId, int $backendUserId, int $expectedStatusCode): void
1264  {
1266  'website-local',
1267  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1268  [
1269  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en/'),
1270  $this->‪buildLanguageConfiguration('FR', '/fr-fr/', ['EN']),
1271  $this->‪buildLanguageConfiguration('FR-CA', '/fr-ca/', ['FR', 'EN']),
1272  ]
1273  );
1274  $response = $this->executeFrontendSubRequest(
1275  (new InternalRequest(‪$url)),
1276  (new InternalRequestContext())
1277  ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0)
1278  ->withBackendUserId($backendUserId)
1279  );
1280  $responseStructure = ResponseContent::fromString(
1281  (string)$response->getBody()
1282  );
1283 
1284  self::assertSame($expectedStatusCode, $response->getStatusCode());
1285  self::assertSame($expectedPageId, $responseStructure->getScopePath('page/uid'));
1286  self::assertSame($expectedPageTitle, $responseStructure->getScopePath('page/title'));
1287  }
1288 
1289  public static function defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExistsDataProvider(): \Generator
1290  {
1291  // ----------------------------------------------------------------
1292  // #1 page slug without trailing slash, request with trailing slash
1293  // ----------------------------------------------------------------
1294 
1295  yield '#1 Default slug with default base resolves' => [
1296  'uri' => 'https://website.local/welcome/',
1297  'recordUpdates' => [],
1298  'fallbackIdentifiers' => [
1299  'EN',
1300  ],
1301  'fallbackType' => 'strict',
1302  'expectedStatusCode' => 200,
1303  'expectedPageTitle' => 'EN: Welcome',
1304  ];
1305 
1306  yield '#1 FR slug with FR base resolves' => [
1307  'uri' => 'https://website.local/fr-fr/bienvenue/',
1308  'recordUpdates' => [],
1309  'fallbackIdentifiers' => [
1310  'EN',
1311  ],
1312  'fallbackType' => 'strict',
1313  'expectedStatusCode' => 200,
1314  'expectedPageTitle' => 'FR: Welcome',
1315  ];
1316 
1317  // Using default language slug with language base should be page not found if language page is active.
1318  yield '#1 Default slug with default base do not resolve' => [
1319  'uri' => 'https://website.local/fr-fr/welcome/',
1320  'recordUpdates' => [],
1321  'fallbackIdentifiers' => [
1322  'EN',
1323  ],
1324  'fallbackType' => 'strict',
1325  'expectedStatusCode' => 404,
1326  'expectedPageTitle' => null,
1327  ];
1328 
1329  // Using default language slug with language base resolves for inactive / hidden language page
1330  yield '#1 Default slug with default base but inactive language page resolves' => [
1331  'uri' => 'https://website.local/fr-fr/welcome/',
1332  'recordUpdates' => [
1333  'pages' => [
1334  [
1335  'data' => [
1336  'hidden' => 1,
1337  ],
1338  'identifiers' => [
1339  'uid' => 1101,
1340  ],
1341  'types' => [],
1342  ],
1343  ],
1344  ],
1345  'fallbackIdentifiers' => [
1346  'EN',
1347  ],
1348  'fallbackType' => 'strict',
1349  'expectedStatusCode' => 200,
1350  'expectedPageTitle' => 'EN: Welcome',
1351  ];
1352 
1353  // -------------------------------------------------------------
1354  // #2 page slug with trailing slash, request with trailing slash
1355  // -------------------------------------------------------------
1356 
1357  yield '#2 Default slug with default base resolves' => [
1358  'uri' => 'https://website.local/welcome/',
1359  'recordUpdates' => [
1360  'pages' => [
1361  [
1362  'data' => [
1363  'slug' => '/welcome/',
1364  ],
1365  'identifiers' => [
1366  'uid' => 1100,
1367  ],
1368  'types' => [],
1369  ],
1370  [
1371  'data' => [
1372  'slug' => '/bienvenue/',
1373  ],
1374  'identifiers' => [
1375  'uid' => 1101,
1376  ],
1377  'types' => [],
1378  ],
1379  [
1380  'data' => [
1381  'slug' => '/bienvenue/',
1382  ],
1383  'identifiers' => [
1384  'uid' => 1102,
1385  ],
1386  'types' => [],
1387  ],
1388  [
1389  'data' => [
1390  'slug' => '/简-bienvenue/',
1391  ],
1392  'identifiers' => [
1393  'uid' => 1103,
1394  ],
1395  'types' => [],
1396  ],
1397  ],
1398  ],
1399  'fallbackIdentifiers' => [
1400  'EN',
1401  ],
1402  'fallbackType' => 'strict',
1403  'expectedStatusCode' => 200,
1404  'expectedPageTitle' => 'EN: Welcome',
1405  ];
1406 
1407  yield '#2 FR slug with FR base resolves' => [
1408  'uri' => 'https://website.local/fr-fr/bienvenue/',
1409  'recordUpdates' => [
1410  'pages' => [
1411  [
1412  'data' => [
1413  'slug' => '/welcome/',
1414  ],
1415  'identifiers' => [
1416  'uid' => 1100,
1417  ],
1418  'types' => [],
1419  ],
1420  [
1421  'data' => [
1422  'slug' => '/bienvenue/',
1423  ],
1424  'identifiers' => [
1425  'uid' => 1101,
1426  ],
1427  'types' => [],
1428  ],
1429  [
1430  'data' => [
1431  'slug' => '/bienvenue/',
1432  ],
1433  'identifiers' => [
1434  'uid' => 1102,
1435  ],
1436  'types' => [],
1437  ],
1438  [
1439  'data' => [
1440  'slug' => '/简-bienvenue/',
1441  ],
1442  'identifiers' => [
1443  'uid' => 1103,
1444  ],
1445  'types' => [],
1446  ],
1447  ],
1448  ],
1449  'fallbackIdentifiers' => [
1450  'EN',
1451  ],
1452  'fallbackType' => 'strict',
1453  'expectedStatusCode' => 200,
1454  'expectedPageTitle' => 'FR: Welcome',
1455  ];
1456 
1457  // Using default language slug with language base should be page not found if language page is active.
1458  yield '#2 Default slug with default base do not resolve' => [
1459  'uri' => 'https://website.local/fr-fr/welcome/',
1460  'recordUpdates' => [
1461  'pages' => [
1462  [
1463  'data' => [
1464  'slug' => '/welcome/',
1465  ],
1466  'identifiers' => [
1467  'uid' => 1100,
1468  ],
1469  'types' => [],
1470  ],
1471  [
1472  'data' => [
1473  'slug' => '/bienvenue/',
1474  ],
1475  'identifiers' => [
1476  'uid' => 1101,
1477  ],
1478  'types' => [],
1479  ],
1480  [
1481  'data' => [
1482  'slug' => '/bienvenue/',
1483  ],
1484  'identifiers' => [
1485  'uid' => 1102,
1486  ],
1487  'types' => [],
1488  ],
1489  [
1490  'data' => [
1491  'slug' => '/简-bienvenue/',
1492  ],
1493  'identifiers' => [
1494  'uid' => 1103,
1495  ],
1496  'types' => [],
1497  ],
1498  ],
1499  ],
1500  'fallbackIdentifiers' => [
1501  'EN',
1502  ],
1503  'fallbackType' => 'strict',
1504  'expectedStatusCode' => 404,
1505  'expectedPageTitle' => null,
1506  ];
1507 
1508  // Using default language slug with language base resolves for inactive / hidden language page
1509  yield '#2 Default slug with default base but inactive language page resolves' => [
1510  'uri' => 'https://website.local/fr-fr/welcome/',
1511  'recordUpdates' => [
1512  'pages' => [
1513  [
1514  'data' => [
1515  'slug' => '/welcome/',
1516  ],
1517  'identifiers' => [
1518  'uid' => 1100,
1519  ],
1520  'types' => [],
1521  ],
1522  [
1523  'data' => [
1524  'slug' => '/bienvenue/',
1525  'hidden' => 1,
1526  ],
1527  'identifiers' => [
1528  'uid' => 1101,
1529  ],
1530  'types' => [],
1531  ],
1532  [
1533  'data' => [
1534  'slug' => '/bienvenue/',
1535  ],
1536  'identifiers' => [
1537  'uid' => 1102,
1538  ],
1539  'types' => [],
1540  ],
1541  [
1542  'data' => [
1543  'slug' => '/简-bienvenue/',
1544  ],
1545  'identifiers' => [
1546  'uid' => 1103,
1547  ],
1548  'types' => [],
1549  ],
1550  ],
1551  ],
1552  'fallbackIdentifiers' => [
1553  'EN',
1554  ],
1555  'fallbackType' => 'strict',
1556  'expectedStatusCode' => 200,
1557  'expectedPageTitle' => 'EN: Welcome',
1558  ];
1559 
1560  // ----------------------------------------------------------------
1561  // #3 page slug with trailing slash, request without trailing slash
1562  // ----------------------------------------------------------------
1563 
1564  yield '#3 Default slug with default base resolves' => [
1565  'uri' => 'https://website.local/welcome',
1566  'recordUpdates' => [
1567  'pages' => [
1568  [
1569  'data' => [
1570  'slug' => '/welcome/',
1571  ],
1572  'identifiers' => [
1573  'uid' => 1100,
1574  ],
1575  'types' => [],
1576  ],
1577  [
1578  'data' => [
1579  'slug' => '/bienvenue/',
1580  ],
1581  'identifiers' => [
1582  'uid' => 1101,
1583  ],
1584  'types' => [],
1585  ],
1586  [
1587  'data' => [
1588  'slug' => '/bienvenue/',
1589  ],
1590  'identifiers' => [
1591  'uid' => 1102,
1592  ],
1593  'types' => [],
1594  ],
1595  [
1596  'data' => [
1597  'slug' => '/简-bienvenue/',
1598  ],
1599  'identifiers' => [
1600  'uid' => 1103,
1601  ],
1602  'types' => [],
1603  ],
1604  ],
1605  ],
1606  'fallbackIdentifiers' => [
1607  'EN',
1608  ],
1609  'fallbackType' => 'strict',
1610  'expectedStatusCode' => 200,
1611  'expectedPageTitle' => 'EN: Welcome',
1612  ];
1613 
1614  yield '#3 FR slug with FR base resolves' => [
1615  'uri' => 'https://website.local/fr-fr/bienvenue',
1616  'recordUpdates' => [
1617  'pages' => [
1618  [
1619  'data' => [
1620  'slug' => '/welcome/',
1621  ],
1622  'identifiers' => [
1623  'uid' => 1100,
1624  ],
1625  'types' => [],
1626  ],
1627  [
1628  'data' => [
1629  'slug' => '/bienvenue/',
1630  ],
1631  'identifiers' => [
1632  'uid' => 1101,
1633  ],
1634  'types' => [],
1635  ],
1636  [
1637  'data' => [
1638  'slug' => '/bienvenue/',
1639  ],
1640  'identifiers' => [
1641  'uid' => 1102,
1642  ],
1643  'types' => [],
1644  ],
1645  [
1646  'data' => [
1647  'slug' => '/简-bienvenue/',
1648  ],
1649  'identifiers' => [
1650  'uid' => 1103,
1651  ],
1652  'types' => [],
1653  ],
1654  ],
1655  ],
1656  'fallbackIdentifiers' => [
1657  'EN',
1658  ],
1659  'fallbackType' => 'strict',
1660  'expectedStatusCode' => 200,
1661  'expectedPageTitle' => 'FR: Welcome',
1662  ];
1663 
1664  // Using default language slug with language base should be page not found if language page is active.
1665  yield '#3 Default slug with default base do not resolve' => [
1666  'uri' => 'https://website.local/fr-fr/welcome',
1667  'recordUpdates' => [
1668  'pages' => [
1669  [
1670  'data' => [
1671  'slug' => '/welcome/',
1672  ],
1673  'identifiers' => [
1674  'uid' => 1100,
1675  ],
1676  'types' => [],
1677  ],
1678  [
1679  'data' => [
1680  'slug' => '/bienvenue/',
1681  ],
1682  'identifiers' => [
1683  'uid' => 1101,
1684  ],
1685  'types' => [],
1686  ],
1687  [
1688  'data' => [
1689  'slug' => '/bienvenue/',
1690  ],
1691  'identifiers' => [
1692  'uid' => 1102,
1693  ],
1694  'types' => [],
1695  ],
1696  [
1697  'data' => [
1698  'slug' => '/简-bienvenue/',
1699  ],
1700  'identifiers' => [
1701  'uid' => 1103,
1702  ],
1703  'types' => [],
1704  ],
1705  ],
1706  ],
1707  'fallbackIdentifiers' => [
1708  'EN',
1709  ],
1710  'fallbackType' => 'strict',
1711  'expectedStatusCode' => 404,
1712  'expectedPageTitle' => null,
1713  ];
1714 
1715  // Using default language slug with language base resolves for inactive / hidden language page
1716  yield '#3 Default slug with default base but inactive language page resolves' => [
1717  'uri' => 'https://website.local/fr-fr/welcome',
1718  'recordUpdates' => [
1719  'pages' => [
1720  [
1721  'data' => [
1722  'slug' => '/welcome/',
1723  ],
1724  'identifiers' => [
1725  'uid' => 1100,
1726  ],
1727  'types' => [],
1728  ],
1729  [
1730  'data' => [
1731  'slug' => '/bienvenue/',
1732  'hidden' => 1,
1733  ],
1734  'identifiers' => [
1735  'uid' => 1101,
1736  ],
1737  'types' => [],
1738  ],
1739  [
1740  'data' => [
1741  'slug' => '/bienvenue/',
1742  ],
1743  'identifiers' => [
1744  'uid' => 1102,
1745  ],
1746  'types' => [],
1747  ],
1748  [
1749  'data' => [
1750  'slug' => '/简-bienvenue/',
1751  ],
1752  'identifiers' => [
1753  'uid' => 1103,
1754  ],
1755  'types' => [],
1756  ],
1757  ],
1758  ],
1759  'fallbackIdentifiers' => [
1760  'EN',
1761  ],
1762  'fallbackType' => 'strict',
1763  'expectedStatusCode' => 200,
1764  'expectedPageTitle' => 'EN: Welcome',
1765  ];
1766 
1767  // -------------------------------------------------------------------
1768  // #4 page slug without trailing slash, request without trailing slash
1769  // -------------------------------------------------------------------
1770 
1771  yield '#4 Default slug with default base resolves' => [
1772  'uri' => 'https://website.local/welcome',
1773  'recordUpdates' => [],
1774  'fallbackIdentifiers' => [
1775  'EN',
1776  ],
1777  'fallbackType' => 'strict',
1778  'expectedStatusCode' => 200,
1779  'expectedPageTitle' => 'EN: Welcome',
1780  ];
1781 
1782  yield '#4 FR slug with FR base resolves' => [
1783  'uri' => 'https://website.local/fr-fr/bienvenue',
1784  'recordUpdates' => [],
1785  'fallbackIdentifiers' => [
1786  'EN',
1787  ],
1788  'fallbackType' => 'strict',
1789  'expectedStatusCode' => 200,
1790  'expectedPageTitle' => 'FR: Welcome',
1791  ];
1792 
1793  // Using default language slug with language base should be page not found if language page is active.
1794  yield '#4 Default slug with default base do not resolve' => [
1795  'uri' => 'https://website.local/fr-fr/welcome',
1796  'recordUpdates' => [],
1797  'fallbackIdentifiers' => [
1798  'EN',
1799  ],
1800  'fallbackType' => 'strict',
1801  'expectedStatusCode' => 404,
1802  'expectedPageTitle' => null,
1803  ];
1804 
1805  // Using default language slug with language base resolves for inactive / hidden language page
1806  yield '#4 Default slug with default base but inactive language page resolves' => [
1807  'uri' => 'https://website.local/fr-fr/welcome',
1808  'recordUpdates' => [
1809  'pages' => [
1810  [
1811  'data' => [
1812  'hidden' => 1,
1813  ],
1814  'identifiers' => [
1815  'uid' => 1101,
1816  ],
1817  'types' => [],
1818  ],
1819  ],
1820  ],
1821  'fallbackIdentifiers' => [
1822  'EN',
1823  ],
1824  'fallbackType' => 'strict',
1825  'expectedStatusCode' => 200,
1826  'expectedPageTitle' => 'EN: Welcome',
1827  ];
1828  }
1829 
1833  #[DataProvider('defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExistsDataProvider')]
1834  #[Test]
1835  public function defaultLanguagePageNotResolvedForSiteLanguageBaseIfLanguagePageExists(string $uri, array $recordUpdates, array $fallbackIdentifiers, string $fallbackType, int $expectedStatusCode, ?string $expectedPageTitle): void
1836  {
1838  'website-local',
1839  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
1840  [
1841  $this->‪buildDefaultLanguageConfiguration('EN', '/'),
1842  $this->‪buildLanguageConfiguration('FR', 'https://website.local/fr-fr/', ['EN']),
1843  ]
1844  );
1845  if ($recordUpdates !== []) {
1846  foreach ($recordUpdates as $table => $records) {
1847  foreach ($records as ‪$record) {
1848  $this->getConnectionPool()->getConnectionForTable($table)
1849  ->update(
1850  $table,
1851  ‪$record['data'] ?? [],
1852  ‪$record['identifiers'] ?? [],
1853  ‪$record['types'] ?? []
1854  );
1855  }
1856  }
1857  }
1858 
1859  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
1860  $responseStructure = ResponseContent::fromString(
1861  (string)$response->getBody()
1862  );
1863 
1864  self::assertSame(
1865  $expectedStatusCode,
1866  $response->getStatusCode()
1867  );
1868  if ($expectedPageTitle !== null) {
1869  self::assertSame(
1870  $expectedPageTitle,
1871  $responseStructure->getScopePath('page/title')
1872  );
1873  }
1874  }
1875 
1876  public static function defaultLanguagePageNotResolvedForSiteLanguageBaseWithNonDefaultLanguageShorterUriIfLanguagePageExistsDataProvider(): \Generator
1877  {
1878  // ----------------------------------------------------------------
1879  // #1 page slug without trailing slash, request with trailing slash
1880  // ----------------------------------------------------------------
1881 
1882  yield '#1 Default slug with default base resolves' => [
1883  'uri' => 'https://website.local/en-en/welcome/',
1884  'recordUpdates' => [],
1885  'fallbackIdentifiers' => [
1886  'EN',
1887  ],
1888  'fallbackType' => 'strict',
1889  'expectedStatusCode' => 200,
1890  'expectedPageTitle' => 'EN: Welcome',
1891  ];
1892 
1893  yield '#1 FR slug with FR base resolves' => [
1894  'uri' => 'https://website.local/bienvenue/',
1895  'recordUpdates' => [],
1896  'fallbackIdentifiers' => [
1897  'EN',
1898  ],
1899  'fallbackType' => 'strict',
1900  'expectedStatusCode' => 200,
1901  'expectedPageTitle' => 'FR: Welcome',
1902  ];
1903 
1904  // Using default language slug with language base should be page not found if language page is active.
1905  yield '#1 Default slug with default base do not resolve' => [
1906  'uri' => 'https://website.local/welcome/',
1907  'recordUpdates' => [],
1908  'fallbackIdentifiers' => [
1909  'EN',
1910  ],
1911  'fallbackType' => 'strict',
1912  'expectedStatusCode' => 404,
1913  'expectedPageTitle' => null,
1914  ];
1915 
1916  // Using default language slug with language base should be page not found if language page is active.
1917  yield '#1 Default slug with default base do not resolve strict without fallback' => [
1918  'uri' => 'https://website.local/welcome/',
1919  'recordUpdates' => [],
1920  'fallbackIdentifiers' => [],
1921  'fallbackType' => 'fallback',
1922  'expectedStatusCode' => 404,
1923  'expectedPageTitle' => null,
1924  ];
1925 
1926  // Using default language slug with language base should be page not found if language page is active.
1927  yield '#1 Default slug with default base do not resolve fallback' => [
1928  'uri' => 'https://website.local/welcome/',
1929  'recordUpdates' => [],
1930  'fallbackIdentifiers' => [
1931  'EN',
1932  ],
1933  'fallbackType' => 'fallback',
1934  'expectedStatusCode' => 404,
1935  'expectedPageTitle' => null,
1936  ];
1937 
1938  // Using default language slug with language base resolves for inactive / hidden language page
1939  yield '#1 Default slug with default base but inactive language page resolves' => [
1940  'uri' => 'https://website.local/welcome/',
1941  'recordUpdates' => [
1942  'pages' => [
1943  [
1944  'data' => [
1945  'hidden' => 1,
1946  ],
1947  'identifiers' => [
1948  'uid' => 1101,
1949  ],
1950  'types' => [],
1951  ],
1952  ],
1953  ],
1954  'fallbackIdentifiers' => [
1955  'EN',
1956  ],
1957  'fallbackType' => 'strict',
1958  'expectedStatusCode' => 200,
1959  'expectedPageTitle' => 'EN: Welcome',
1960  ];
1961 
1962  // -------------------------------------------------------------
1963  // #2 page slug with trailing slash, request with trailing slash
1964  // -------------------------------------------------------------
1965 
1966  yield '#2 Default slug with default base resolves' => [
1967  'uri' => 'https://website.local/en-en/welcome/',
1968  'recordUpdates' => [
1969  'pages' => [
1970  [
1971  'data' => [
1972  'slug' => '/welcome/',
1973  ],
1974  'identifiers' => [
1975  'uid' => 1100,
1976  ],
1977  'types' => [],
1978  ],
1979  [
1980  'data' => [
1981  'slug' => '/bienvenue/',
1982  ],
1983  'identifiers' => [
1984  'uid' => 1101,
1985  ],
1986  'types' => [],
1987  ],
1988  [
1989  'data' => [
1990  'slug' => '/bienvenue/',
1991  ],
1992  'identifiers' => [
1993  'uid' => 1102,
1994  ],
1995  'types' => [],
1996  ],
1997  [
1998  'data' => [
1999  'slug' => '/简-bienvenue/',
2000  ],
2001  'identifiers' => [
2002  'uid' => 1103,
2003  ],
2004  'types' => [],
2005  ],
2006  ],
2007  ],
2008  'fallbackIdentifiers' => [
2009  'EN',
2010  ],
2011  'fallbackType' => 'strict',
2012  'expectedStatusCode' => 200,
2013  'expectedPageTitle' => 'EN: Welcome',
2014  ];
2015 
2016  yield '#2 FR slug with FR base resolves' => [
2017  'uri' => 'https://website.local/bienvenue/',
2018  'recordUpdates' => [
2019  'pages' => [
2020  [
2021  'data' => [
2022  'slug' => '/welcome/',
2023  ],
2024  'identifiers' => [
2025  'uid' => 1100,
2026  ],
2027  'types' => [],
2028  ],
2029  [
2030  'data' => [
2031  'slug' => '/bienvenue/',
2032  ],
2033  'identifiers' => [
2034  'uid' => 1101,
2035  ],
2036  'types' => [],
2037  ],
2038  [
2039  'data' => [
2040  'slug' => '/bienvenue/',
2041  ],
2042  'identifiers' => [
2043  'uid' => 1102,
2044  ],
2045  'types' => [],
2046  ],
2047  [
2048  'data' => [
2049  'slug' => '/简-bienvenue/',
2050  ],
2051  'identifiers' => [
2052  'uid' => 1103,
2053  ],
2054  'types' => [],
2055  ],
2056  ],
2057  ],
2058  'fallbackIdentifiers' => [
2059  'EN',
2060  ],
2061  'fallbackType' => 'strict',
2062  'expectedStatusCode' => 200,
2063  'expectedPageTitle' => 'FR: Welcome',
2064  ];
2065 
2066  // Using default language slug with language base should be page not found if language page is active.
2067  yield '#2 Default slug with default base do not resolve' => [
2068  'uri' => 'https://website.local/welcome/',
2069  'recordUpdates' => [
2070  'pages' => [
2071  [
2072  'data' => [
2073  'slug' => '/welcome/',
2074  ],
2075  'identifiers' => [
2076  'uid' => 1100,
2077  ],
2078  'types' => [],
2079  ],
2080  [
2081  'data' => [
2082  'slug' => '/bienvenue/',
2083  ],
2084  'identifiers' => [
2085  'uid' => 1101,
2086  ],
2087  'types' => [],
2088  ],
2089  [
2090  'data' => [
2091  'slug' => '/bienvenue/',
2092  ],
2093  'identifiers' => [
2094  'uid' => 1102,
2095  ],
2096  'types' => [],
2097  ],
2098  [
2099  'data' => [
2100  'slug' => '/简-bienvenue/',
2101  ],
2102  'identifiers' => [
2103  'uid' => 1103,
2104  ],
2105  'types' => [],
2106  ],
2107  ],
2108  ],
2109  'fallbackIdentifiers' => [
2110  'EN',
2111  ],
2112  'fallbackType' => 'strict',
2113  'expectedStatusCode' => 404,
2114  'expectedPageTitle' => null,
2115  ];
2116 
2117  // Using default language slug with language base should be page not found if language page is active.
2118  yield '#2 Default slug with default base do not resolve strict without fallback' => [
2119  'uri' => 'https://website.local/welcome/',
2120  'recordUpdates' => [
2121  'pages' => [
2122  [
2123  'data' => [
2124  'slug' => '/welcome/',
2125  ],
2126  'identifiers' => [
2127  'uid' => 1100,
2128  ],
2129  'types' => [],
2130  ],
2131  [
2132  'data' => [
2133  'slug' => '/bienvenue/',
2134  ],
2135  'identifiers' => [
2136  'uid' => 1101,
2137  ],
2138  'types' => [],
2139  ],
2140  [
2141  'data' => [
2142  'slug' => '/bienvenue/',
2143  ],
2144  'identifiers' => [
2145  'uid' => 1102,
2146  ],
2147  'types' => [],
2148  ],
2149  [
2150  'data' => [
2151  'slug' => '/简-bienvenue/',
2152  ],
2153  'identifiers' => [
2154  'uid' => 1103,
2155  ],
2156  'types' => [],
2157  ],
2158  ],
2159  ],
2160  'fallbackIdentifiers' => [],
2161  'fallbackType' => 'fallback',
2162  'expectedStatusCode' => 404,
2163  'expectedPageTitle' => null,
2164  ];
2165 
2166  // Using default language slug with language base should be page not found if language page is active.
2167  yield '#2 Default slug with default base do not resolve fallback' => [
2168  'uri' => 'https://website.local/welcome/',
2169  'recordUpdates' => [
2170  'pages' => [
2171  [
2172  'data' => [
2173  'slug' => '/welcome/',
2174  ],
2175  'identifiers' => [
2176  'uid' => 1100,
2177  ],
2178  'types' => [],
2179  ],
2180  [
2181  'data' => [
2182  'slug' => '/bienvenue/',
2183  ],
2184  'identifiers' => [
2185  'uid' => 1101,
2186  ],
2187  'types' => [],
2188  ],
2189  [
2190  'data' => [
2191  'slug' => '/bienvenue/',
2192  ],
2193  'identifiers' => [
2194  'uid' => 1102,
2195  ],
2196  'types' => [],
2197  ],
2198  [
2199  'data' => [
2200  'slug' => '/简-bienvenue/',
2201  ],
2202  'identifiers' => [
2203  'uid' => 1103,
2204  ],
2205  'types' => [],
2206  ],
2207  ],
2208  ],
2209  'fallbackIdentifiers' => [
2210  'EN',
2211  ],
2212  'fallbackType' => 'fallback',
2213  'expectedStatusCode' => 404,
2214  'expectedPageTitle' => null,
2215  ];
2216 
2217  // Using default language slug with language base resolves for inactive / hidden language page
2218  yield '#2 Default slug with default base but inactive language page resolves' => [
2219  'uri' => 'https://website.local/welcome/',
2220  'recordUpdates' => [
2221  'pages' => [
2222  [
2223  'data' => [
2224  'slug' => '/welcome/',
2225  ],
2226  'identifiers' => [
2227  'uid' => 1100,
2228  ],
2229  'types' => [],
2230  ],
2231  [
2232  'data' => [
2233  'slug' => '/bienvenue/',
2234  'hidden' => 1,
2235  ],
2236  'identifiers' => [
2237  'uid' => 1101,
2238  ],
2239  'types' => [],
2240  ],
2241  [
2242  'data' => [
2243  'slug' => '/bienvenue/',
2244  ],
2245  'identifiers' => [
2246  'uid' => 1102,
2247  ],
2248  'types' => [],
2249  ],
2250  [
2251  'data' => [
2252  'slug' => '/简-bienvenue/',
2253  ],
2254  'identifiers' => [
2255  'uid' => 1103,
2256  ],
2257  'types' => [],
2258  ],
2259  ],
2260  ],
2261  'fallbackIdentifiers' => [
2262  'EN',
2263  ],
2264  'fallbackType' => 'strict',
2265  'expectedStatusCode' => 200,
2266  'expectedPageTitle' => 'EN: Welcome',
2267  ];
2268 
2269  // ----------------------------------------------------------------
2270  // #3 page slug with trailing slash, request without trailing slash
2271  // ----------------------------------------------------------------
2272 
2273  yield '#3 Default slug with default base resolves' => [
2274  'uri' => 'https://website.local/en-en/welcome',
2275  'recordUpdates' => [
2276  'pages' => [
2277  [
2278  'data' => [
2279  'slug' => '/welcome/',
2280  ],
2281  'identifiers' => [
2282  'uid' => 1100,
2283  ],
2284  'types' => [],
2285  ],
2286  [
2287  'data' => [
2288  'slug' => '/bienvenue/',
2289  ],
2290  'identifiers' => [
2291  'uid' => 1101,
2292  ],
2293  'types' => [],
2294  ],
2295  [
2296  'data' => [
2297  'slug' => '/bienvenue/',
2298  ],
2299  'identifiers' => [
2300  'uid' => 1102,
2301  ],
2302  'types' => [],
2303  ],
2304  [
2305  'data' => [
2306  'slug' => '/简-bienvenue/',
2307  ],
2308  'identifiers' => [
2309  'uid' => 1103,
2310  ],
2311  'types' => [],
2312  ],
2313  ],
2314  ],
2315  'fallbackIdentifiers' => [
2316  'EN',
2317  ],
2318  'fallbackType' => 'strict',
2319  'expectedStatusCode' => 200,
2320  'expectedPageTitle' => 'EN: Welcome',
2321  ];
2322 
2323  yield '#3 FR slug with FR base resolves' => [
2324  'uri' => 'https://website.local/bienvenue',
2325  'recordUpdates' => [
2326  'pages' => [
2327  [
2328  'data' => [
2329  'slug' => '/welcome/',
2330  ],
2331  'identifiers' => [
2332  'uid' => 1100,
2333  ],
2334  'types' => [],
2335  ],
2336  [
2337  'data' => [
2338  'slug' => '/bienvenue/',
2339  ],
2340  'identifiers' => [
2341  'uid' => 1101,
2342  ],
2343  'types' => [],
2344  ],
2345  [
2346  'data' => [
2347  'slug' => '/bienvenue/',
2348  ],
2349  'identifiers' => [
2350  'uid' => 1102,
2351  ],
2352  'types' => [],
2353  ],
2354  [
2355  'data' => [
2356  'slug' => '/简-bienvenue/',
2357  ],
2358  'identifiers' => [
2359  'uid' => 1103,
2360  ],
2361  'types' => [],
2362  ],
2363  ],
2364  ],
2365  'fallbackIdentifiers' => [
2366  'EN',
2367  ],
2368  'fallbackType' => 'strict',
2369  'expectedStatusCode' => 200,
2370  'expectedPageTitle' => 'FR: Welcome',
2371  ];
2372 
2373  // Using default language slug with language base should be page not found if language page is active.
2374  yield '#3 Default slug with default base do not resolve' => [
2375  'uri' => 'https://website.local/welcome',
2376  'recordUpdates' => [
2377  'pages' => [
2378  [
2379  'data' => [
2380  'slug' => '/welcome/',
2381  ],
2382  'identifiers' => [
2383  'uid' => 1100,
2384  ],
2385  'types' => [],
2386  ],
2387  [
2388  'data' => [
2389  'slug' => '/bienvenue/',
2390  ],
2391  'identifiers' => [
2392  'uid' => 1101,
2393  ],
2394  'types' => [],
2395  ],
2396  [
2397  'data' => [
2398  'slug' => '/bienvenue/',
2399  ],
2400  'identifiers' => [
2401  'uid' => 1102,
2402  ],
2403  'types' => [],
2404  ],
2405  [
2406  'data' => [
2407  'slug' => '/简-bienvenue/',
2408  ],
2409  'identifiers' => [
2410  'uid' => 1103,
2411  ],
2412  'types' => [],
2413  ],
2414  ],
2415  ],
2416  'fallbackIdentifiers' => [
2417  'EN',
2418  ],
2419  'fallbackType' => 'strict',
2420  'expectedStatusCode' => 404,
2421  'expectedPageTitle' => null,
2422  ];
2423 
2424  // Using default language slug with language base should be page not found if language page is active.
2425  yield '#3 Default slug with default base do not resolve strict without fallback' => [
2426  'uri' => 'https://website.local/welcome',
2427  'recordUpdates' => [
2428  'pages' => [
2429  [
2430  'data' => [
2431  'slug' => '/welcome/',
2432  ],
2433  'identifiers' => [
2434  'uid' => 1100,
2435  ],
2436  'types' => [],
2437  ],
2438  [
2439  'data' => [
2440  'slug' => '/bienvenue/',
2441  ],
2442  'identifiers' => [
2443  'uid' => 1101,
2444  ],
2445  'types' => [],
2446  ],
2447  [
2448  'data' => [
2449  'slug' => '/bienvenue/',
2450  ],
2451  'identifiers' => [
2452  'uid' => 1102,
2453  ],
2454  'types' => [],
2455  ],
2456  [
2457  'data' => [
2458  'slug' => '/简-bienvenue/',
2459  ],
2460  'identifiers' => [
2461  'uid' => 1103,
2462  ],
2463  'types' => [],
2464  ],
2465  ],
2466  ],
2467  'fallbackIdentifiers' => [],
2468  'fallbackType' => 'fallback',
2469  'expectedStatusCode' => 404,
2470  'expectedPageTitle' => null,
2471  ];
2472 
2473  // Using default language slug with language base should be page not found if language page is active.
2474  yield '#3 Default slug with default base do not resolve fallback' => [
2475  'uri' => 'https://website.local/welcome',
2476  'recordUpdates' => [
2477  'pages' => [
2478  [
2479  'data' => [
2480  'slug' => '/welcome/',
2481  ],
2482  'identifiers' => [
2483  'uid' => 1100,
2484  ],
2485  'types' => [],
2486  ],
2487  [
2488  'data' => [
2489  'slug' => '/bienvenue/',
2490  ],
2491  'identifiers' => [
2492  'uid' => 1101,
2493  ],
2494  'types' => [],
2495  ],
2496  [
2497  'data' => [
2498  'slug' => '/bienvenue/',
2499  ],
2500  'identifiers' => [
2501  'uid' => 1102,
2502  ],
2503  'types' => [],
2504  ],
2505  [
2506  'data' => [
2507  'slug' => '/简-bienvenue/',
2508  ],
2509  'identifiers' => [
2510  'uid' => 1103,
2511  ],
2512  'types' => [],
2513  ],
2514  ],
2515  ],
2516  'fallbackIdentifiers' => [
2517  'EN',
2518  ],
2519  'fallbackType' => 'fallback',
2520  'expectedStatusCode' => 404,
2521  'expectedPageTitle' => null,
2522  ];
2523 
2524  // Using default language slug with language base resolves for inactive / hidden language page
2525  yield '#3 Default slug with default base but inactive language page resolves' => [
2526  'uri' => 'https://website.local/welcome',
2527  'recordUpdates' => [
2528  'pages' => [
2529  [
2530  'data' => [
2531  'slug' => '/welcome/',
2532  ],
2533  'identifiers' => [
2534  'uid' => 1100,
2535  ],
2536  'types' => [],
2537  ],
2538  [
2539  'data' => [
2540  'slug' => '/bienvenue/',
2541  'hidden' => 1,
2542  ],
2543  'identifiers' => [
2544  'uid' => 1101,
2545  ],
2546  'types' => [],
2547  ],
2548  [
2549  'data' => [
2550  'slug' => '/bienvenue/',
2551  ],
2552  'identifiers' => [
2553  'uid' => 1102,
2554  ],
2555  'types' => [],
2556  ],
2557  [
2558  'data' => [
2559  'slug' => '/简-bienvenue/',
2560  ],
2561  'identifiers' => [
2562  'uid' => 1103,
2563  ],
2564  'types' => [],
2565  ],
2566  ],
2567  ],
2568  'fallbackIdentifiers' => [
2569  'EN',
2570  ],
2571  'fallbackType' => 'strict',
2572  'expectedStatusCode' => 200,
2573  'expectedPageTitle' => 'EN: Welcome',
2574  ];
2575 
2576  // -------------------------------------------------------------------
2577  // #4 page slug without trailing slash, request without trailing slash
2578  // -------------------------------------------------------------------
2579 
2580  yield '#4 Default slug with default base resolves' => [
2581  'uri' => 'https://website.local/en-en/welcome',
2582  'recordUpdates' => [],
2583  'fallbackIdentifiers' => [
2584  'EN',
2585  ],
2586  'fallbackType' => 'strict',
2587  'expectedStatusCode' => 200,
2588  'expectedPageTitle' => 'EN: Welcome',
2589  ];
2590 
2591  yield '#4 FR slug with FR base resolves' => [
2592  'uri' => 'https://website.local/bienvenue',
2593  'recordUpdates' => [],
2594  'fallbackIdentifiers' => [
2595  'EN',
2596  ],
2597  'fallbackType' => 'strict',
2598  'expectedStatusCode' => 200,
2599  'expectedPageTitle' => 'FR: Welcome',
2600  ];
2601 
2602  // Using default language slug with language base should be page not found if language page is active.
2603  yield '#4 Default slug with default base do not resolve' => [
2604  'uri' => 'https://website.local/welcome',
2605  'recordUpdates' => [],
2606  'fallbackIdentifiers' => [
2607  'EN',
2608  ],
2609  'fallbackType' => 'strict',
2610  'expectedStatusCode' => 404,
2611  'expectedPageTitle' => null,
2612  ];
2613 
2614  // Using default language slug with language base should be page not found if language page is active.
2615  yield '#4 Default slug with default base do not resolve strict without fallback' => [
2616  'uri' => 'https://website.local/welcome',
2617  'recordUpdates' => [],
2618  'fallbackIdentifiers' => [],
2619  'fallbackType' => 'fallback',
2620  'expectedStatusCode' => 404,
2621  'expectedPageTitle' => null,
2622  ];
2623 
2624  // Using default language slug with language base should be page not found if language page is active.
2625  yield '#4 Default slug with default base do not resolve fallback' => [
2626  'uri' => 'https://website.local/welcome',
2627  'recordUpdates' => [],
2628  'fallbackIdentifiers' => [
2629  'EN',
2630  ],
2631  'fallbackType' => 'fallback',
2632  'expectedStatusCode' => 404,
2633  'expectedPageTitle' => null,
2634  ];
2635 
2636  // Using default language slug with language base resolves for inactive / hidden language page
2637  yield '#4 Default slug with default base but inactive language page resolves' => [
2638  'uri' => 'https://website.local/welcome',
2639  'recordUpdates' => [
2640  'pages' => [
2641  [
2642  'data' => [
2643  'hidden' => 1,
2644  ],
2645  'identifiers' => [
2646  'uid' => 1101,
2647  ],
2648  'types' => [],
2649  ],
2650  ],
2651  ],
2652  'fallbackIdentifiers' => [
2653  'EN',
2654  ],
2655  'fallbackType' => 'strict',
2656  'expectedStatusCode' => 200,
2657  'expectedPageTitle' => 'EN: Welcome',
2658  ];
2659  }
2660 
2664  #[DataProvider('defaultLanguagePageNotResolvedForSiteLanguageBaseWithNonDefaultLanguageShorterUriIfLanguagePageExistsDataProvider')]
2665  #[Test]
2666  public function defaultLanguagePageNotResolvedForSiteLanguageBaseWithNonDefaultLanguageShorterUriIfLanguagePageExists(string $uri, array $recordUpdates, array $fallbackIdentifiers, string $fallbackType, int $expectedStatusCode, ?string $expectedPageTitle): void
2667  {
2669  'website-local',
2670  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
2671  [
2672  $this->‪buildDefaultLanguageConfiguration('EN', '/en-en'),
2673  $this->‪buildLanguageConfiguration('FR', 'https://website.local/', ['EN']),
2674  ]
2675  );
2676  if ($recordUpdates !== []) {
2677  foreach ($recordUpdates as $table => $records) {
2678  foreach ($records as ‪$record) {
2679  $this->getConnectionPool()->getConnectionForTable($table)
2680  ->update(
2681  $table,
2682  ‪$record['data'] ?? [],
2683  ‪$record['identifiers'] ?? [],
2684  ‪$record['types'] ?? []
2685  );
2686  }
2687  }
2688  }
2689 
2690  $response = $this->executeFrontendSubRequest(new InternalRequest($uri));
2691  $responseStructure = ResponseContent::fromString(
2692  (string)$response->getBody()
2693  );
2694 
2695  self::assertSame(
2696  $expectedStatusCode,
2697  $response->getStatusCode()
2698  );
2699  if ($expectedPageTitle !== null) {
2700  self::assertSame(
2701  $expectedPageTitle,
2702  $responseStructure->getScopePath('page/title')
2703  );
2704  }
2705  }
2706 
2707  public static function getUrisWithInvalidLegacyQueryParameters(): \Generator
2708  {
2709  $uri = new ‪Uri('https://website.local/welcome/');
2710  yield '#0 id with float value having a zero decimal' => [
2711  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => '1110.0'])),
2712  ];
2713  yield '#1 id string value with tailing numbers' => [
2714  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => 'step1110'])),
2715  ];
2716  yield '#2 id string value with leading numbers' => [
2717  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => '1110step'])),
2718  ];
2719  yield '#3 id string value without numbers' => [
2720  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => 'foobar'])),
2721  ];
2722  yield '#4 id string value with a exponent' => [
2723  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => '11e10'])),
2724  ];
2725  yield '#5 id with a zero as value' => [
2726  'uri' => $uri->withQuery(‪HttpUtility::buildQueryString(['id' => 0])),
2727  ];
2728  }
2729 
2730  #[DataProvider('getUrisWithInvalidLegacyQueryParameters')]
2731  #[Test]
2732  public function requestWithInvalidLegacyQueryParametersDisplayPageNotFoundPage(UriInterface $uri): void
2733  {
2735  'website-local',
2736  $this->‪buildSiteConfiguration(1000, 'https://website.local/'),
2737  [],
2738  $this->‪buildErrorHandlingConfiguration('PHP', [404])
2739  );
2740  $response = $this->executeFrontendSubRequest(
2741  new InternalRequest((string)$uri),
2742  new InternalRequestContext()
2743  );
2744  $json = json_decode((string)$response->getBody(), true);
2745  self::assertSame(404, $response->getStatusCode());
2746  self::assertThat(
2747  $json['message'] ?? null,
2748  self::stringContains('The requested page does not exist')
2749  );
2750  }
2751 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildLanguageConfiguration
‪buildLanguageConfiguration(string $identifier, string $base, array $fallbackIdentifiers=[], string $fallbackType=null)
Definition: SiteBasedTestTrait.php:108
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\SlugSiteRequestTest
Definition: SlugSiteRequestTest.php:34
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\writeSiteConfiguration
‪writeSiteConfiguration(string $identifier, array $site=[], array $languages=[], array $errorHandling=[])
Definition: SiteBasedTestTrait.php:50
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\AbstractTestCase
Definition: AbstractTestCase.php:29
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildSiteConfiguration
‪buildSiteConfiguration(int $rootPageId, string $base='')
Definition: SiteBasedTestTrait.php:88
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildErrorHandlingConfiguration
‪buildErrorHandlingConfiguration(string $handler, array $codes)
Definition: SiteBasedTestTrait.php:142
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:30
‪TYPO3\CMS\Core\Utility\PermutationUtility
Definition: PermutationUtility.php:24
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:124
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling
Definition: AbstractTestCase.php:18
‪TYPO3\CMS\Core\Utility\PermutationUtility\meltStringItems
‪static string[] meltStringItems(array $payload, string $previousResult='')
Definition: PermutationUtility.php:36
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildDefaultLanguageConfiguration
‪buildDefaultLanguageConfiguration(string $identifier, string $base)
Definition: SiteBasedTestTrait.php:98
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:24