[FEATURE] Add additional configuration for external URLs
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Unit / Controller / TypoScriptFrontendControllerTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Unit\Controller;
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\Cache\Backend\NullBackend;
19 use TYPO3\CMS\Core\Cache\CacheManager;
20 use TYPO3\CMS\Core\Context\Context;
21 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
22 use TYPO3\CMS\Core\Http\ImmediateResponseException;
23 use TYPO3\CMS\Core\Http\ServerRequest;
24 use TYPO3\CMS\Core\Http\ServerRequestFactory;
25 use TYPO3\CMS\Core\Http\Uri;
26 use TYPO3\CMS\Core\Page\PageRenderer;
27 use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
28 use TYPO3\CMS\Core\Routing\PageArguments;
29 use TYPO3\CMS\Core\Site\Entity\Site;
30 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
33 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
34
35 /**
36 * Test case
37 */
38 class TypoScriptFrontendControllerTest extends UnitTestCase
39 {
40 /**
41 * @var bool Reset singletons created by subject
42 */
43 protected $resetSingletonInstances = true;
44
45 /**
46 * @var \PHPUnit\Framework\MockObject\MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface|TypoScriptFrontendController
47 */
48 protected $subject;
49
50 protected function setUp(): void
51 {
52 parent::setUp();
53 $this->subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
54 $this->subject->_set('context', new Context());
55 $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = '170928423746123078941623042360abceb12341234231';
56
57 $pageRepository = $this->getMockBuilder(PageRepository::class)->getMock();
58 $this->subject->sys_page = $pageRepository;
59
60 $pageRenderer = $this->getMockBuilder(PageRenderer::class)->getMock();
61 $this->subject->_set('pageRenderer', $pageRenderer);
62 }
63
64 /**
65 * Tests concerning rendering content
66 */
67
68 /**
69 * @test
70 */
71 public function headerAndFooterMarkersAreReplacedDuringIntProcessing()
72 {
73 $GLOBALS['TSFE'] = $this->setupTsfeMockForHeaderFooterReplacementCheck();
74 $GLOBALS['TSFE']->INTincScript();
75 self::assertStringContainsString('headerData', $GLOBALS['TSFE']->content);
76 self::assertStringContainsString('footerData', $GLOBALS['TSFE']->content);
77 }
78
79 /**
80 * This is the callback that mimics a USER_INT extension
81 */
82 public function INTincScript_processCallback()
83 {
84 $GLOBALS['TSFE']->additionalHeaderData[] = 'headerData';
85 $GLOBALS['TSFE']->additionalFooterData[] = 'footerData';
86 }
87
88 /**
89 * Setup a \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController object only for testing the header and footer
90 * replacement during USER_INT rendering
91 *
92 * @return \PHPUnit\Framework\MockObject\MockObject|TypoScriptFrontendController
93 */
94 protected function setupTsfeMockForHeaderFooterReplacementCheck()
95 {
96 /** @var \PHPUnit\Framework\MockObject\MockObject|TypoScriptFrontendController $tsfe */
97 $tsfe = $this->getMockBuilder(TypoScriptFrontendController::class)
98 ->setMethods([
99 'INTincScript_process',
100 'INTincScript_loadJSCode',
101 'setAbsRefPrefix',
102 'regeneratePageTitle'
103 ])->disableOriginalConstructor()
104 ->getMock();
105 $tsfe->expects(self::exactly(2))->method('processNonCacheableContentPartsAndSubstituteContentMarkers')->willReturnCallback([$this, 'INTincScript_processCallback']);
106 $tsfe->content = file_get_contents(__DIR__ . '/Fixtures/renderedPage.html');
107 $config = [
108 'INTincScript_ext' => [
109 'divKey' => '679b52796e75d474ccbbed486b6837ab',
110 ],
111 'INTincScript' => [
112 'INT_SCRIPT.679b52796e75d474ccbbed486b6837ab' => [],
113 ]
114 ];
115 $tsfe->config = $config;
116
117 return $tsfe;
118 }
119
120 /**
121 * Tests concerning sL
122 */
123
124 /**
125 * @test
126 */
127 public function localizationReturnsUnchangedStringIfNotLocallangLabel()
128 {
129 $string = $this->getUniqueId();
130 $this->subject->page = [];
131 $this->subject->_call('setOutputLanguage');
132 self::assertEquals($string, $this->subject->sL($string));
133 }
134
135 /**
136 * Tests concerning getSysDomainCache
137 */
138
139 /**
140 * @return array
141 */
142 public function getSysDomainCacheDataProvider()
143 {
144 return [
145 'typo3.org' => [
146 'typo3.org',
147 ],
148 'foo.bar' => [
149 'foo.bar',
150 ],
151 'example.com' => [
152 'example.com',
153 ],
154 ];
155 }
156
157 /**
158 * @return array
159 */
160 public function baseUrlWrapHandlesDifferentUrlsDataProvider()
161 {
162 return [
163 'without base url' => [
164 '',
165 'fileadmin/user_uploads/image.jpg',
166 'fileadmin/user_uploads/image.jpg'
167 ],
168 'with base url' => [
169 'http://www.google.com/',
170 'fileadmin/user_uploads/image.jpg',
171 'http://www.google.com/fileadmin/user_uploads/image.jpg'
172 ],
173 'without base url but with url prepended with a forward slash' => [
174 '',
175 '/fileadmin/user_uploads/image.jpg',
176 '/fileadmin/user_uploads/image.jpg',
177 ],
178 'with base url but with url prepended with a forward slash' => [
179 'http://www.google.com/',
180 '/fileadmin/user_uploads/image.jpg',
181 '/fileadmin/user_uploads/image.jpg',
182 ],
183 ];
184 }
185
186 /**
187 * @dataProvider baseUrlWrapHandlesDifferentUrlsDataProvider
188 * @test
189 * @param string $baseUrl
190 * @param string $url
191 * @param string $expected
192 */
193 public function baseUrlWrapHandlesDifferentUrls($baseUrl, $url, $expected)
194 {
195 $this->subject->baseUrl = $baseUrl;
196 self::assertSame($expected, $this->subject->baseUrlWrap($url));
197 }
198
199 /**
200 * @return array
201 */
202 public function initializeSearchWordDataBuildsCorrectRegexDataProvider()
203 {
204 return [
205 'one simple search word' => [
206 ['test'],
207 false,
208 'test',
209 ],
210 'one simple search word with standalone words' => [
211 ['test'],
212 true,
213 '[[:space:]]test[[:space:]]',
214 ],
215 'two simple search words' => [
216 ['test', 'test2'],
217 false,
218 'test|test2',
219 ],
220 'two simple search words with standalone words' => [
221 ['test', 'test2'],
222 true,
223 '[[:space:]]test[[:space:]]|[[:space:]]test2[[:space:]]',
224 ],
225 'word with regex chars' => [
226 ['A \\ word with / a bunch of [] regex () chars .*'],
227 false,
228 'A \\\\ word with \\/ a bunch of \\[\\] regex \\(\\) chars \\.\\*',
229 ],
230 ];
231 }
232
233 /**
234 * @test
235 * @dataProvider initializeSearchWordDataBuildsCorrectRegexDataProvider
236 *
237 * @param array $searchWordGetParameters The values that should be loaded in the sword_list GET parameter.
238 * @param bool $enableStandaloneSearchWords If TRUE the sword_standAlone option will be enabled.
239 * @param string $expectedRegex The expected regex after processing the search words.
240 */
241 public function initializeSearchWordDataBuildsCorrectRegex(array $searchWordGetParameters, $enableStandaloneSearchWords, $expectedRegex)
242 {
243 $_GET['sword_list'] = $searchWordGetParameters;
244 $_SERVER['HTTP_HOST'] = 'localhost';
245 $_SERVER['SCRIPT_NAME'] = '/index.php';
246
247 $this->subject->page = [];
248 if ($enableStandaloneSearchWords) {
249 $this->subject->config = ['config' => ['sword_standAlone' => 1]];
250 }
251
252 $request = ServerRequestFactory::fromGlobals();
253 $this->subject->preparePageContentGeneration($request);
254 self::assertEquals($this->subject->sWordRegEx, $expectedRegex);
255 }
256
257 /**
258 * @test
259 * @dataProvider splitLinkVarsDataProvider
260 *
261 * @param $string
262 * @param $expected
263 */
264 public function splitLinkVarsStringSplitsStringByComma($string, $expected)
265 {
266 self::assertEquals($expected, $this->subject->_callRef('splitLinkVarsString', $string));
267 }
268
269 /**
270 * @return array
271 */
272 public function splitLinkVarsDataProvider()
273 {
274 return [
275 [
276 'L',
277 ['L']
278 ],
279 [
280 'L,a',
281 [
282 'L',
283 'a'
284 ]
285 ],
286 [
287 'L, a',
288 [
289 'L',
290 'a'
291 ]
292 ],
293 [
294 'L , a',
295 [
296 'L',
297 'a'
298 ]
299 ],
300 [
301 ' L, a ',
302 [
303 'L',
304 'a'
305 ]
306 ],
307 [
308 'L(1)',
309 [
310 'L(1)'
311 ]
312 ],
313 [
314 'L(1),a',
315 [
316 'L(1)',
317 'a'
318 ]
319 ],
320 [
321 'L(1) , a',
322 [
323 'L(1)',
324 'a'
325 ]
326 ],
327 [
328 'a,L(1)',
329 [
330 'a',
331 'L(1)'
332 ]
333 ],
334 [
335 'L(1),a(2-3)',
336 [
337 'L(1)',
338 'a(2-3)'
339 ]
340 ],
341 [
342 'L(1),a((2-3))',
343 [
344 'L(1)',
345 'a((2-3))'
346 ]
347 ],
348 [
349 'L(1),a(a{2,4})',
350 [
351 'L(1)',
352 'a(a{2,4})'
353 ]
354 ],
355 [
356 'L(1),a(/a{2,4}\,()/)',
357 [
358 'L(1)',
359 'a(/a{2,4}\,()/)'
360 ]
361 ],
362 [
363 'L,a , b(c) , dd(/g{1,2}/), eee(, ()f) , 2',
364 [
365 'L',
366 'a',
367 'b(c)',
368 'dd(/g{1,2}/)',
369 'eee(, ()f)',
370 '2'
371 ]
372 ]
373 ];
374 }
375
376 /**
377 * @test
378 * @dataProvider calculateLinkVarsDataProvider
379 * @param string $linkVars
380 * @param array $getVars
381 * @param string $expected
382 */
383 public function calculateLinkVarsConsidersCorrectVariables(string $linkVars, array $getVars, string $expected)
384 {
385 $this->subject->config['config']['linkVars'] = $linkVars;
386 $this->subject->calculateLinkVars($getVars);
387 self::assertEquals($expected, $this->subject->linkVars);
388 }
389
390 public function calculateLinkVarsDataProvider(): array
391 {
392 return [
393 'simple variable' => [
394 'L',
395 [
396 'L' => 1
397 ],
398 '&L=1'
399 ],
400 'missing variable' => [
401 'L',
402 [
403 ],
404 ''
405 ],
406 'restricted variables' => [
407 'L(1-3),bar(3),foo(array),blub(array)',
408 [
409 'L' => 1,
410 'bar' => 2,
411 'foo' => [ 1, 2, 'f' => [ 4, 5 ] ],
412 'blub' => 123
413 ],
414 '&L=1&foo%5B0%5D=1&foo%5B1%5D=2&foo%5Bf%5D%5B0%5D=4&foo%5Bf%5D%5B1%5D=5'
415 ],
416 'nested variables' => [
417 'bar|foo(1-2)',
418 [
419 'bar' => [
420 'foo' => 1,
421 'unused' => 'never'
422 ]
423 ],
424 '&bar[foo]=1'
425 ],
426 ];
427 }
428
429 /**
430 * @test
431 */
432 public function initializeSearchWordDataDoesNothingWithNullValue()
433 {
434 $subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
435 $subject->_call('initializeSearchWordData', null);
436 self::assertEquals('', $subject->sWordRegEx);
437 self::assertEquals('', $subject->sWordList);
438 }
439
440 /**
441 * @test
442 */
443 public function initializeSearchWordDataDoesNothingWithEmptyStringValue()
444 {
445 $subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
446 $subject->_call('initializeSearchWordData', '');
447 self::assertEquals('', $subject->sWordRegEx);
448 self::assertEquals('', $subject->sWordList);
449 }
450
451 /**
452 * @test
453 */
454 public function initializeSearchWordDataDoesNothingWithEmptyArrayValue()
455 {
456 $subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
457 $subject->_call('initializeSearchWordData', []);
458 self::assertEquals('', $subject->sWordRegEx);
459 self::assertEquals([], $subject->sWordList);
460 }
461
462 /**
463 * @test
464 */
465 public function initializeSearchWordDataFillsProperRegexpWithArray()
466 {
467 $subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
468 $subject->_call('initializeSearchWordData', ['stop', 'word']);
469 self::assertEquals('stop|word', $subject->sWordRegEx);
470 self::assertEquals(['stop', 'word'], $subject->sWordList);
471 }
472
473 /**
474 * @test
475 */
476 public function initializeSearchWordDataFillsProperRegexpWithArrayAndStandaloneOption()
477 {
478 $subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
479 $subject->config['config']['sword_standAlone'] = 1;
480 $subject->_call('initializeSearchWordData', ['stop', 'word']);
481 self::assertEquals('[[:space:]]stop[[:space:]]|[[:space:]]word[[:space:]]', $subject->sWordRegEx);
482 self::assertEquals(['stop', 'word'], $subject->sWordList);
483 }
484
485 /**
486 * @test
487 * @see https://forge.typo3.org/issues/88041
488 */
489 public function indexedSearchHookUsesPageTitleApi(): void
490 {
491 $pageTitle = 'This is a test page title coming from PageTitleProviderManager';
492
493 $pageTitleProvider = $this->prophesize(PageTitleProviderManager::class);
494 $pageTitleProvider->getTitle()->willReturn($pageTitle);
495 GeneralUtility::setSingletonInstance(PageTitleProviderManager::class, $pageTitleProvider->reveal());
496
497 $nullCacheBackend = new NullBackend('');
498 $cacheManager = $this->prophesize(CacheManager::class);
499 $cacheManager->getCache('pages')->willReturn($nullCacheBackend);
500 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManager->reveal());
501
502 $this->subject->generatePageTitle();
503 self::assertSame($pageTitle, $this->subject->indexedDocTitle);
504 }
505
506 /**
507 * @test
508 */
509 public function pageRendererLanguageIsSetToSiteLanguageTypo3LanguageInConstructor(): void
510 {
511 $nullCacheBackend = new NullBackend('');
512 $cacheManager = $this->prophesize(CacheManager::class);
513 $cacheManager->getCache('pages')->willReturn($nullCacheBackend);
514 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManager->reveal());
515 $GLOBALS['TYPO3_REQUEST'] = new ServerRequest('https://www.example.com/');
516 $site = new Site('test', 13, ['base' => 'https://www.example.com/']);
517 $language = new SiteLanguage(0, 'fr', new Uri('/'), ['typo3Language' => 'fr-test']);
518 // Constructor calling initPageRenderer()
519 new TypoScriptFrontendController(
520 new Context(),
521 $site,
522 $language,
523 new PageArguments(13, '0', [])
524 );
525 // since PageRenderer is a singleton, this can be queried via the makeInstance call
526 self::assertEquals('fr-test', GeneralUtility::makeInstance(PageRenderer::class)->getLanguage());
527 }
528
529 /**
530 * @test
531 */
532 public function languageServiceIsSetUpWithSiteLanguageTypo3LanguageInConstructor(): void
533 {
534 $nullCacheBackend = new NullBackend('');
535 $cacheManager = $this->prophesize(CacheManager::class);
536 $cacheManager->getCache('pages')->willReturn($nullCacheBackend);
537 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManager->reveal());
538 $GLOBALS['TYPO3_REQUEST'] = new ServerRequest('https://www.example.com/');
539 $site = new Site('test', 13, ['base' => 'https://www.example.com/']);
540 $language = new SiteLanguage(0, 'fr', new Uri('/'), ['typo3Language' => 'fr']);
541 // Constructor calling setOutputLanguage()
542 $subject = $this->getAccessibleMock(
543 TypoScriptFrontendController::class,
544 ['dummy'],
545 [
546 new Context(),
547 $site,
548 $language,
549 new PageArguments(13, '0', [])
550 ]
551 );
552 $languageService = $subject->_get('languageService');
553 // since PageRenderer is a singleton, this can be queried via the makeInstance call
554 self::assertEquals('fr', $languageService->lang);
555 }
556
557 public function requireCacheHashValidateRelevantParametersDataProvider(): array
558 {
559 return [
560 'no extra params' => [
561 [],
562 false,
563 ],
564 'with required param' => [
565 [
566 'abc' => 1,
567 ],
568 true,
569 ],
570 'with required params' => [
571 [
572 'abc' => 1,
573 'abcd' => 1,
574 ],
575 true,
576 ],
577 'with not required param' => [
578 [
579 'fbclid' => 1,
580 ],
581 false,
582 ],
583 'with not required params' => [
584 [
585 'fbclid' => 1,
586 'gclid' => 1,
587 'foo' => [
588 'bar' => 1,
589 ],
590 ],
591 false,
592 ],
593 'with combined params' => [
594 [
595 'abc' => 1,
596 'fbclid' => 1,
597 ],
598 true,
599 ],
600 'with multiple combined params' => [
601 [
602 'abc' => 1,
603 'fbclid' => 1,
604 'abcd' => 1,
605 'gclid' => 1
606 ],
607 true,
608 ]
609 ];
610 }
611
612 /**
613 * @test
614 *
615 * @dataProvider requireCacheHashValidateRelevantParametersDataProvider
616 * @param array $remainingArguments
617 * @param bool $expected
618 */
619 public function requireCacheHashValidateRelevantParameters(array $remainingArguments, bool $expected): void
620 {
621 $GLOBALS['TYPO3_REQUEST'] = new ServerRequest();
622 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError'] = true;
623 $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'] = ['gclid', 'fbclid', 'foo[bar]'];
624
625 $this->subject = $this->getAccessibleMock(TypoScriptFrontendController::class, ['dummy'], [], '', false);
626 $this->subject->_set('pageArguments', new PageArguments(1, '0', ['tx_test' => 1], ['tx_test' => 1], $remainingArguments));
627
628 if ($expected) {
629 static::expectException(ImmediateResponseException::class);
630 }
631 $this->subject->reqCHash();
632 }
633 }