[BUGFIX] Resolve slashed values in PageTypeDecorator correctly
[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()
40 {
41 parent::setUpBeforeClass();
42 static::initializeDatabaseSnapshot();
43 }
44
45 public static function tearDownAfterClass()
46 {
47 static::destroyDatabaseSnapshot();
48 parent::tearDownAfterClass();
49 }
50
51 protected function setUp()
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()
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 * @return array
112 */
113 protected function createDataSet(
114 array $aspect,
115 array $languages,
116 array $enhancers,
117 string $variableName = 'value',
118 array $templateOptions = []
119 ): array {
120 $dataSet = [];
121 foreach ($enhancers as $enhancer) {
122 foreach ($languages as $languageId => $expectation) {
123 $dataSet[] = [
124 array_merge(
125 $enhancer['enhancer'],
126 ['aspects' => [$variableName => $aspect]]
127 ),
128 $enhancer['parameters'],
129 $languageId,
130 $expectation,
131 ];
132 }
133 }
134 $templatePrefix = isset($templateOptions['prefix']) ? $templateOptions['prefix'] : '';
135 $templateSuffix = isset($templateOptions['suffix']) ? $templateOptions['suffix'] : '';
136 return $this->keysFromTemplate(
137 $dataSet,
138 $templatePrefix . 'enhancer:%1$s, lang:%3$d' . $templateSuffix,
139 function (array $items) {
140 array_splice(
141 $items,
142 0,
143 1,
144 $items[0]['type']
145 );
146 return $items;
147 }
148 );
149 }
150
151 /**
152 * @param array $options
153 * @return array
154 */
155 protected function getEnhancers(array $options = []): array
156 {
157 $options = array_merge(
158 ['name' => 'enhance', 'value' => 100, 'additionalParameters' => ''],
159 $options
160 );
161 return [
162 [
163 'parameters' => sprintf('&value=%s%s', $options['value'], $options['additionalParameters']),
164 'enhancer' => [
165 'type' => 'Simple',
166 'routePath' => sprintf('/%s/{value}', $options['name']),
167 '_arguments' => [],
168 ],
169 ],
170 [
171 'parameters' => sprintf('&testing[value]=%s%s', $options['value'], $options['additionalParameters']),
172 'enhancer' => [
173 'type' => 'Plugin',
174 'routePath' => sprintf('/%s/{value}', $options['name']),
175 'namespace' => 'testing',
176 '_arguments' => [],
177 ],
178 ],
179 [
180 'parameters' => sprintf(
181 '&tx_testing_link[value]=%s&tx_testing_link[controller]=Link&tx_testing_link[action]=index%s',
182 $options['value'],
183 $options['additionalParameters']
184 ),
185 'enhancer' => [
186 'type' => 'Extbase',
187 'routes' => [
188 [
189 'routePath' => sprintf('/%s/{value}', $options['name']),
190 '_controller' => 'Link::index',
191 '_arguments' => [],
192 ],
193 ],
194 'extension' => 'testing',
195 'plugin' => 'link',
196 ],
197 ],
198 ];
199 }
200
201 /**
202 * @return array
203 */
204 protected function createPageTypeDecorator(): array
205 {
206 return [
207 'type' => 'PageType',
208 'default' => '.html',
209 'index' => 'index',
210 'map' => [
211 '.html' => 0,
212 'menu.json' => 10,
213 ]
214 ];
215 }
216
217 /**
218 * @param string|array|null $options
219 * @return array
220 */
221 public function localeModifierDataProvider($options = null): array
222 {
223 if (!is_array($options)) {
224 $options = [];
225 }
226 $aspect = [
227 'type' => 'LocaleModifier',
228 'default' => 'enhance',
229 'localeMap' => [
230 [
231 'locale' => 'fr_FR',
232 'value' => 'augmenter'
233 ]
234 ],
235 ];
236
237 $languages = [
238 '0' => sprintf('https://acme.us/welcome/enhance/100%s?cHash=', $options['pathSuffix'] ?? ''),
239 '1' => sprintf('https://acme.fr/bienvenue/augmenter/100%s?cHash=', $options['pathSuffix'] ?? ''),
240 ];
241
242 return $this->createDataSet(
243 $aspect,
244 $languages,
245 $this->getEnhancers([
246 'name' => '{enhance_name}',
247 'additionalParameters' => $options['additionalParameters'] ?? ''
248 ]),
249 'enhance_name',
250 ['prefix' => 'localeModifier/']
251 );
252 }
253
254 /**
255 * @param array $enhancer
256 * @param string $additionalParameters
257 * @param int $targetLanguageId
258 * @param string $expectation
259 *
260 * @test
261 * @dataProvider localeModifierDataProvider
262 */
263 public function localeModifierIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
264 {
265 $this->mergeSiteConfiguration('acme-com', [
266 'routeEnhancers' => ['Enhancer' => $enhancer]
267 ]);
268
269 $response = $this->executeFrontendRequest(
270 (new InternalRequest('https://acme.us/'))
271 ->withPageId(1100)
272 ->withInstructions([
273 $this->createTypoLinkUrlInstruction([
274 'parameter' => 1100,
275 'language' => $targetLanguageId,
276 'additionalParams' => $additionalParameters,
277 'forceAbsoluteUrl' => 1,
278 ])
279 ]),
280 $this->internalRequestContext
281 );
282
283 static::assertStringStartsWith($expectation, (string)$response->getBody());
284 }
285
286 /**
287 * @param string|array|null $options
288 * @return array
289 */
290 public function persistedAliasMapperDataProvider($options = null): array
291 {
292 if (!is_array($options)) {
293 $options = [];
294 }
295 $aspect = [
296 'type' => 'PersistedAliasMapper',
297 'tableName' => 'pages',
298 'routeFieldName' => 'slug',
299 'routeValuePrefix' => '/',
300 ];
301
302 $languages = [
303 '0' => sprintf('https://acme.us/welcome/enhance/welcome%s', $options['pathSuffix'] ?? ''),
304 '1' => sprintf('https://acme.fr/bienvenue/enhance/bienvenue%s', $options['pathSuffix'] ?? ''),
305 ];
306
307 return $this->createDataSet(
308 $aspect,
309 $languages,
310 $this->getEnhancers([
311 'value' => 1100,
312 'additionalParameters' => $options['additionalParameters'] ?? ''
313 ]),
314 'value',
315 ['prefix' => 'persistedAliasMapper/']
316 );
317 }
318
319 /**
320 * @param array $enhancer
321 * @param string $additionalParameters
322 * @param int $targetLanguageId
323 * @param string $expectation
324 *
325 * @test
326 * @dataProvider persistedAliasMapperDataProvider
327 */
328 public function persistedAliasMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
329 {
330 $this->mergeSiteConfiguration('acme-com', [
331 'routeEnhancers' => ['Enhancer' => $enhancer]
332 ]);
333
334 $response = $this->executeFrontendRequest(
335 (new InternalRequest('https://acme.us/'))
336 ->withPageId(1100)
337 ->withInstructions([
338 $this->createTypoLinkUrlInstruction([
339 'parameter' => 1100,
340 'language' => $targetLanguageId,
341 'additionalParams' => $additionalParameters,
342 'forceAbsoluteUrl' => 1,
343 ])
344 ]),
345 $this->internalRequestContext
346 );
347
348 static::assertSame($expectation, (string)$response->getBody());
349 }
350
351 /**
352 * @param string|array|null $options
353 * @return array
354 */
355 public function persistedPatternMapperDataProvider($options = null): array
356 {
357 if (!is_array($options)) {
358 $options = [];
359 }
360 $aspect = [
361 'type' => 'PersistedPatternMapper',
362 'tableName' => 'pages',
363 'routeFieldPattern' => '^(?P<subtitle>.+)-(?P<uid>\d+)$',
364 'routeFieldResult' => '{subtitle}-{uid}',
365 ];
366
367 $languages = [
368 '0' => sprintf('https://acme.us/welcome/enhance/hello-and-welcome-1100%s', $options['pathSuffix'] ?? ''),
369 '1' => sprintf('https://acme.fr/bienvenue/enhance/salut-et-bienvenue-1100%s', $options['pathSuffix'] ?? ''),
370 ];
371
372 return $this->createDataSet(
373 $aspect,
374 $languages,
375 $this->getEnhancers([
376 'value' => 1100,
377 'additionalParameters' => $options['additionalParameters'] ?? ''
378 ]),
379 'value',
380 ['prefix' => 'persistedPatternMapper/']
381 );
382 }
383
384 /**
385 * @param array $enhancer
386 * @param string $additionalParameters
387 * @param int $targetLanguageId
388 * @param string $expectation
389 *
390 * @test
391 * @dataProvider persistedPatternMapperDataProvider
392 */
393 public function persistedPatternMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
394 {
395 $this->mergeSiteConfiguration('acme-com', [
396 'routeEnhancers' => ['Enhancer' => $enhancer]
397 ]);
398
399 $response = $this->executeFrontendRequest(
400 (new InternalRequest('https://acme.us/'))
401 ->withPageId(1100)
402 ->withInstructions([
403 $this->createTypoLinkUrlInstruction([
404 'parameter' => 1100,
405 'language' => $targetLanguageId,
406 'additionalParams' => $additionalParameters,
407 'forceAbsoluteUrl' => 1,
408 ])
409 ]),
410 $this->internalRequestContext
411 );
412
413 static::assertSame($expectation, (string)$response->getBody());
414 }
415
416 /**
417 * @param string|array|null $options
418 * @return array
419 */
420 public function staticValueMapperDataProvider($options = null): array
421 {
422 if (!is_array($options)) {
423 $options = [];
424 }
425 $aspect = [
426 'type' => 'StaticValueMapper',
427 'map' => [
428 'hundred' => 100,
429 ],
430 'localeMap' => [
431 [
432 'locale' => 'fr_FR',
433 'map' => [
434 'cent' => 100,
435 ],
436 ]
437 ],
438 ];
439
440 $languages = [
441 '0' => sprintf('https://acme.us/welcome/enhance/hundred%s', $options['pathSuffix'] ?? ''),
442 '1' => sprintf('https://acme.fr/bienvenue/enhance/cent%s', $options['pathSuffix'] ?? ''),
443 ];
444
445 return $this->createDataSet(
446 $aspect,
447 $languages,
448 $this->getEnhancers([
449 'additionalParameters' => $options['additionalParameters'] ?? ''
450 ]),
451 'value',
452 ['prefix' => 'staticValueMapper/']
453 );
454 }
455
456 /**
457 * @param array $enhancer
458 * @param string $additionalParameters
459 * @param int $targetLanguageId
460 * @param string $expectation
461 *
462 * @test
463 * @dataProvider staticValueMapperDataProvider
464 */
465 public function staticValueMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
466 {
467 $this->mergeSiteConfiguration('acme-com', [
468 'routeEnhancers' => ['Enhancer' => $enhancer]
469 ]);
470
471 $response = $this->executeFrontendRequest(
472 (new InternalRequest('https://acme.us/'))
473 ->withPageId(1100)
474 ->withInstructions([
475 $this->createTypoLinkUrlInstruction([
476 'parameter' => 1100,
477 'language' => $targetLanguageId,
478 'additionalParams' => $additionalParameters,
479 'forceAbsoluteUrl' => 1,
480 ])
481 ]),
482 $this->internalRequestContext
483 );
484
485 static::assertStringStartsWith($expectation, (string)$response->getBody());
486 }
487
488 /**
489 * @param string|array|null $options
490 * @return array
491 */
492 public function staticRangeMapperDataProvider($options = null): array
493 {
494 if (!is_array($options)) {
495 $options = [];
496 }
497 $aspect = [
498 'type' => 'StaticRangeMapper',
499 'start' => '1',
500 'end' => '100',
501 ];
502
503 $dataSet = [];
504 foreach (range(10, 100, 30) as $value) {
505 $languages = [
506 '0' => sprintf('https://acme.us/welcome/enhance/%s%s', $value, $options['pathSuffix'] ?? ''),
507 '1' => sprintf('https://acme.fr/bienvenue/enhance/%s%s', $value, $options['pathSuffix'] ?? ''),
508 ];
509
510 $dataSet = array_merge(
511 $dataSet,
512 $this->createDataSet(
513 $aspect,
514 $languages,
515 $this->getEnhancers([
516 'value' => $value,
517 'additionalParameters' => $options['additionalParameters'] ?? ''
518 ]),
519 'value',
520 [
521 'prefix' => 'staticRangeMapper/',
522 'suffix' => sprintf(', value:%d', $value),
523 ]
524 )
525 );
526 }
527 return $dataSet;
528 }
529
530 /**
531 * @param array $enhancer
532 * @param string $additionalParameters
533 * @param int $targetLanguageId
534 * @param string $expectation
535 *
536 * @test
537 * @dataProvider staticRangeMapperDataProvider
538 */
539 public function staticRangeMapperIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
540 {
541 $this->mergeSiteConfiguration('acme-com', [
542 'routeEnhancers' => ['Enhancer' => $enhancer]
543 ]);
544
545 $response = $this->executeFrontendRequest(
546 (new InternalRequest('https://acme.us/'))
547 ->withPageId(1100)
548 ->withInstructions([
549 $this->createTypoLinkUrlInstruction([
550 'parameter' => 1100,
551 'language' => $targetLanguageId,
552 'additionalParams' => $additionalParameters,
553 'forceAbsoluteUrl' => 1,
554 ])
555 ]),
556 $this->internalRequestContext
557 );
558
559 static::assertStringStartsWith($expectation, (string)$response->getBody());
560 }
561
562 /**
563 * Combines all previous data providers for mappable aspects into one large
564 * data set that is permuted for several page type decorator instructions.
565 *
566 * @return array
567 */
568 public function pageTypeDecoratorIsAppliedDataProvider(): array
569 {
570 $instructions = [
571 ['pathSuffix' => '.html', 'type' => null],
572 ['pathSuffix' => '.html', 'type' => 0],
573 ['pathSuffix' => '/menu.json', 'type' => 10],
574 ];
575
576 $dataSet = [];
577 foreach ($instructions as $instruction) {
578 $templateSuffix = sprintf(
579 ' [%s=>%s]',
580 $instruction['pathSuffix'],
581 $instruction['type'] ?? 'null'
582 );
583 $dataProviderOptions = [
584 'pathSuffix' => $instruction['pathSuffix'],
585 'additionalParameters' => $instruction['type'] !== null
586 ? '&type=' . $instruction['type']
587 : ''
588 ];
589 $dataSetCandidates = array_merge(
590 $this->localeModifierDataProvider($dataProviderOptions),
591 $this->persistedAliasMapperDataProvider($dataProviderOptions),
592 $this->persistedPatternMapperDataProvider($dataProviderOptions),
593 $this->staticValueMapperDataProvider($dataProviderOptions),
594 $this->staticRangeMapperDataProvider($dataProviderOptions)
595 );
596 $dataSetCandidatesKeys = array_map(
597 function (string $dataSetCandidatesKey) use ($templateSuffix) {
598 return $dataSetCandidatesKey . $templateSuffix;
599 },
600 array_keys($dataSetCandidates)
601 );
602 $dataSet = array_merge(
603 $dataSet,
604 array_combine($dataSetCandidatesKeys, $dataSetCandidates)
605 );
606 }
607 return $dataSet;
608 }
609
610 /**
611 * @param array $enhancer
612 * @param string $additionalParameters
613 * @param int $targetLanguageId
614 * @param string $expectation
615 *
616 * @test
617 * @dataProvider pageTypeDecoratorIsAppliedDataProvider
618 */
619 public function pageTypeDecoratorIsApplied(array $enhancer, string $additionalParameters, int $targetLanguageId, string $expectation)
620 {
621 $this->mergeSiteConfiguration('acme-com', [
622 'routeEnhancers' => [
623 'Enhancer' => $enhancer,
624 'PageType' => $this->createPageTypeDecorator()
625 ]
626 ]);
627
628 $response = $this->executeFrontendRequest(
629 (new InternalRequest('https://acme.us/'))
630 ->withPageId(1100)
631 ->withInstructions([
632 $this->createTypoLinkUrlInstruction([
633 'parameter' => 1100,
634 'language' => $targetLanguageId,
635 'additionalParams' => $additionalParameters,
636 'forceAbsoluteUrl' => 1,
637 ])
638 ]),
639 $this->internalRequestContext
640 );
641
642 static::assertStringStartsWith($expectation, (string)$response->getBody());
643 }
644 }