e50a7032ca8041a8c54045b298d46b3966d8466c
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / SiteHandling / EnhancerLinkGeneratorTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Core\Bootstrap;
19 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
20 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
21 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
22 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
23
24 /**
25 * Test case for frontend requests having site handling configured using enhancers.
26 */
27 class EnhancerLinkGeneratorTest extends AbstractTestCase
28 {
29 /**
30 * @var string
31 */
32 private $siteTitle = 'A Company that Manufactures Everything Inc';
33
34 /**
35 * @var InternalRequestContext
36 */
37 private $internalRequestContext;
38
39 public static function setUpBeforeClass(): void
40 {
41 parent::setUpBeforeClass();
42 static::initializeDatabaseSnapshot();
43 }
44
45 public static function tearDownAfterClass(): void
46 {
47 static::destroyDatabaseSnapshot();
48 parent::tearDownAfterClass();
49 }
50
51 protected function setUp(): void
52 {
53 parent::setUp();
54
55 // these settings are forwarded to the frontend sub-request as well
56 $this->internalRequestContext = (new InternalRequestContext())
57 ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
58
59 $this->writeSiteConfiguration(
60 'acme-com',
61 $this->buildSiteConfiguration(1000, 'https://acme.com/'),
62 [
63 $this->buildDefaultLanguageConfiguration('EN', 'https://acme.us/'),
64 $this->buildLanguageConfiguration('FR', 'https://acme.fr/', ['EN']),
65 $this->buildLanguageConfiguration('FR-CA', 'https://acme.ca/', ['FR', 'EN']),
66 ]
67 );
68
69 $this->withDatabaseSnapshot(function () {
70 $this->setUpDatabase();
71 });
72 }
73
74 protected function setUpDatabase()
75 {
76 $backendUser = $this->setUpBackendUserFromFixture(1);
77 Bootstrap::initializeLanguageObject();
78
79 $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml';
80 $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
81 $writer = DataHandlerWriter::withBackendUser($backendUser);
82 $writer->invokeFactory($factory);
83 static::failIfArrayIsNotEmpty(
84 $writer->getErrors()
85 );
86
87 $this->setUpFrontendRootPage(
88 1000,
89 [
90 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript',
91 ],
92 [
93 'title' => 'ACME Root',
94 'sitetitle' => $this->siteTitle,
95 ]
96 );
97 }
98
99 protected function tearDown(): void
100 {
101 unset($this->internalRequestContext);
102 parent::tearDown();
103 }
104
105 /**
106 * @param array $aspect
107 * @param array $languages
108 * @param array $enhancers
109 * @param string $variableName
110 * @param array $templateOptions
111 * @param array $pageTypeSettings
112 * @return array
113 */
114 protected function createDataSet(
115 array $aspect,
116 array $languages,
117 array $enhancers,
118 string $variableName = 'value',
119 array $templateOptions = [],
120 array $pageTypeSettings
121 ): array {
122 $dataSet = [];
123 foreach ($enhancers as $enhancer) {
124 foreach ($languages as $languageId => $expectation) {
125 $dataSet[] = [
126 array_merge(
127 $enhancer['enhancer'],
128 ['aspects' => [$variableName => $aspect]]
129 ),
130 $enhancer['parameters'],
131 $languageId,
132 $expectation,
133 $pageTypeSettings,
134 ];
135 }
136 }
137 $templatePrefix = isset($templateOptions['prefix']) ? $templateOptions['prefix'] : '';
138 $templateSuffix = isset($templateOptions['suffix']) ? $templateOptions['suffix'] : '';
139 return $this->keysFromTemplate(
140 $dataSet,
141 $templatePrefix . 'enhancer:%1$s, lang:%3$d' . $templateSuffix,
142 function (array $items) {
143 array_splice(
144 $items,
145 0,
146 1,
147 $items[0]['type']
148 );
149 return $items;
150 }
151 );
152 }
153
154 /**
155 * @param array $options
156 * @return array
157 */
158 protected function getEnhancers(array $options = []): array
159 {
160 $options = array_merge(
161 ['name' => 'enhance', 'value' => 100, 'additionalParameters' => ''],
162 $options
163 );
164 return [
165 [
166 'parameters' => sprintf('&value=%s%s', $options['value'], $options['additionalParameters']),
167 'enhancer' => [
168 'type' => 'Simple',
169 'routePath' => sprintf('/%s/{value}', $options['name']),
170 '_arguments' => [],
171 ],
172 ],
173 [
174 'parameters' => sprintf('&testing[value]=%s%s', $options['value'], $options['additionalParameters']),
175 'enhancer' => [
176 'type' => 'Plugin',
177 'routePath' => sprintf('/%s/{value}', $options['name']),
178 'namespace' => 'testing',
179 '_arguments' => [],
180 ],
181 ],
182 [
183 'parameters' => sprintf(
184 '&tx_testing_link[value]=%s&tx_testing_link[controller]=Link&tx_testing_link[action]=index%s',
185 $options['value'],
186 $options['additionalParameters']
187 ),
188 'enhancer' => [
189 'type' => 'Extbase',
190 'routes' => [
191 [
192 'routePath' => sprintf('/%s/{value}', $options['name']),
193 '_controller' => 'Link::index',
194 '_arguments' => [],
195 ],
196 ],
197 'extension' => 'testing',
198 'plugin' => 'link',
199 ],
200 ],
201 ];
202 }
203
204 /**
205 * @return array
206 */
207 protected function createPageTypeDecorator(): array
208 {
209 return [
210 'type' => 'PageType',
211 'default' => '.html',
212 'index' => 'index',
213 'map' => [
214 '.html' => 0,
215 'menu.json' => 10,
216 ]
217 ];
218 }
219
220 /**
221 * @param string|array|null $options
222 * @return array
223 */
224 public function localeModifierDataProvider($options = null): array
225 {
226 if (!is_array($options)) {
227 $options = [];
228 }
229 $aspect = [
230 'type' => 'LocaleModifier',
231 'default' => 'enhance',
232 'localeMap' => [
233 [
234 'locale' => 'fr_FR',
235 'value' => 'augmenter'
236 ]
237 ],
238 ];
239
240 $languages = [
241 '0' => sprintf('https://acme.us/welcome/enhance/100%s?cHash=', $options['pathSuffix'] ?? ''),
242 '1' => sprintf('https://acme.fr/bienvenue/augmenter/100%s?cHash=', $options['pathSuffix'] ?? ''),
243 ];
244
245 return $this->createDataSet(
246 $aspect,
247 $languages,
248 $this->getEnhancers([
249 'name' => '{enhance_name}',
250 'additionalParameters' => $options['additionalParameters'] ?? ''
251 ]),
252 'enhance_name',
253 ['prefix' => 'localeModifier/'],
254 array_key_exists('pageTypeSettings', $options) ? $options['pageTypeSettings'] : []
255 );
256 }
257
258 /**
259 * @param array $enhancer
260 * @param string $additionalParameters
261 * @param int $targetLanguageId
262 * @param string $expectation
263 *
264 * @test
265 * @dataProvider localeModifierDataProvider
266 */
267 public function localeModifierIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
268 {
269 $this->mergeSiteConfiguration('acme-com', [
270 'routeEnhancers' => ['Enhancer' => $enhancer]
271 ]);
272
273 $response = $this->executeFrontendRequest(
274 (new InternalRequest('https://acme.us/'))
275 ->withPageId(1100)
276 ->withInstructions([
277 $this->createTypoLinkUrlInstruction([
278 'parameter' => 1100,
279 'language' => $targetLanguageId,
280 'additionalParams' => $additionalParameters,
281 'forceAbsoluteUrl' => 1,
282 ])
283 ]),
284 $this->internalRequestContext
285 );
286
287 static::assertStringStartsWith($expectation, (string)$response->getBody());
288 }
289
290 /**
291 * @param string|array|null $options
292 * @return array
293 */
294 public function persistedAliasMapperDataProvider($options = null): array
295 {
296 if (!is_array($options)) {
297 $options = [];
298 }
299 $aspect = [
300 'type' => 'PersistedAliasMapper',
301 'tableName' => 'pages',
302 'routeFieldName' => 'slug',
303 'routeValuePrefix' => '/',
304 ];
305
306 $languages = [
307 '0' => sprintf('https://acme.us/welcome/enhance/welcome%s', $options['pathSuffix'] ?? ''),
308 '1' => sprintf('https://acme.fr/bienvenue/enhance/bienvenue%s', $options['pathSuffix'] ?? ''),
309 ];
310
311 return $this->createDataSet(
312 $aspect,
313 $languages,
314 $this->getEnhancers([
315 'value' => 1100,
316 'additionalParameters' => $options['additionalParameters'] ?? ''
317 ]),
318 'value',
319 ['prefix' => 'persistedAliasMapper/'],
320 array_key_exists('pageTypeSettings', $options) ? $options['pageTypeSettings'] : []
321 );
322 }
323
324 /**
325 * @param array $enhancer
326 * @param string $additionalParameters
327 * @param int $targetLanguageId
328 * @param string $expectation
329 *
330 * @test
331 * @dataProvider persistedAliasMapperDataProvider
332 */
333 public function persistedAliasMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
334 {
335 $this->mergeSiteConfiguration('acme-com', [
336 'routeEnhancers' => ['Enhancer' => $enhancer]
337 ]);
338
339 $response = $this->executeFrontendRequest(
340 (new InternalRequest('https://acme.us/'))
341 ->withPageId(1100)
342 ->withInstructions([
343 $this->createTypoLinkUrlInstruction([
344 'parameter' => 1100,
345 'language' => $targetLanguageId,
346 'additionalParams' => $additionalParameters,
347 'forceAbsoluteUrl' => 1,
348 ])
349 ]),
350 $this->internalRequestContext
351 );
352
353 static::assertSame($expectation, (string)$response->getBody());
354 }
355
356 /**
357 * @param string|array|null $options
358 * @return array
359 */
360 public function persistedPatternMapperDataProvider($options = null): array
361 {
362 if (!is_array($options)) {
363 $options = [];
364 }
365 $aspect = [
366 'type' => 'PersistedPatternMapper',
367 'tableName' => 'pages',
368 'routeFieldPattern' => '^(?P<subtitle>.+)-(?P<uid>\d+)$',
369 'routeFieldResult' => '{subtitle}-{uid}',
370 ];
371
372 $languages = [
373 '0' => sprintf('https://acme.us/welcome/enhance/hello-and-welcome-1100%s', $options['pathSuffix'] ?? ''),
374 '1' => sprintf('https://acme.fr/bienvenue/enhance/salut-et-bienvenue-1100%s', $options['pathSuffix'] ?? ''),
375 ];
376
377 return $this->createDataSet(
378 $aspect,
379 $languages,
380 $this->getEnhancers([
381 'value' => 1100,
382 'additionalParameters' => $options['additionalParameters'] ?? ''
383 ]),
384 'value',
385 ['prefix' => 'persistedPatternMapper/'],
386 array_key_exists('pageTypeSettings', $options) ? $options['pageTypeSettings'] : []
387 );
388 }
389
390 /**
391 * @param array $enhancer
392 * @param string $additionalParameters
393 * @param int $targetLanguageId
394 * @param string $expectation
395 *
396 * @test
397 * @dataProvider persistedPatternMapperDataProvider
398 */
399 public function persistedPatternMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
400 {
401 $this->mergeSiteConfiguration('acme-com', [
402 'routeEnhancers' => ['Enhancer' => $enhancer]
403 ]);
404
405 $response = $this->executeFrontendRequest(
406 (new InternalRequest('https://acme.us/'))
407 ->withPageId(1100)
408 ->withInstructions([
409 $this->createTypoLinkUrlInstruction([
410 'parameter' => 1100,
411 'language' => $targetLanguageId,
412 'additionalParams' => $additionalParameters,
413 'forceAbsoluteUrl' => 1,
414 ])
415 ]),
416 $this->internalRequestContext
417 );
418
419 static::assertSame($expectation, (string)$response->getBody());
420 }
421
422 /**
423 * @param string|array|null $options
424 * @return array
425 */
426 public function staticValueMapperDataProvider($options = null): array
427 {
428 if (!is_array($options)) {
429 $options = [];
430 }
431 $aspect = [
432 'type' => 'StaticValueMapper',
433 'map' => [
434 'hundred' => 100,
435 ],
436 'localeMap' => [
437 [
438 'locale' => 'fr_FR',
439 'map' => [
440 'cent' => 100,
441 ],
442 ]
443 ],
444 ];
445
446 $languages = [
447 '0' => sprintf('https://acme.us/welcome/enhance/hundred%s', $options['pathSuffix'] ?? ''),
448 '1' => sprintf('https://acme.fr/bienvenue/enhance/cent%s', $options['pathSuffix'] ?? ''),
449 ];
450
451 return $this->createDataSet(
452 $aspect,
453 $languages,
454 $this->getEnhancers([
455 'additionalParameters' => $options['additionalParameters'] ?? ''
456 ]),
457 'value',
458 ['prefix' => 'staticValueMapper/'],
459 array_key_exists('pageTypeSettings', $options) ? $options['pageTypeSettings'] : []
460 );
461 }
462
463 /**
464 * @param array $enhancer
465 * @param string $additionalParameters
466 * @param int $targetLanguageId
467 * @param string $expectation
468 *
469 * @test
470 * @dataProvider staticValueMapperDataProvider
471 */
472 public function staticValueMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
473 {
474 $this->mergeSiteConfiguration('acme-com', [
475 'routeEnhancers' => ['Enhancer' => $enhancer]
476 ]);
477
478 $response = $this->executeFrontendRequest(
479 (new InternalRequest('https://acme.us/'))
480 ->withPageId(1100)
481 ->withInstructions([
482 $this->createTypoLinkUrlInstruction([
483 'parameter' => 1100,
484 'language' => $targetLanguageId,
485 'additionalParams' => $additionalParameters,
486 'forceAbsoluteUrl' => 1,
487 ])
488 ]),
489 $this->internalRequestContext
490 );
491
492 static::assertStringStartsWith($expectation, (string)$response->getBody());
493 }
494
495 /**
496 * @param string|array|null $options
497 * @return array
498 */
499 public function staticRangeMapperDataProvider($options = null): array
500 {
501 if (!is_array($options)) {
502 $options = [];
503 }
504 $aspect = [
505 'type' => 'StaticRangeMapper',
506 'start' => '1',
507 'end' => '100',
508 ];
509
510 $dataSet = [[]];
511 foreach (range(10, 100, 30) as $value) {
512 $languages = [
513 '0' => sprintf('https://acme.us/welcome/enhance/%s%s', $value, $options['pathSuffix'] ?? ''),
514 '1' => sprintf('https://acme.fr/bienvenue/enhance/%s%s', $value, $options['pathSuffix'] ?? ''),
515 ];
516
517 $dataSet[] = $this->createDataSet(
518 $aspect,
519 $languages,
520 $this->getEnhancers([
521 'value' => $value,
522 'additionalParameters' => $options['additionalParameters'] ?? ''
523 ]),
524 'value',
525 [
526 'prefix' => 'staticRangeMapper/',
527 'suffix' => sprintf(', value:%d', $value),
528 ],
529 array_key_exists('pageTypeSettings', $options) ? $options['pageTypeSettings'] : []
530 );
531 }
532 return array_merge(...$dataSet);
533 }
534
535 /**
536 * @param array $enhancer
537 * @param string $additionalParameters
538 * @param int $targetLanguageId
539 * @param string $expectation
540 *
541 * @test
542 * @dataProvider staticRangeMapperDataProvider
543 */
544 public function staticRangeMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
545 {
546 $this->mergeSiteConfiguration('acme-com', [
547 'routeEnhancers' => ['Enhancer' => $enhancer]
548 ]);
549
550 $response = $this->executeFrontendRequest(
551 (new InternalRequest('https://acme.us/'))
552 ->withPageId(1100)
553 ->withInstructions([
554 $this->createTypoLinkUrlInstruction([
555 'parameter' => 1100,
556 'language' => $targetLanguageId,
557 'additionalParams' => $additionalParameters,
558 'forceAbsoluteUrl' => 1,
559 ])
560 ]),
561 $this->internalRequestContext
562 );
563
564 static::assertStringStartsWith($expectation, (string)$response->getBody());
565 }
566
567 /**
568 * Combines all previous data providers for mappable aspects into one large
569 * data set that is permuted for several page type decorator instructions.
570 *
571 * @return array
572 */
573 public function pageTypeDecoratorIsAppliedDataProvider(): array
574 {
575 $instructions = [
576 [
577 'pathSuffix' => '.html',
578 'type' => null,
579 'pageTypeSettings' => $this->createPageTypeDecorator()
580 ],
581 [
582 'pathSuffix' => '.html',
583 'type' => 0,
584 'pageTypeSettings' => $this->createPageTypeDecorator()
585 ],
586 [
587 'pathSuffix' => '/menu.json',
588 'type' => 10,
589 'pageTypeSettings' => $this->createPageTypeDecorator()
590 ],
591 [
592 'pathSuffix' => '/',
593 'type' => null,
594 'pageTypeSettings' => [
595 'type' => 'PageType',
596 'default' => '/',
597 'index' => '/',
598 'map' => [
599 'menu.json' => 10,
600 ]
601 ]
602 ],
603 [
604 'pathSuffix' => '/',
605 'type' => 0,
606 'pageTypeSettings' => [
607 'type' => 'PageType',
608 'default' => '/',
609 'index' => '/',
610 'map' => [
611 'menu.json' => 10,
612 ]
613 ]
614 ]
615 ];
616
617 $dataSet = [[]];
618 foreach ($instructions as $instruction) {
619 $templateSuffix = sprintf(
620 ' [%s=>%s]',
621 $instruction['pathSuffix'],
622 $instruction['type'] ?? 'null'
623 );
624 $dataProviderOptions = [
625 'pathSuffix' => $instruction['pathSuffix'],
626 'additionalParameters' => $instruction['type'] !== null
627 ? '&type=' . $instruction['type']
628 : '',
629 'pageTypeSettings' => $instruction['pageTypeSettings']
630 ];
631 $dataSetCandidates = array_merge(
632 $this->localeModifierDataProvider($dataProviderOptions),
633 $this->persistedAliasMapperDataProvider($dataProviderOptions),
634 $this->persistedPatternMapperDataProvider($dataProviderOptions),
635 $this->staticValueMapperDataProvider($dataProviderOptions),
636 $this->staticRangeMapperDataProvider($dataProviderOptions)
637 );
638 $dataSetCandidatesKeys = array_map(
639 function (string $dataSetCandidatesKey) use ($templateSuffix) {
640 return $dataSetCandidatesKey . $templateSuffix;
641 },
642 array_keys($dataSetCandidates)
643 );
644 $dataSet[] = array_combine($dataSetCandidatesKeys, $dataSetCandidates);
645 }
646 return array_merge(...$dataSet);
647 }
648
649 /**
650 * @param array $enhancer
651 * @param string $additionalParameters
652 * @param int $targetLanguageId
653 * @param string $expectation
654 * @param array $pageTypeSettings
655 *
656 * @test
657 * @dataProvider pageTypeDecoratorIsAppliedDataProvider
658 */
659 public function pageTypeDecoratorIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation, array $pageTypeSettings)
660 {
661 if (empty($pageTypeSettings)) {
662 $pageTypeSettings = $this->createPageTypeDecorator();
663 }
664
665 $this->mergeSiteConfiguration('acme-com', [
666 'routeEnhancers' => [
667 'Enhancer' => $enhancer,
668 'PageType' => $pageTypeSettings
669 ]
670 ]);
671
672 $response = $this->executeFrontendRequest(
673 (new InternalRequest('https://acme.us/'))
674 ->withPageId(1100)
675 ->withInstructions([
676 $this->createTypoLinkUrlInstruction([
677 'parameter' => 1100,
678 'language' => $targetLanguageId,
679 'additionalParams' => $additionalParameters,
680 'forceAbsoluteUrl' => 1,
681 ])
682 ]),
683 $this->internalRequestContext
684 );
685
686 static::assertStringStartsWith($expectation, (string)$response->getBody());
687 }
688 }