‪TYPO3CMS  ‪main
SlugLinkGeneratorTest.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 TYPO3\CMS\Backend\Utility\BackendUtility;
25 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
26 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
27 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
28 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
29 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
30 
32 {
33  protected function ‪setUp(): void
34  {
35  parent::setUp();
36 
38  'acme-com',
39  $this->‪buildSiteConfiguration(1000, 'https://acme.com/'),
40  [
41  $this->‪buildDefaultLanguageConfiguration('EN', 'https://acme.us/'),
42  $this->‪buildLanguageConfiguration('FR', 'https://acme.fr/', ['EN']),
43  $this->‪buildLanguageConfiguration('FR-CA', 'https://acme.ca/', ['FR', 'EN']),
44  ]
45  );
47  'products-acme-com',
48  $this->‪buildSiteConfiguration(1300, 'https://products.acme.com/')
49  );
51  'blog-acme-com',
52  $this->‪buildSiteConfiguration(2000, 'https://blog.acme.com/')
53  );
55  'john-blog-acme-com',
56  $this->‪buildSiteConfiguration(2110, 'https://blog.acme.com/john/')
57  );
59  'jane-blog-acme-com',
60  $this->‪buildSiteConfiguration(2120, 'https://blog.acme.com/jane/')
61  );
63  'archive-acme-com',
64  $this->‪buildSiteConfiguration(3000, 'https://archive.acme.com/'),
65  [
66  $this->‪buildDefaultLanguageConfiguration('EN', '/'),
67  $this->‪buildLanguageConfiguration('FR', 'https://archive.acme.com/fr/', ['EN']),
68  $this->‪buildLanguageConfiguration('FR-CA', 'https://archive.acme.com/ca/', ['FR', 'EN']),
69  ]
70  );
72  'common-collection',
73  $this->‪buildSiteConfiguration(7000, 'https://common.acme.com/')
74  );
76  'usual-collection',
77  $this->‪buildSiteConfiguration(8000, 'https://usual.acme.com/')
78  );
79 
80  $this->withDatabaseSnapshot(
81  function () {
82  $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
83  $backendUser = $this->setUpBackendUser(1);
84  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
85  $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml';
86  $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
87  $writer = DataHandlerWriter::withBackendUser($backendUser);
88  $writer->invokeFactory($factory);
89  static::failIfArrayIsNotEmpty($writer->getErrors());
90  $this->setUpFrontendRootPage(1000, ['EXT:frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript'], ['title' => 'ACME Root']);
91  $this->setUpFrontendRootPage(2000, ['EXT:frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript'], ['title' => 'ACME Blog']);
92  },
93  function () {
94  $backendUser = $this->setUpBackendUser(1);
95  ‪$GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
96  }
97  );
98  }
99 
100  public static function ‪linkIsGeneratedDataProvider(): array
101  {
102  $instructions = [
103  // acme.com -> acme.com (same site)
104  ['https://acme.us/', 1100, 1000, '/welcome'], // shortcut page is resolved directly
105  ['https://acme.us/', 1100, 1100, '/welcome'],
106  ['https://acme.us/', 1100, 1200, '/features'],
107  ['https://acme.us/', 1100, 1210, '/features/frontend-editing/'],
108  ['https://acme.us/', 1100, 404, '/404'],
109  // acme.com -> products.acme.com (nested sub-site)
110  ['https://acme.us/', 1100, 1300, 'https://products.acme.com/products'],
111  ['https://acme.us/', 1100, 1310, 'https://products.acme.com/products/planets'],
112  // acme.com -> blog.acme.com (different site)
113  ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/authors'], // recursive shortcut page is resolved directly
114  ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/authors'],
115  ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/john'],
116  ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/about-john'],
117  // blog.acme.com -> acme.com (different site)
118  ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/welcome'], // shortcut page is resolved directly
119  ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/welcome'],
120  ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/features'],
121  ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/features/frontend-editing/'],
122  ['https://blog.acme.com/', 2100, 404, 'https://acme.us/404'],
123  // blog.acme.com -> products.acme.com (different sub-site)
124  ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/products'],
125  ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/products/planets'],
126  ];
127  return self::keysFromTemplate($instructions, '%2$d->%3$d');
128  }
129 
130  #[DataProvider('linkIsGeneratedDataProvider')]
131  #[Test]
132  public function ‪linkIsGenerated(string $hostPrefix, int $sourcePageId, int $targetPageId, string $expectation): void
133  {
134  $response = $this->executeFrontendSubRequest(
135  (new InternalRequest($hostPrefix))
136  ->withPageId($sourcePageId)
137  ->withInstructions([
138  $this->createTypoLinkUrlInstruction([
139  'parameter' => $targetPageId,
140  ]),
141  ])
142  );
143 
144  self::assertSame($expectation, (string)$response->getBody());
145  }
146 
147  public static function ‪linkIsGeneratedFromMountPointDataProvider(): array
148  {
149  $instructions = [
150  // acme.com -> acme.com (same site)
151  ['https://acme.us/', [7100, 1700], 7110, 1000, '/welcome'], // shortcut page is resolved directly
152  ['https://acme.us/', [7100, 1700], 7110, 1100, '/welcome'],
153  ['https://acme.us/', [7100, 1700], 7110, 1200, '/features'],
154  ['https://acme.us/', [7100, 1700], 7110, 1210, '/features/frontend-editing/'],
155  ['https://acme.us/', [7100, 1700], 7110, 404, '/404'],
156  // acme.com -> products.acme.com (nested sub-site)
157  ['https://acme.us/', [7100, 1700], 7110, 1300, 'https://products.acme.com/products'],
158  ['https://acme.us/', [7100, 1700], 7110, 1310, 'https://products.acme.com/products/planets'],
159  // acme.com -> blog.acme.com (different site)
160  ['https://acme.us/', [7100, 1700], 7110, 2000, 'https://blog.acme.com/authors'], // shortcut page is resolved directly
161  ['https://acme.us/', [7100, 1700], 7110, 2100, 'https://blog.acme.com/authors'],
162  ['https://acme.us/', [7100, 1700], 7110, 2110, 'https://blog.acme.com/john/john'],
163  ['https://acme.us/', [7100, 1700], 7110, 2111, 'https://blog.acme.com/john/about-john'],
164  // blog.acme.com -> acme.com (different site)
165  ['https://blog.acme.com/', [7100, 2700], 7110, 1000, 'https://acme.us/welcome'], // shortcut page is resolved directly
166  ['https://blog.acme.com/', [7100, 2700], 7110, 1100, 'https://acme.us/welcome'],
167  ['https://blog.acme.com/', [7100, 2700], 7110, 1200, 'https://acme.us/features'],
168  ['https://blog.acme.com/', [7100, 2700], 7110, 1210, 'https://acme.us/features/frontend-editing/'],
169  ['https://blog.acme.com/', [7100, 2700], 7110, 404, 'https://acme.us/404'],
170  // blog.acme.com -> products.acme.com (different sub-site)
171  ['https://blog.acme.com/', [7100, 2700], 7110, 1300, 'https://products.acme.com/products'],
172  ['https://blog.acme.com/', [7100, 2700], 7110, 1310, 'https://products.acme.com/products/planets'],
173  ];
174 
175  return self::keysFromTemplate(
176  $instructions,
177  '%3$d->%4$d (mount:%2$s)',
178  static function (array $items) {
179  array_splice(
180  $items,
181  1,
182  1,
183  [implode('->', $items[1])]
184  );
185  return $items;
186  }
187  );
188  }
189 
190  #[DataProvider('linkIsGeneratedFromMountPointDataProvider')]
191  #[Test]
192  public function ‪linkIsGeneratedFromMountPoint(string $hostPrefix, array $pageMount, int $sourcePageId, int $targetPageId, string $expectation): void
193  {
194  $response = $this->executeFrontendSubRequest(
195  (new InternalRequest($hostPrefix))
196  ->withMountPoint(...$pageMount)
197  ->withPageId($sourcePageId)
198  ->withInstructions([
199  $this->createTypoLinkUrlInstruction([
200  'parameter' => $targetPageId,
201  ]),
202  ])
203  );
204 
205  self::assertSame($expectation, (string)$response->getBody());
206  }
207 
208  public static function ‪linkIsGeneratedForLanguageDataProvider(): array
209  {
210  $instructions = [
211  // acme.com -> acme.com (same site)
212  ['https://acme.us/', 1100, 1100, 0, '/welcome'],
213  ['https://acme.us/', 1100, 1100, 1, 'https://acme.fr/bienvenue'],
214  ['https://acme.us/', 1100, 1100, 2, 'https://acme.ca/bienvenue'],
215  ['https://acme.us/', 1100, 1101, 0, 'https://acme.fr/bienvenue'],
216  ['https://acme.us/', 1100, 1102, 0, 'https://acme.ca/bienvenue'],
217  // acme.com -> products.acme.com (nested sub-site)
218  ['https://acme.us/', 1100, 1300, 0, 'https://products.acme.com/products'],
219  ['https://acme.us/', 1100, 1310, 0, 'https://products.acme.com/products/planets'],
220  // acme.com -> products.acme.com (nested sub-site, l18n_cfg=1)
221  ['https://acme.us/', 1100, 1410, 0, ''],
222  ['https://acme.us/', 1100, 1410, 1, 'https://acme.fr/acme-dans-votre-region/groupes'],
223  ['https://acme.us/', 1100, 1410, 2, 'https://acme.ca/acme-dans-votre-quebec/groupes'],
224  ['https://acme.us/', 1100, 1411, 0, 'https://acme.fr/acme-dans-votre-region/groupes'],
225  ['https://acme.us/', 1100, 1412, 0, 'https://acme.ca/acme-dans-votre-quebec/groupes'],
226  // acme.com -> archive (outside site)
227  ['https://acme.us/', 1100, 3100, 0, 'https://archive.acme.com/statistics'],
228  ['https://acme.us/', 1100, 3100, 1, 'https://archive.acme.com/fr/statistics'],
229  ['https://acme.us/', 1100, 3100, 2, 'https://archive.acme.com/ca/statistics'],
230  ['https://acme.us/', 1100, 3101, 0, 'https://archive.acme.com/fr/statistics'],
231  ['https://acme.us/', 1100, 3102, 0, 'https://archive.acme.com/ca/statistics'],
232  // blog.acme.com -> acme.com (different site)
233  ['https://blog.acme.com/', 2100, 1100, 0, 'https://acme.us/welcome'],
234  ['https://blog.acme.com/', 2100, 1100, 1, 'https://acme.fr/bienvenue'],
235  ['https://blog.acme.com/', 2100, 1100, 2, 'https://acme.ca/bienvenue'],
236  ['https://blog.acme.com/', 2100, 1101, 0, 'https://acme.fr/bienvenue'],
237  ['https://blog.acme.com/', 2100, 1102, 0, 'https://acme.ca/bienvenue'],
238  // blog.acme.com -> archive (outside site)
239  ['https://blog.acme.com/', 2100, 3100, 0, 'https://archive.acme.com/statistics'],
240  ['https://blog.acme.com/', 2100, 3100, 1, 'https://archive.acme.com/fr/statistics'],
241  ['https://blog.acme.com/', 2100, 3100, 2, 'https://archive.acme.com/ca/statistics'],
242  ['https://blog.acme.com/', 2100, 3101, 0, 'https://archive.acme.com/fr/statistics'],
243  ['https://blog.acme.com/', 2100, 3102, 0, 'https://archive.acme.com/ca/statistics'],
244  // blog.acme.com -> products.acme.com (different sub-site)
245  ['https://blog.acme.com/', 2100, 1300, 0, 'https://products.acme.com/products'],
246  ['https://blog.acme.com/', 2100, 1310, 0, 'https://products.acme.com/products/planets'],
247  ];
248  return self::keysFromTemplate($instructions, '%2$d->%3$d (lang:%4$d)');
249  }
250 
251  #[DataProvider('linkIsGeneratedForLanguageDataProvider')]
252  #[Test]
253  public function ‪linkIsGeneratedForLanguageWithLanguageProperty(string $hostPrefix, int $sourcePageId, int $targetPageId, int $targetLanguageId, string $expectation): void
254  {
255  $response = $this->executeFrontendSubRequest(
256  (new InternalRequest($hostPrefix))
257  ->withPageId($sourcePageId)
258  ->withInstructions([
259  $this->createTypoLinkUrlInstruction([
260  'parameter' => $targetPageId,
261  'language' => $targetLanguageId,
262  ]),
263  ])
264  );
265 
266  self::assertSame($expectation, (string)$response->getBody());
267  }
268 
270  {
271  $instructions = [
272  // acme.com -> acme.com (same site)
273  ['https://acme.us/', 1100, 1000, '/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'], // shortcut page is resolved directly
274  ['https://acme.us/', 1100, 1100, '/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
275  ['https://acme.us/', 1100, 1200, '/features?testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
276  ['https://acme.us/', 1100, 1210, '/features/frontend-editing/?testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
277  ['https://acme.us/', 1100, 404, '/404?testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
278  // acme.com -> products.acme.com (nested sub-site)
279  ['https://acme.us/', 1100, 1300, 'https://products.acme.com/products?testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
280  ['https://acme.us/', 1100, 1310, 'https://products.acme.com/products/planets?testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
281  // acme.com -> blog.acme.com (different site)
282  ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/authors?testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'], // shortcut page is resolved directly
283  ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/authors?testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'],
284  ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/john?testing%5Bvalue%5D=1&cHash=bf25eea89f44a9a79dabdca98f38a432'],
285  ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/about-john?testing%5Bvalue%5D=1&cHash=42dbaeb9172b6b1ca23b49941e194db2'],
286  // blog.acme.com -> acme.com (different site)
287  ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'], // shortcut page is resolved directly
288  ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/welcome?testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
289  ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/features?testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
290  ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/features/frontend-editing/?testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
291  ['https://blog.acme.com/', 2100, 404, 'https://acme.us/404?testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
292  // blog.acme.com -> products.acme.com (different sub-site)
293  ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/products?testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
294  ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/products/planets?testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
295  ];
296 
297  return self::keysFromTemplate(
298  $instructions,
299  '%2$d->%3$d'
300  );
301  }
302 
303  #[DataProvider('linkIsGeneratedWithQueryParametersDataProvider')]
304  #[Test]
305  public function ‪linkIsGeneratedWithQueryParameters(string $hostPrefix, int $sourcePageId, int $targetPageId, string $expectation): void
306  {
307  $response = $this->executeFrontendSubRequest(
308  (new InternalRequest($hostPrefix))
309  ->withPageId($sourcePageId)
310  ->withInstructions([
311  $this->createTypoLinkUrlInstruction([
312  'parameter' => $targetPageId,
313  'additionalParams' => '&testing[value]=1',
314  ]),
315  ])
316  );
317 
318  self::assertSame($expectation, (string)$response->getBody());
319  }
320 
321  public static function ‪linkIsGeneratedForRestrictedPageDataProvider(): array
322  {
323  $instructions = [
324  ['https://acme.us/', 1100, 1510, 0, ''],
325  // ['https://acme.us/', 1100, 1511, 0, ''], // @todo Fails, not expanded to sub-pages
326  ['https://acme.us/', 1100, 1512, 0, ''],
327  ['https://acme.us/', 1100, 1515, 0, ''],
328  ['https://acme.us/', 1100, 1520, 0, ''],
329  // ['https://acme.us/', 1100, 1521, 0, ''], // @todo Fails, not expanded to sub-pages
330  //
331  ['https://acme.us/', 1100, 1510, 1, '/my-acme/whitepapers'],
332  ['https://acme.us/', 1100, 1511, 1, '/my-acme/whitepapers/products'],
333  ['https://acme.us/', 1100, 1512, 1, '/my-acme/whitepapers/solutions'],
334  ['https://acme.us/', 1100, 1515, 1, ''],
335  ['https://acme.us/', 1100, 1520, 1, ''],
336  // ['https://acme.us/', 1100, 1521, 1, ''], // @todo Fails, not expanded to sub-pages
337  //
338  ['https://acme.us/', 1100, 1510, 2, '/my-acme/whitepapers'],
339  ['https://acme.us/', 1100, 1511, 2, '/my-acme/whitepapers/products'],
340  ['https://acme.us/', 1100, 1512, 2, ''],
341  ['https://acme.us/', 1100, 1515, 2, '/my-acme/whitepapers/research'],
342  ['https://acme.us/', 1100, 1520, 2, '/my-acme/forecasts'],
343  ['https://acme.us/', 1100, 1521, 2, '/my-acme/forecasts/current-year'],
344  //
345  ['https://acme.us/', 1100, 1510, 3, '/my-acme/whitepapers'],
346  ['https://acme.us/', 1100, 1511, 3, '/my-acme/whitepapers/products'],
347  ['https://acme.us/', 1100, 1512, 3, '/my-acme/whitepapers/solutions'],
348  ['https://acme.us/', 1100, 1515, 3, '/my-acme/whitepapers/research'],
349  ['https://acme.us/', 1100, 1520, 3, '/my-acme/forecasts'],
350  ['https://acme.us/', 1100, 1521, 3, '/my-acme/forecasts/current-year'],
351  ];
352  return self::keysFromTemplate($instructions, '%2$d->%3$d (user:%4$d)');
353  }
354 
355  #[DataProvider('linkIsGeneratedForRestrictedPageDataProvider')]
356  #[Test]
357  public function ‪linkIsGeneratedForRestrictedPage(string $hostPrefix, int $sourcePageId, int $targetPageId, int $frontendUserId, string $expectation): void
358  {
359  $response = $this->executeFrontendSubRequest(
360  (new InternalRequest($hostPrefix))
361  ->withPageId($sourcePageId)
362  ->withInstructions([
363  $this->createTypoLinkUrlInstruction([
364  'parameter' => $targetPageId,
365  ]),
366  ]),
367  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
368  );
369 
370  self::assertSame($expectation, (string)$response->getBody());
371  }
372 
374  {
375  $instructions = [
376  // no frontend user given
377  ['https://acme.us/', 1100, 1510, 1500, 0, '<a href="/my-acme?pageId=1510&amp;cHash=119c4870e323bb7e8c9fae2941726b0d" data-access-restricted="true">Whitepapers</a>'],
378  // ['https://acme.us/', 1100, 1511, 1500, 0, '<a href="/my-acme?pageId=1511"></a>'], // @todo Fails, not expanded to sub-pages
379  ['https://acme.us/', 1100, 1512, 1500, 0, '<a href="/my-acme?pageId=1512&amp;cHash=0ced3db0fd4aae0019a99f59cfa58cb0" data-access-restricted="true">Solutions</a>'],
380  ['https://acme.us/', 1100, 1515, 1500, 0, '<a href="/my-acme?pageId=1515&amp;cHash=176f16b31d2c731347d411861d8b06dc" data-access-restricted="true">Research</a>'],
381  ['https://acme.us/', 1100, 1520, 1500, 0, '<a href="/my-acme?pageId=1520&amp;cHash=253d3dccd4794c4a9473226f683bc36a" data-access-restricted="true">Forecasts</a>'],
382  // ['https://acme.us/', 1100, 1521, 1500, 0, '<a href="/my-acme?pageId=1521"></a>'], // @todo Fails, not expanded to sub-pages
383  // frontend user 1
384  ['https://acme.us/', 1100, 1510, 1500, 1, '<a href="/my-acme/whitepapers">Whitepapers</a>'],
385  ['https://acme.us/', 1100, 1511, 1500, 1, '<a href="/my-acme/whitepapers/products">Products</a>'],
386  ['https://acme.us/', 1100, 1512, 1500, 1, '<a href="/my-acme/whitepapers/solutions">Solutions</a>'],
387  ['https://acme.us/', 1100, 1515, 1500, 1, '<a href="/my-acme?pageId=1515&amp;cHash=176f16b31d2c731347d411861d8b06dc" data-access-restricted="true">Research</a>'],
388  ['https://acme.us/', 1100, 1520, 1500, 1, '<a href="/my-acme?pageId=1520&amp;cHash=253d3dccd4794c4a9473226f683bc36a" data-access-restricted="true">Forecasts</a>'],
389  // ['https://acme.us/', 1100, 1521, 1500, 1, '<a href="/my-acme?pageId=1521"></a>'], // @todo Fails, not expanded to sub-pages
390  // frontend user 2
391  ['https://acme.us/', 1100, 1510, 1500, 2, '<a href="/my-acme/whitepapers">Whitepapers</a>'],
392  ['https://acme.us/', 1100, 1511, 1500, 2, '<a href="/my-acme/whitepapers/products">Products</a>'],
393  ['https://acme.us/', 1100, 1512, 1500, 2, '<a href="/my-acme?pageId=1512&amp;cHash=0ced3db0fd4aae0019a99f59cfa58cb0" data-access-restricted="true">Solutions</a>'],
394  ['https://acme.us/', 1100, 1515, 1500, 2, '<a href="/my-acme/whitepapers/research">Research</a>'],
395  ['https://acme.us/', 1100, 1520, 1500, 2, '<a href="/my-acme/forecasts">Forecasts</a>'],
396  ['https://acme.us/', 1100, 1521, 1500, 2, '<a href="/my-acme/forecasts/current-year">Current Year</a>'],
397  // frontend user 3
398  ['https://acme.us/', 1100, 1510, 1500, 3, '<a href="/my-acme/whitepapers">Whitepapers</a>'],
399  ['https://acme.us/', 1100, 1511, 1500, 3, '<a href="/my-acme/whitepapers/products">Products</a>'],
400  ['https://acme.us/', 1100, 1512, 1500, 3, '<a href="/my-acme/whitepapers/solutions">Solutions</a>'],
401  ['https://acme.us/', 1100, 1515, 1500, 3, '<a href="/my-acme/whitepapers/research">Research</a>'],
402  ['https://acme.us/', 1100, 1520, 1500, 3, '<a href="/my-acme/forecasts">Forecasts</a>'],
403  ['https://acme.us/', 1100, 1521, 1500, 3, '<a href="/my-acme/forecasts/current-year">Current Year</a>'],
404  ];
405  return self::keysFromTemplate($instructions, '%2$d->%3$d (via: %4$d, user:%5$d)');
406  }
407 
408  #[DataProvider('linkIsGeneratedForRestrictedPageUsingLoginPageDataProvider')]
409  #[Test]
410  public function ‪linkIsGeneratedForRestrictedPageUsingLoginPage(string $hostPrefix, int $sourcePageId, int $targetPageId, int $loginPageId, int $frontendUserId, string $expectation): void
411  {
412  $response = $this->executeFrontendSubRequest(
413  (new InternalRequest($hostPrefix))
414  ->withPageId($sourcePageId)
415  ->withInstructions([
416  (new TypoScriptInstruction())
417  ->withTypoScript([
418  'config.' => [
419  'typolinkLinkAccessRestrictedPages' => $loginPageId,
420  'typolinkLinkAccessRestrictedPages_addParams' => '&pageId=###PAGE_ID###',
421  'typolinkLinkAccessRestrictedPages.' => [
422  'ATagParams' => 'data-access-restricted="true"',
423  ],
424  ],
425  ]),
426  $this->createTypoLinkTagInstruction([
427  'parameter' => $targetPageId,
428  ]),
429  ]),
430  (new InternalRequestContext())->withFrontendUserId($frontendUserId)
431  );
432 
433  self::assertSame($expectation, (string)$response->getBody());
434  }
435 
437  {
438  $instructions = [
439  // default language (0)
440  ['https://acme.us/', 1100, 1510, 0, '/my-acme/whitepapers'],
441  ['https://acme.us/', 1100, 1512, 0, '/my-acme/whitepapers/solutions'],
442  ['https://acme.us/', 1100, 1515, 0, '/my-acme/whitepapers/research'],
443  // french language (1)
444  ['https://acme.fr/', 1100, 1510, 1, '/my-acme/papiersblanc'],
445  ['https://acme.fr/', 1100, 1512, 1, '/my-acme/papiersblanc/la-solutions'],
446  ['https://acme.fr/', 1100, 1515, 1, '/my-acme/papiersblanc/recherche'],
447  // canadian french language (2)
448  ['https://acme.ca/', 1100, 1510, 2, '/my-acme-ca/papiersblanc'],
449  ['https://acme.ca/', 1100, 1512, 2, '/my-acme-ca/papiersblanc/la-solutions'],
450  ['https://acme.ca/', 1100, 1515, 2, '/my-acme-ca/papiersblanc/recherche'],
451  ];
452  return self::keysFromTemplate($instructions, '%2$d->%3$d (language: %4$d)');
453  }
454 
455  #[DataProvider('linkIsGeneratedForRestrictedPageForGuestsUsingTypolinkLinkAccessRestrictedPagesDataProvider')]
456  #[Test]
457  public function ‪linkIsGeneratedForRestrictedPageForGuestsUsingTypolinkLinkAccessRestrictedPages(string $hostPrefix, int $sourcePageId, int $targetPageId, int $languageId, string $expectation): void
458  {
459  $response = $this->executeFrontendSubRequest(
460  (new InternalRequest($hostPrefix))
461  ->withPageId($sourcePageId)
462  ->withInstructions([
463  (new TypoScriptInstruction())
464  ->withTypoScript([
465  'config.' => [
466  'typolinkLinkAccessRestrictedPages' => 'NONE',
467  ],
468  ]),
469  $this->createTypoLinkUrlInstruction([
470  'parameter' => $targetPageId,
471  ]),
472  ])
473  );
474 
475  self::assertSame($expectation, (string)$response->getBody());
476  }
477 
478  public static function ‪linkIsGeneratedForPageVersionDataProvider(): array
479  {
480  $instructions = [
481  // acme.com -> acme.com (same site): link to changed page
482  ['https://acme.us/', 1100, 1100, false, 1, '/welcome-modified'],
483  ['https://acme.us/', 1100, 1100, true, 1, '/welcome-modified'],
484  ['https://acme.us/', 1100, 1100, false, 0, '/welcome'],
485  ['https://acme.us/', 1100, 1100, true, 0, ''], // @todo link is empty, but should create a link
486  // acme.com -> acme.com (same site): link to new page (no need to resolve the version for the new page)
487  ['https://acme.us/', 1100, 1950, false, 1, '/bye'],
488  ['https://acme.us/', 1100, 1950, false, 0, ''],
489  // blog.acme.com -> acme.com (different site): link to changed page
490  ['https://blog.acme.com/', 2100, 1100, true, 1, 'https://acme.us/welcome-modified'],
491  ['https://blog.acme.com/', 2100, 1100, false, 1, 'https://acme.us/welcome-modified'],
492  ['https://blog.acme.com/', 2100, 1100, false, 0, 'https://acme.us/welcome'],
493  ['https://blog.acme.com/', 2100, 1100, true, 0, ''], // @todo link is empty, but should create a link
494  // blog.acme.com -> acme.com (different site): link to new page (no need to resolve the version for the new page)
495  ['https://blog.acme.com/', 2100, 1950, false, 1, 'https://acme.us/bye'],
496  ['https://blog.acme.com/', 2100, 1950, false, 0, ''],
497  ];
498  return self::keysFromTemplate($instructions, '%2$d->%3$d (resolve:%4$d, be_user:%5$d)');
499  }
500 
501  #[DataProvider('linkIsGeneratedForPageVersionDataProvider')]
502  #[Test]
503  public function ‪linkIsGeneratedForPageVersion(string $hostPrefix, int $sourcePageId, int $targetPageId, bool $resolveVersion, int $backendUserId, string $expectation): void
504  {
505  $workspaceId = 1;
506  if ($resolveVersion) {
507  $targetPageId = BackendUtility::getWorkspaceVersionOfRecord(
508  $workspaceId,
509  'pages',
510  $targetPageId,
511  'uid'
512  )['uid'] ?? null;
513  $targetPageId = $targetPageId ? (int)$targetPageId : null;
514  }
515 
516  $response = $this->executeFrontendSubRequest(
517  (new InternalRequest($hostPrefix))
518  ->withPageId($sourcePageId)
519  ->withInstructions([
520  $this->createTypoLinkUrlInstruction([
521  'parameter' => $targetPageId,
522  ]),
523  ]),
524  (new InternalRequestContext())
525  ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0)
526  ->withBackendUserId($backendUserId)
527  );
528 
529  $expectation = str_replace(
530  ['{targetPageId}'],
531  [$targetPageId],
532  $expectation
533  );
534 
535  self::assertSame($expectation, (string)$response->getBody());
536  }
537 
538  public static function ‪hierarchicalMenuIsGeneratedDataProvider(): array
539  {
540  return [
541  'ACME Inc' => [
542  'https://acme.us/',
543  1100,
544  [
545  ['title' => 'EN: Welcome', 'link' => '/welcome', 'target' => ''],
546  [
547  'title' => 'ZH-CN: Welcome Default',
548  // Symfony UrlGenerator, which is used for uri generation, rawurlencodes the url internally.
549  'link' => '/%E7%AE%80-bienvenue',
550  'target' => '',
551  ],
552  [
553  'title' => 'EN: Features',
554  'link' => '/features',
555  'target' => '',
556  'children' => [
557  [
558  'title' => 'EN: Frontend Editing',
559  'link' => '/features/frontend-editing/',
560  'target' => '',
561  ],
562  ],
563  ],
564  [
565  'title' => 'EN: Products',
566  'link' => 'https://products.acme.com/products',
567  'target' => '',
568  'children' => [
569  [
570  'title' => 'EN: Planets',
571  'link' => 'https://products.acme.com/products/planets',
572  'target' => '',
573  ],
574  [
575  'title' => 'EN: Spaceships',
576  'link' => 'https://products.acme.com/products/spaceships',
577  'target' => '',
578  ],
579  [
580  'title' => 'EN: Dark Matter',
581  'link' => 'https://products.acme.com/products/dark-matter',
582  'target' => '',
583  ],
584  ],
585  ],
586  ['title' => 'EN: ACME in your Region', 'link' => '/acme-in-your-region', 'target' => ''],
587  [
588  'title' => 'Divider',
589  'link' => '/divider',
590  'target' => '',
591  'children' => [
592  [
593  'title' => 'EN: Subpage of Spacer',
594  'link' => '/divider/subpage-of-spacer',
595  'target' => '',
596  ],
597  ],
598  ],
599  ['title' => 'Internal', 'link' => '/my-acme', 'target' => ''],
600  ['title' => 'About us', 'link' => '/about', 'target' => ''],
601  [
602  'title' => 'Announcements & News',
603  'link' => '/news',
604  'target' => '',
605  'children' => [
606  [
607  'title' => 'Markets',
608  'link' => '/news/common/markets',
609  'target' => '',
610  ],
611  [
612  'title' => 'Products',
613  'link' => '/news/common/products',
614  'target' => '',
615  ],
616  [
617  'title' => 'Partners',
618  'link' => '/news/common/partners',
619  'target' => '_blank',
620  ],
621  ],
622  ],
623  ['title' => 'That page is forbidden to you', 'link' => '/403', 'target' => ''],
624  ['title' => 'That page was not found', 'link' => '/404', 'target' => ''],
625  ['title' => 'Our Blog', 'link' => 'https://blog.acme.com/authors', 'target' => ''],
626  ['title' => 'Cross Site Shortcut', 'link' => 'https://blog.acme.com/authors', 'target' => ''],
627  ],
628  ],
629  'ACME Blog' => [
630  'https://blog.acme.com/',
631  2100,
632  [
633  [
634  'title' => 'Authors',
635  'link' => '/authors',
636  'target' => '',
637  'children' => [
638  [
639  'title' => 'John Doe',
640  'link' => 'https://blog.acme.com/john/john',
641  'target' => '',
642  ],
643  [
644  'title' => 'Jane Doe',
645  'link' => 'https://blog.acme.com/jane/jane',
646  'target' => '',
647  ],
648  [
649  'title' => 'Malloy Doe',
650  'link' => '/malloy',
651  'target' => '',
652  ],
653  ],
654  ],
655  1 =>
656  [
657  'title' => 'Announcements & News',
658  'link' => '/news',
659  'target' => '',
660  'children' => [
661  [
662  'title' => 'Markets',
663  'link' => '/news/common/markets',
664  'target' => '',
665  ],
666  [
667  'title' => 'Products',
668  'link' => '/news/common/products',
669  'target' => '',
670  ],
671  [
672  'title' => 'Partners',
673  'link' => '/news/common/partners',
674  'target' => '_blank',
675  ],
676  ],
677  ],
678  ['title' => 'What is a blog on Wikipedia', 'link' => 'https://en.wikipedia.org/wiki/Blog', 'target' => 'a_new_tab'],
679  ['title' => 'Link to a query parameter', 'link' => '/authors?showOption=1&cHash=3ba1e68f3a2f76b865952c40b7c82c8b', 'target' => ''],
680  // target is empty because no fluid_styled_content typoscript with config.extTarget is active
681  ['title' => 'What is Wikipedia in a separate window', 'link' => 'https://en.wikipedia.org/', 'target' => ''],
682  ['title' => 'ACME Inc', 'link' => 'https://acme.us/welcome', 'target' => ''],
683  ],
684  ],
685  ];
686  }
687 
688  #[DataProvider('hierarchicalMenuIsGeneratedDataProvider')]
689  #[Test]
690  public function ‪hierarchicalMenuIsGenerated(string $hostPrefix, int $sourcePageId, array $expectation): void
691  {
692  $response = $this->executeFrontendSubRequest(
693  (new InternalRequest($hostPrefix))
694  ->withPageId($sourcePageId)
695  ->withInstructions([
696  $this->createHierarchicalMenuProcessorInstruction([
697  'levels' => 2,
698  'entryLevel' => 0,
699  'expandAll' => 1,
700  'includeSpacer' => 1,
701  'titleField' => 'title',
702  ]),
703  ])
704  );
705 
706  $json = json_decode((string)$response->getBody(), true);
707  $json = $this->filterMenu($json, ['title', 'link', 'target']);
708 
709  self::assertSame($expectation, $json);
710  }
711 
712  #[Test]
714  {
715  $expectation = [
716  [
717  'title' => 'John Doe',
718  'link' => 'https://blog.acme.com/john/john',
719  'hasSubpages' => 1,
720  'children' => [
721  [
722  'title' => 'About',
723  'link' => 'https://blog.acme.com/john/about-john',
724  'hasSubpages' => 0,
725  ],
726  ],
727  ],
728  [
729  'title' => 'Jane Doe',
730  'link' => 'https://blog.acme.com/jane/jane',
731  'hasSubpages' => 1,
732  'children' => [
733  [
734  'title' => 'About',
735  'link' => 'https://blog.acme.com/jane/about-jane',
736  'hasSubpages' => 0,
737  ],
738  ],
739  ],
740  [
741  'title' => 'Malloy Doe',
742  'link' => '/malloy',
743  'hasSubpages' => 0,
744  ],
745  ];
746  $response = $this->executeFrontendSubRequest(
747  (new InternalRequest('https://blog.acme.com/'))
748  ->withPageId(2130)
749  ->withInstructions([
750  $this->createHierarchicalMenuProcessorInstruction([
751  'levels' => 2,
752  'entryLevel' => 1,
753  'expandAll' => 1,
754  'includeSpacer' => 1,
755  'titleField' => 'title',
756  ]),
757  ])
758  );
759 
760  $json = json_decode((string)$response->getBody(), true);
761  $json = $this->filterMenu($json, ['title', 'link', 'hasSubpages']);
762 
763  self::assertSame($expectation, $json);
764  }
765 
767  {
768  return [
769  'regular page' => [
770  'https://acme.us/',
771  1310,
772  '1300',
773  [
774  [
775  'title' => 'EN: Products',
776  'link' => 'https://products.acme.com/products',
777  'active' => 1,
778  'current' => 0,
779  'children' => [
780  [
781  'title' => 'EN: Planets',
782  'link' => 'https://products.acme.com/products/planets',
783  'active' => 1,
784  'current' => 1,
785  ],
786  [
787  'title' => 'EN: Spaceships',
788  'link' => 'https://products.acme.com/products/spaceships',
789  'active' => 0,
790  'current' => 0,
791  ],
792  [
793  'title' => 'EN: Dark Matter',
794  'link' => 'https://products.acme.com/products/dark-matter',
795  'active' => 0,
796  'current' => 0,
797  ],
798  ],
799  ],
800  ],
801  ],
802  'resolved shortcut' => [
803  'https://blog.acme.com/',
804  2100,
805  '1930',
806  [
807  [
808  'title' => 'Our Blog',
809  'link' => '/authors',
810  'active' => 0,
811  'current' => 0,
812  ],
813  ],
814  ],
815  'resolved shortcut in translation' => [
816  'https://acme.fr/',
817  1100,
818  '2030',
819  [
820  [
821  'title' => 'Shortcut to Research - shows a different page',
822  'link' => '/acme-dans-votre-region',
823  'active' => 0,
824  'current' => 0,
825  ],
826  ],
827  ],
828 
829  ];
830  }
831 
832  #[DataProvider('hierarchicalMenuSetsActiveStateProperlyDataProvider')]
833  #[Test]
834  public function ‪hierarchicalMenuSetsActiveStateProperly(string $hostPrefix, int $sourcePageId, string $menuPageIds, array $expectation, int $languageId = 0): void
835  {
836  $response = $this->executeFrontendSubRequest(
837  (new InternalRequest($hostPrefix))
838  ->withPageId($sourcePageId)
839  ->withInstructions([
840  $this->createHierarchicalMenuProcessorInstruction([
841  'levels' => 2,
842  'special' => 'list',
843  'special.' => [
844  'value' => $menuPageIds,
845  ],
846  'includeSpacer' => 1,
847  'titleField' => 'title',
848  ]),
849  ]),
850  );
851 
852  $json = json_decode((string)$response->getBody(), true);
853  $json = $this->filterMenu($json, ['title', 'active', 'current', 'link']);
854 
855  self::assertSame($expectation, $json);
856  }
857 
859  {
860  return [
861  'no banned IDs in default languageId' => [
862  'languageId' => 0,
863  'excludedUidList' => '',
864  'expectedMenuItems' => 13,
865  ],
866  'no banned IDs in FR' => [
867  'languageId' => 1,
868  'excludedUidList' => '',
869  'expectedMenuItems' => 13,
870  ],
871  'banned IDs in default languageId' => [
872  'languageId' => 0,
873  'excludedUidList' => '1100,1200,1300,1400,403,404',
874  'expectedMenuItems' => 7,
875  ],
876  'banned IDs in FR languageId' => [
877  'languageId' => 1,
878  'excludedUidList' => '1100,1200,1300,1400,403,404',
879  'expectedMenuItems' => 7,
880  ],
881  'banned translated IDs in default languageId' => [
882  'languageId' => 0,
883  'excludedUidList' => '1101,1200,1300,1400,403,404',
884  'expectedMenuItems' => 8,
885  ],
886  'banned translated IDs in FR languageId' => [
887  'languageId' => 1,
888  'excludedUidList' => '1101,1200,1300,1400,403,404',
889  'expectedMenuItems' => 7,
890  ],
891  ];
892  }
893 
897  #[DataProvider('hierarchicalMenuAlwaysResolvesToDefaultLanguageDataProvider')]
898  #[Test]
899  public function ‪hierarchicalMenuAlwaysResolvesToDefaultLanguage(int $languageId, string $excludedUidList, int $expectedMenuItems): void
900  {
901  $response = $this->executeFrontendSubRequest(
902  (new InternalRequest('https://acme.us/'))
903  ->withPageId(1100)
904  ->withLanguageId($languageId)
905  ->withInstructions([
906  $this->createHierarchicalMenuProcessorInstruction([
907  'levels' => 1,
908  'entryLevel' => 0,
909  'excludeUidList' => $excludedUidList,
910  'expandAll' => 1,
911  'includeSpacer' => 1,
912  'titleField' => 'title',
913  ]),
914  ])
915  );
916 
917  $json = json_decode((string)$response->getBody(), true);
918  self::assertSame($expectedMenuItems, count($json));
919  }
920 
921  public static function ‪directoryMenuIsGeneratedDataProvider(): array
922  {
923  return [
924  'ACME Inc First Level - Live' => [
925  'https://acme.us/',
926  1100,
927  1000,
928  0,
929  0,
930  [
931  [
932  'title' => 'EN: Welcome',
933  'link' => '/welcome',
934  ],
935  [
936  'title' => 'ZH-CN: Welcome Default',
937  // Symfony UrlGenerator, which is used for uri generation, rawurlencodes the url internally.
938  'link' => '/%E7%AE%80-bienvenue',
939  ],
940  [
941  'title' => 'EN: Features',
942  'link' => '/features',
943  ],
944  [
945  'title' => 'EN: Products',
946  'link' => 'https://products.acme.com/products',
947  ],
948  [
949  'title' => 'EN: ACME in your Region',
950  'link' => '/acme-in-your-region',
951  ],
952  [
953  'title' => 'Internal',
954  'link' => '/my-acme',
955  ],
956  [
957  'title' => 'About us',
958  'link' => '/about',
959  ],
960  [
961  'title' => 'Announcements & News',
962  'link' => '/news',
963  ],
964  [
965  'title' => 'That page is forbidden to you',
966  'link' => '/403',
967  ],
968  [
969  'title' => 'That page was not found',
970  'link' => '/404',
971  ],
972  [
973  'title' => 'Our Blog',
974  'link' => 'https://blog.acme.com/authors',
975  ],
976  [
977  'title' => 'Cross Site Shortcut',
978  'link' => 'https://blog.acme.com/authors',
979  ],
980  ],
981  ],
982  'ACME Inc First Level - Draft Workspace' => [
983  'https://acme.us/',
984  1100,
985  1000,
986  1,
987  1,
988  [
989  [
990  'title' => 'EN: Goodbye',
991  'link' => '/bye',
992  ],
993  [
994  'title' => 'EN: Welcome to ACME Inc',
995  'link' => '/welcome-modified',
996  ],
997  [
998  'title' => 'ZH-CN: Welcome Default',
999  // Symfony UrlGenerator, which is used for uri generation, rawurlencodes the url internally.
1000  'link' => '/%E7%AE%80-bienvenue',
1001  ],
1002  [
1003  'title' => 'EN: Features',
1004  'link' => '/features',
1005  ],
1006  [
1007  'title' => 'EN: Products',
1008  'link' => 'https://products.acme.com/products',
1009  ],
1010  [
1011  'title' => 'EN: ACME in your Region',
1012  'link' => '/acme-in-your-region',
1013  ],
1014  [
1015  'title' => 'Internal',
1016  'link' => '/my-acme',
1017  ],
1018  [
1019  'title' => 'About us',
1020  'link' => '/about',
1021  ],
1022  [
1023  'title' => 'Announcements & News',
1024  'link' => '/news',
1025  ],
1026  [
1027  'title' => 'That page is forbidden to you',
1028  'link' => '/403',
1029  ],
1030  [
1031  'title' => 'That page was not found',
1032  'link' => '/404',
1033  ],
1034  [
1035  'title' => 'Our Blog',
1036  'link' => 'https://blog.acme.com/authors',
1037  ],
1038  [
1039  'title' => 'Cross Site Shortcut',
1040  'link' => 'https://blog.acme.com/authors',
1041  ],
1042  ],
1043  ],
1044  ];
1045  }
1046 
1047  #[DataProvider('directoryMenuIsGeneratedDataProvider')]
1048  #[Test]
1049  public function ‪directoryMenuIsGenerated(string $hostPrefix, int $sourcePageId, int $directoryMenuParentPage, int $backendUserId, int $workspaceId, array $expectation): void
1050  {
1051  $response = $this->executeFrontendSubRequest(
1052  (new InternalRequest($hostPrefix))
1053  ->withPageId($sourcePageId)
1054  ->withInstructions([
1055  $this->createHierarchicalMenuProcessorInstruction([
1056  'special' => 'directory',
1057  'special.' => [
1058  'value' => $directoryMenuParentPage,
1059  ],
1060  'titleField' => 'title',
1061  ]),
1062  ]),
1063  (new InternalRequestContext())
1064  ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0)
1065  ->withBackendUserId($backendUserId)
1066  );
1067 
1068  $json = json_decode((string)$response->getBody(), true);
1069  $json = $this->filterMenu($json);
1070 
1071  self::assertSame($expectation, $json);
1072  }
1073 
1075  {
1076  return [
1077  'All restricted pages are linked to welcome page' => [
1078  'https://acme.us/',
1079  1100,
1080  1500,
1081  1100,
1082  0,
1083  0,
1084  [
1085  [
1086  'title' => 'Whitepapers',
1087  'link' => '/welcome',
1088  ],
1089  [
1090  'title' => 'Forecasts',
1091  'link' => '/welcome',
1092  ],
1093  [
1094  // Shortcut page, which resolves the shortcut and then the next page
1095  'title' => 'Employees',
1096  'link' => '/welcome',
1097  ],
1098  ],
1099  ],
1100  'Inherited restricted pages are linked' => [
1101  'https://acme.us/',
1102  1100,
1103  1520,
1104  1100,
1105  0,
1106  0,
1107  [
1108  [
1109  'title' => 'Current Year',
1110  // Should be
1111  // 'link' => '/welcome',
1112  // see https://forge.typo3.org/issues/16561
1113  'link' => '/my-acme/forecasts/current-year',
1114  ],
1115  [
1116  'title' => 'Next Year',
1117  // Should be
1118  // 'link' => '/welcome',
1119  // see https://forge.typo3.org/issues/16561
1120  'link' => '/my-acme/forecasts/next-year',
1121  ],
1122  [
1123  'title' => 'Five Years',
1124  // Should be
1125  // 'link' => '/welcome',
1126  // see https://forge.typo3.org/issues/16561
1127  'link' => '/my-acme/forecasts/five-years',
1128  ],
1129  ],
1130  ],
1131  ];
1132  }
1133 
1134  #[DataProvider('directoryMenuToAccessRestrictedPagesIsGeneratedDataProvider')]
1135  #[Test]
1136  public function ‪directoryMenuToAccessRestrictedPagesIsGenerated(string $hostPrefix, int $sourcePageId, int $directoryMenuParentPage, int $loginPageId, int $backendUserId, int $workspaceId, array $expectation): void
1137  {
1138  $response = $this->executeFrontendSubRequest(
1139  (new InternalRequest($hostPrefix))
1140  ->withPageId($sourcePageId)
1141  ->withInstructions([
1142  $this->createHierarchicalMenuProcessorInstruction([
1143  'special' => 'directory',
1144  'special.' => [
1145  'value' => $directoryMenuParentPage,
1146  ],
1147  'levels' => 1,
1148  'showAccessRestrictedPages' => $loginPageId,
1149  ]),
1150  ]),
1151  (new InternalRequestContext())
1152  ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0)
1153  ->withBackendUserId($backendUserId)
1154  );
1155 
1156  $json = json_decode((string)$response->getBody(), true);
1157  $json = $this->filterMenu($json);
1158 
1159  self::assertSame($expectation, $json);
1160  }
1161 
1162  public static function ‪listMenuIsGeneratedDataProvider(): array
1163  {
1164  return [
1165  'Live' => [
1166  'https://acme.us/',
1167  1100,
1168  [1600, 1100, 1700, 1800, 1520],
1169  0,
1170  0,
1171  [],
1172  [
1173  [
1174  'title' => 'About us',
1175  'link' => '/about',
1176  ],
1177  [
1178  'title' => 'EN: Welcome',
1179  'link' => '/welcome',
1180  ],
1181  [
1182  'title' => 'Announcements & News',
1183  'link' => '/news',
1184  ],
1185  ],
1186  ],
1187  'Workspaces' => [
1188  'https://acme.us/',
1189  1100,
1190  [1600, 1100, 1700, 1800, 1520],
1191  1,
1192  1,
1193  [],
1194  [
1195  [
1196  'title' => 'About us',
1197  'link' => '/about',
1198  ],
1199  [
1200  'title' => 'EN: Welcome to ACME Inc',
1201  'link' => '/welcome-modified',
1202  ],
1203  [
1204  'title' => 'Announcements & News',
1205  'link' => '/news',
1206  ],
1207  ],
1208  ],
1209  'Folder as base directory, needs to set excludeDoktypes in order to show the folder itself' => [
1210  'https://acme.us/',
1211  1100,
1212  [7000],
1213  0,
1214  0,
1215  [
1216  'levels' => 2,
1217  'expandAll' => 1,
1218  'excludeDoktypes' => ‪PageRepository::DOKTYPE_BE_USER_SECTION,
1219  ],
1220  [
1221  [
1222  'title' => 'Common Collection',
1223  // @todo Folders should not be linked in frontend menus, as they are not accessible there.
1224  // @todo Folder as rootpage - reconsider if this should be a valid use/test case, as marking
1225  // it as root_page is not possible if page is doktype sysfolder first.
1226  'link' => 'https://common.acme.com/common',
1227  'children' => [
1228  [
1229  'title' => 'Announcements & News',
1230  'link' => 'https://common.acme.com/common/news',
1231  ],
1232  ],
1233  ],
1234  ],
1235  ],
1236  'Non-Rootpage Sysfolder, needs to set excludeDoktypes in order to show the folder itself' => [
1237  'https://acme.us/',
1238  1100,
1239  [8000],
1240  0,
1241  0,
1242  [
1243  'levels' => 2,
1244  'expandAll' => 1,
1245  'excludeDoktypes' => ‪PageRepository::DOKTYPE_BE_USER_SECTION,
1246  ],
1247  [
1248  [
1249  'title' => 'Usual Collection Non-Root',
1250  'link' => 'https://usual.acme.com/usual',
1251  'children' => [
1252  [
1253  'title' => 'Announcements & News',
1254  // @todo Folders should not be linked in frontend menus, as they are not accessible there.
1255  'link' => 'https://usual.acme.com/usual/news-folder',
1256  ],
1257  ],
1258  ],
1259  ],
1260  ],
1261  ];
1262  }
1263 
1264  #[DataProvider('listMenuIsGeneratedDataProvider')]
1265  #[Test]
1266  public function ‪listMenuIsGenerated(string $hostPrefix, int $sourcePageId, array $menuPageIds, int $backendUserId, int $workspaceId, array $additionalMenuConfiguration, array $expectation): void
1267  {
1268  $response = $this->executeFrontendSubRequest(
1269  (new InternalRequest($hostPrefix))
1270  ->withPageId($sourcePageId)
1271  ->withInstructions([
1272  $this->createHierarchicalMenuProcessorInstruction(array_replace_recursive([
1273  'special' => 'list',
1274  'special.' => [
1275  'value' => implode(',', $menuPageIds),
1276  ],
1277  'titleField' => 'title',
1278  ], $additionalMenuConfiguration)),
1279  ]),
1280  (new InternalRequestContext())
1281  ->withWorkspaceId($backendUserId !== 0 ? $workspaceId : 0)
1282  ->withBackendUserId($backendUserId)
1283  );
1284 
1285  $json = json_decode((string)$response->getBody(), true);
1286  $json = $this->filterMenu($json);
1287 
1288  self::assertSame($expectation, $json);
1289  }
1290 
1291  public static function ‪languageMenuIsGeneratedDataProvider(): array
1292  {
1293  return [
1294  'ACME Inc (EN)' => [
1295  'https://acme.us/',
1296  1100,
1297  [
1298  ['title' => 'English', 'link' => '/welcome', 'active' => 1, 'current' => 0, 'available' => 1],
1299  ['title' => 'French', 'link' => 'https://acme.fr/bienvenue', 'active' => 0, 'current' => 0, 'available' => 1],
1300  ['title' => 'Franco-Canadian', 'link' => 'https://acme.ca/bienvenue', 'active' => 0, 'current' => 0, 'available' => 1],
1301  ],
1302  ],
1303  'ACME Inc (FR)' => [
1304  'https://acme.fr/',
1305  1100,
1306  [
1307  ['title' => 'English', 'link' => 'https://acme.us/welcome', 'active' => 0, 'current' => 0, 'available' => 1],
1308  ['title' => 'French', 'link' => '/bienvenue', 'active' => 1, 'current' => 0, 'available' => 1],
1309  ['title' => 'Franco-Canadian', 'link' => 'https://acme.ca/bienvenue', 'active' => 0, 'current' => 0, 'available' => 1],
1310  ],
1311  ],
1312  'ACME Inc (FR-CA)' => [
1313  'https://acme.ca/',
1314  1100,
1315  [
1316  ['title' => 'English', 'link' => 'https://acme.us/welcome', 'active' => 0, 'current' => 0, 'available' => 1],
1317  ['title' => 'French', 'link' => 'https://acme.fr/bienvenue', 'active' => 0, 'current' => 0, 'available' => 1],
1318  ['title' => 'Franco-Canadian', 'link' => '/bienvenue', 'active' => 1, 'current' => 0, 'available' => 1],
1319  ],
1320  ],
1321  'ACME Blog' => [
1322  'https://blog.acme.com/',
1323  2100,
1324  [
1325  ['title' => 'Default', 'link' => '/authors', 'active' => 1, 'current' => 0, 'available' => 1],
1326  ],
1327  ],
1328  'ACME Inc (EN) with a subpage' => [
1329  'https://acme.us/about',
1330  1600,
1331  [
1332  ['title' => 'English', 'link' => '/about', 'active' => 1, 'current' => 0, 'available' => 1],
1333  ['title' => 'French', 'link' => 'https://acme.fr/about', 'active' => 0, 'current' => 0, 'available' => 0],
1334  ['title' => 'Franco-Canadian', 'link' => 'https://acme.ca/about', 'active' => 0, 'current' => 0, 'available' => 0],
1335  ],
1336  ],
1337  ];
1338  }
1339 
1340  #[DataProvider('languageMenuIsGeneratedDataProvider')]
1341  #[Test]
1342  public function ‪languageMenuIsGenerated(string $hostPrefix, int $sourcePageId, array $expectation): void
1343  {
1344  $response = $this->executeFrontendSubRequest(
1345  (new InternalRequest($hostPrefix))
1346  ->withPageId($sourcePageId)
1347  ->withInstructions([
1348  $this->createLanguageMenuProcessorInstruction([
1349  'languages' => 'auto',
1350  ]),
1351  ])
1352  );
1353 
1354  $json = json_decode((string)$response->getBody(), true);
1355  $json = $this->filterMenu($json, ['title', 'link', 'available', 'active', 'current']);
1356 
1357  self::assertSame($expectation, $json);
1358  }
1359 }
‪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\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\Domain\Repository\PageRepository\DOKTYPE_BE_USER_SECTION
‪const DOKTYPE_BE_USER_SECTION
Definition: PageRepository.php:101
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling
Definition: AbstractTestCase.php:18
‪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\Domain\Repository\PageRepository
Definition: PageRepository.php:69