[FEATURE] Allow to override htmlTag attributes
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Unit / Http / RequestHandlerTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Unit\Http;
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 Prophecy\Argument;
19 use TYPO3\CMS\Core\Http\ServerRequestFactory;
20 use TYPO3\CMS\Core\Page\PageRenderer;
21 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
22 use TYPO3\CMS\Core\TypoScript\TemplateService;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
25 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
26 use TYPO3\CMS\Frontend\Http\RequestHandler;
27 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
28
29 /**
30 * Test case
31 */
32 class RequestHandlerTest extends UnitTestCase
33 {
34 public function generateHtmlTagIncludesAllPossibilitiesDataProvider()
35 {
36 return [
37 'no original values' => [
38 [],
39 [],
40 '<html>'
41 ],
42 'no additional values' => [
43 ['dir' => 'left'],
44 [],
45 '<html dir="left">'
46 ],
47 'no additional values #2' => [
48 ['dir' => 'left', 'xmlns:dir' => 'left'],
49 [],
50 '<html dir="left" xmlns:dir="left">'
51 ],
52 'disable all attributes' => [
53 ['dir' => 'left', 'xmlns:dir' => 'left'],
54 ['htmlTag_setParams' => 'none'],
55 '<html>'
56 ],
57 'only add setParams' => [
58 ['dir' => 'left', 'xmlns:dir' => 'left'],
59 ['htmlTag_setParams' => 'amp'],
60 '<html amp>'
61 ],
62 'attributes property trumps htmlTag_setParams' => [
63 ['dir' => 'left', 'xmlns:dir' => 'left'],
64 ['htmlTag.' => ['attributes.' => ['amp' => '']], 'htmlTag_setParams' => 'none'],
65 '<html dir="left" xmlns:dir="left" amp>'
66 ],
67 'attributes property with mixed values' => [
68 ['dir' => 'left', 'xmlns:dir' => 'left'],
69 ['htmlTag.' => ['attributes.' => ['amp' => '', 'no-js' => 'true', 'additional-enabled' => 0]]],
70 '<html dir="left" xmlns:dir="left" amp no-js="true" additional-enabled="0">'
71 ],
72 'attributes property overrides default settings' => [
73 ['dir' => 'left'],
74 ['htmlTag.' => ['attributes.' => ['amp' => '', 'dir' => 'right']]],
75 '<html amp dir="right">'
76 ],
77 ];
78 }
79
80 /**
81 * Does not test stdWrap functionality.
82 *
83 * @param $htmlTagAttributes
84 * @param $configuration
85 * @param $expectedResult
86 * @test
87 * @dataProvider generateHtmlTagIncludesAllPossibilitiesDataProvider
88 */
89 public function generateHtmlTagIncludesAllPossibilities($htmlTagAttributes, $configuration, $expectedResult)
90 {
91 $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
92 $cObj = $this->prophesize(ContentObjectRenderer::class);
93 $cObj->stdWrap(Argument::cetera())->shouldNotBeCalled();
94 $result = $subject->_call('generateHtmlTag', $htmlTagAttributes, $configuration, $cObj->reveal());
95
96 $this->assertEquals($expectedResult, $result);
97 }
98
99 /**
100 * @return array
101 */
102 public function generateMetaTagHtmlGeneratesCorrectTagsDataProvider()
103 {
104 return [
105 'simple meta' => [
106 [
107 'author' => 'Markus Klein',
108 ],
109 '',
110 [
111 'type' => 'name',
112 'name' => 'author',
113 'content' => 'Markus Klein'
114 ]
115 ],
116 'httpEquivalent meta' => [
117 [
118 'X-UA-Compatible' => 'IE=edge,chrome=1',
119 'X-UA-Compatible.' => ['httpEquivalent' => 1]
120 ],
121 'IE=edge,chrome=1',
122 [
123 'type' => 'http-equiv',
124 'name' => 'X-UA-Compatible',
125 'content' => 'IE=edge,chrome=1'
126 ]
127 ],
128 'httpEquivalent meta xhtml new notation' => [
129 [
130 'X-UA-Compatible' => 'IE=edge,chrome=1',
131 'X-UA-Compatible.' => ['attribute' => 'http-equiv']
132 ],
133 'IE=edge,chrome=1',
134 [
135 'type' => 'http-equiv',
136 'name' => 'X-UA-Compatible',
137 'content' => 'IE=edge,chrome=1'
138 ]
139 ],
140 'refresh meta' => [
141 [
142 'refresh' => '10',
143 ],
144 '',
145 [
146 'type' => 'http-equiv',
147 'name' => 'refresh',
148 'content' => '10'
149 ]
150 ],
151 'refresh meta new notation' => [
152 [
153 'refresh' => '10',
154 'refresh.' => ['attribute' => 'http-equiv']
155 ],
156 '10',
157 [
158 'type' => 'http-equiv',
159 'name' => 'refresh',
160 'content' => '10'
161 ]
162 ],
163 'meta with dot' => [
164 [
165 'DC.author' => 'Markus Klein',
166 ],
167 '',
168 [
169 'type' => 'name',
170 'name' => 'DC.author',
171 'content' => 'Markus Klein'
172 ]
173 ],
174 'meta with colon' => [
175 [
176 'OG:title' => 'Magic Tests',
177 ],
178 '',
179 [
180 'type' => 'name',
181 'name' => 'OG:title',
182 'content' => 'Magic Tests'
183 ]
184 ],
185 'different attribute name' => [
186 [
187 'og:site_title' => 'My TYPO3 site',
188 'og:site_title.' => ['attribute' => 'property'],
189 ],
190 'My TYPO3 site',
191 [
192 'type' => 'property',
193 'name' => 'og:site_title',
194 'content' => 'My TYPO3 site'
195 ]
196 ],
197 'meta with 0 value' => [
198 [
199 'custom:key' => '0',
200 ],
201 '',
202 [
203 'type' => 'name',
204 'name' => 'custom:key',
205 'content' => '0'
206 ]
207 ],
208 ];
209 }
210
211 /**
212 * @test
213 */
214 public function generateMetaTagExpectExceptionOnBogusTags()
215 {
216 $stdWrapResult = '10';
217
218 $typoScript = [
219 'refresh' => '10',
220 'refresh.' => ['attribute' => 'http-equiv-new']
221 ];
222
223 $expectedTags = [
224 'type' => 'http-equiv-new',
225 'name' => 'refresh',
226 'content' => '10'
227 ];
228
229 $cObj = $this->prophesize(ContentObjectRenderer::class);
230 $cObj->cObjGet(Argument::cetera())->shouldBeCalled();
231 $cObj->stdWrap(Argument::cetera())->willReturn($stdWrapResult);
232 $tmpl = $this->prophesize(TemplateService::class);
233 $tsfe = $this->prophesize(TypoScriptFrontendController::class);
234 $tsfe->generatePageTitle()->willReturn('');
235 $tsfe->INTincScript_loadJSCode()->shouldBeCalled();
236 $tsfe->cObj = $cObj->reveal();
237 $tsfe->tmpl = $tmpl->reveal();
238 $tsfe->page = [
239 'title' => ''
240 ];
241 $tsfe->pSetup = [
242 'meta.' => $typoScript
243 ];
244
245 $pageRendererProphecy = $this->prophesize(PageRenderer::class);
246 $subject = $this->getAccessibleMock(RequestHandler::class, ['getPageRenderer'], [], '', false);
247 $subject->expects($this->any())->method('getPageRenderer')->willReturn($pageRendererProphecy->reveal());
248 $subject->_set('timeTracker', new TimeTracker(false));
249 $subject->_call('generatePageContentWithHeader', $tsfe->reveal(), null);
250
251 $pageRendererProphecy->setMetaTag($expectedTags['type'], $expectedTags['name'], $expectedTags['content'])->willThrow(\InvalidArgumentException::class);
252 }
253
254 /**
255 * @test
256 * @dataProvider generateMetaTagHtmlGeneratesCorrectTagsDataProvider
257 *
258 * @param array $typoScript
259 * @param string $stdWrapResult
260 * @param array $expectedTags
261 */
262 public function generateMetaTagHtmlGeneratesCorrectTags(array $typoScript, string $stdWrapResult, array $expectedTags)
263 {
264 $cObj = $this->prophesize(ContentObjectRenderer::class);
265 $cObj->cObjGet(Argument::cetera())->shouldBeCalled();
266 $cObj->stdWrap(Argument::cetera())->willReturn($stdWrapResult);
267 $tmpl = $this->prophesize(TemplateService::class);
268 $tsfe = $this->prophesize(TypoScriptFrontendController::class);
269 $tsfe->generatePageTitle()->willReturn('');
270 $tsfe->INTincScript_loadJSCode()->shouldBeCalled();
271 $tsfe->cObj = $cObj->reveal();
272 $tsfe->tmpl = $tmpl->reveal();
273 $tsfe->config = [
274 'config' => [],
275 ];
276 $tsfe->page = [
277 'title' => ''
278 ];
279 $tsfe->pSetup = [
280 'meta.' => $typoScript
281 ];
282 $pageRendererProphecy = $this->prophesize(PageRenderer::class);
283 $subject = $this->getAccessibleMock(RequestHandler::class, ['getPageRenderer'], [], '', false);
284 $subject->expects($this->any())->method('getPageRenderer')->willReturn($pageRendererProphecy->reveal());
285 $subject->_set('timeTracker', new TimeTracker(false));
286 $subject->_call('generatePageContentWithHeader', $tsfe->reveal(), null);
287
288 $pageRendererProphecy->setMetaTag($expectedTags['type'], $expectedTags['name'], $expectedTags['content'], [], false)->shouldHaveBeenCalled();
289 }
290
291 /**
292 * @test
293 */
294 public function generateMetaTagHtmlGenerateNoTagWithEmptyContent()
295 {
296 $stdWrapResult = '';
297
298 $typoScript = [
299 'custom:key' => '',
300 ];
301
302 $cObj = $this->prophesize(ContentObjectRenderer::class);
303 $cObj->cObjGet(Argument::cetera())->shouldBeCalled();
304 $cObj->stdWrap(Argument::cetera())->willReturn($stdWrapResult);
305 $tmpl = $this->prophesize(TemplateService::class);
306 $tsfe = $this->prophesize(TypoScriptFrontendController::class);
307 $tsfe->generatePageTitle()->willReturn('');
308 $tsfe->INTincScript_loadJSCode()->shouldBeCalled();
309 $tsfe->cObj = $cObj->reveal();
310 $tsfe->tmpl = $tmpl->reveal();
311 $tsfe->config = [
312 'config' => [],
313 ];
314 $tsfe->page = [
315 'title' => ''
316 ];
317 $tsfe->pSetup = [
318 'meta.' => $typoScript
319 ];
320
321 $pageRendererProphecy = $this->prophesize(PageRenderer::class);
322 $subject = $this->getAccessibleMock(RequestHandler::class, ['getPageRenderer'], [], '', false);
323 $subject->expects($this->any())->method('getPageRenderer')->willReturn($pageRendererProphecy->reveal());
324 $subject->_set('timeTracker', new TimeTracker(false));
325 $subject->_call('generatePageContentWithHeader', $tsfe->reveal(), null);
326
327 $pageRendererProphecy->setMetaTag(null, null, null)->shouldNotBeCalled();
328 }
329
330 public function generateMultipleMetaTagsDataProvider()
331 {
332 return [
333 'multi value attribute name' => [
334 [
335 'og:locale:alternate.' => [
336 'attribute' => 'property',
337 'value' => [
338 10 => 'nl_NL',
339 20 => 'de_DE',
340 ]
341 ],
342 ],
343 '',
344 [
345 [
346 'type' => 'property',
347 'name' => 'og:locale:alternate',
348 'content' => 'nl_NL'
349 ],
350 [
351 'type' => 'property',
352 'name' => 'og:locale:alternate',
353 'content' => 'de_DE'
354 ]
355 ]
356 ],
357 'multi value attribute name (empty values are skipped)' => [
358 [
359 'og:locale:alternate.' => [
360 'attribute' => 'property',
361 'value' => [
362 10 => 'nl_NL',
363 20 => '',
364 30 => 'de_DE',
365 ]
366 ],
367 ],
368 '',
369 [
370 [
371 'type' => 'property',
372 'name' => 'og:locale:alternate',
373 'content' => 'nl_NL'
374 ],
375 [
376 'type' => 'property',
377 'name' => 'og:locale:alternate',
378 'content' => 'de_DE'
379 ]
380 ],
381 ],
382 ];
383 }
384
385 /**
386 * @test
387 * @dataProvider generateMultipleMetaTagsDataProvider
388 *
389 * @param array $typoScript
390 * @param string $stdWrapResult
391 * @param array $expectedTags
392 */
393 public function generateMultipleMetaTags(array $typoScript, string $stdWrapResult, array $expectedTags)
394 {
395 $cObj = $this->prophesize(ContentObjectRenderer::class);
396 $cObj->cObjGet(Argument::cetera())->shouldBeCalled();
397 $cObj->stdWrap(Argument::cetera())->willReturn($stdWrapResult);
398 $tmpl = $this->prophesize(TemplateService::class);
399 $tsfe = $this->prophesize(TypoScriptFrontendController::class);
400 $tsfe->generatePageTitle()->willReturn('');
401 $tsfe->INTincScript_loadJSCode()->shouldBeCalled();
402 $tsfe->cObj = $cObj->reveal();
403 $tsfe->tmpl = $tmpl->reveal();
404 $tsfe->config = [
405 'config' => [],
406 ];
407 $tsfe->page = [
408 'title' => ''
409 ];
410 $tsfe->pSetup = [
411 'meta.' => $typoScript
412 ];
413 $pageRendererProphecy = $this->prophesize(PageRenderer::class);
414 $subject = $this->getAccessibleMock(RequestHandler::class, ['getPageRenderer'], [], '', false);
415 $subject->expects($this->any())->method('getPageRenderer')->willReturn($pageRendererProphecy->reveal());
416 $subject->_set('timeTracker', new TimeTracker(false));
417 $subject->_call('generatePageContentWithHeader', $tsfe->reveal(), null);
418
419 $pageRendererProphecy->setMetaTag($expectedTags[0]['type'], $expectedTags[0]['name'], $expectedTags[0]['content'], [], false)->shouldHaveBeenCalled();
420 $pageRendererProphecy->setMetaTag($expectedTags[1]['type'], $expectedTags[1]['name'], $expectedTags[1]['content'], [], false)->shouldHaveBeenCalled();
421 }
422
423 /**
424 * Test if the method is called, and the object is still the same.
425 *
426 * @test
427 */
428 public function addModifiedGlobalsToIncomingRequestFindsSameObject()
429 {
430 GeneralUtility::flushInternalRuntimeCaches();
431 $_SERVER['REQUEST_METHOD'] = 'POST';
432 $_SERVER['HTTP_HOST'] = 'https://www.example.com/my/path/';
433 $_GET = ['foo' => '1'];
434 $_POST = ['bar' => 'yo'];
435 $request = ServerRequestFactory::fromGlobals();
436 $request = $request->withAttribute('_originalGetParameters', $_GET);
437 $request = $request->withAttribute('_originalPostParameters', $_POST);
438
439 $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
440 $resultRequest = $subject->_call('addModifiedGlobalsToIncomingRequest', $request);
441 $this->assertSame($request, $resultRequest);
442 }
443
444 /**
445 * @return array
446 */
447 public function addModifiedGlobalsToIncomingRequestDataProvider()
448 {
449 return [
450 'No parameters have been modified via hook or middleware' => [
451 ['batman' => '1'],
452 ['no_cache' => 1],
453 // Enriched within PSR-7 query params + parsed body
454 [],
455 [],
456 // modified GET / POST parameters
457 [],
458 [],
459 // expected merged results
460 ['batman' => '1'],
461 ['no_cache' => 1],
462 ],
463 'No parameters have been modified via hook' => [
464 ['batman' => '1'],
465 [],
466 // Enriched within PSR-7 query params + parsed body
467 ['ARD' => 'TV', 'Oscars' => 'Cinema'],
468 ['no_cache' => '1'],
469 // modified GET / POST parameters
470 [],
471 [],
472 // expected merged results
473 ['batman' => '1', 'ARD' => 'TV', 'Oscars' => 'Cinema'],
474 ['no_cache' => 1],
475 ],
476 'Hooks and Middlewares modified' => [
477 ['batman' => '1'],
478 [],
479 // Enriched within PSR-7 query params + parsed body
480 ['ARD' => 'TV', 'Oscars' => 'Cinema'],
481 ['no_cache' => '1'],
482 // modified GET / POST parameters
483 ['batman' => '1', 'add_via_hook' => 'yes'],
484 ['submitForm' => 'download now'],
485 // expected merged results
486 ['batman' => '1', 'add_via_hook' => 'yes', 'ARD' => 'TV', 'Oscars' => 'Cinema'],
487 ['submitForm' => 'download now', 'no_cache' => 1],
488 ],
489 'Hooks and Middlewares modified with middleware overruling hooks' => [
490 ['batman' => '1'],
491 [],
492 // Enriched within PSR-7 query params + parsed body
493 ['ARD' => 'TV', 'Oscars' => 'Cinema'],
494 ['no_cache' => '1'],
495 // modified GET / POST parameters
496 ['batman' => '0', 'add_via_hook' => 'yes'],
497 ['submitForm' => 'download now', 'no_cache' => 0],
498 // expected merged results
499 ['batman' => '1', 'add_via_hook' => 'yes', 'ARD' => 'TV', 'Oscars' => 'Cinema'],
500 ['submitForm' => 'download now', 'no_cache' => 1],
501 ],
502 'Hooks and Middlewares modified with middleware overruling hooks with nested parameters' => [
503 ['batman' => '1'],
504 [['tx_siteexample_pi2' => ['uid' => 13]]],
505 // Enriched within PSR-7 query params + parsed body
506 ['ARD' => 'TV', 'Oscars' => 'Cinema', ['tx_blogexample_pi1' => ['uid' => 123]]],
507 ['no_cache' => '1', ['tx_siteexample_pi2' => ['name' => 'empty-tail']]],
508 // modified GET / POST parameters
509 ['batman' => '0', 'add_via_hook' => 'yes', ['tx_blogexample_pi1' => ['uid' => 234]]],
510 ['submitForm' => 'download now', 'no_cache' => 0],
511 // expected merged results
512 ['batman' => '1', 'add_via_hook' => 'yes', 'ARD' => 'TV', 'Oscars' => 'Cinema', ['tx_blogexample_pi1' => ['uid' => 123]]],
513 ['submitForm' => 'download now', 'no_cache' => '1', ['tx_siteexample_pi2' => ['uid' => 13, 'name' => 'empty-tail']]],
514 ],
515 ];
516 }
517
518 /**
519 * Test if the method modifies GET and POST to the expected result, when enriching an object.
520 *
521 * @param array $initialGetParams
522 * @param array $initialPostParams
523 * @param array $addedQueryParams
524 * @param array $addedParsedBody
525 * @param array $modifiedGetParams
526 * @param array $modifiedPostParams
527 * @param array $expectedQueryParams
528 * @param array $expectedParsedBody
529 * @dataProvider addModifiedGlobalsToIncomingRequestDataProvider
530 * @test
531 */
532 public function addModifiedGlobalsToIncomingRequestModifiesObject(
533 $initialGetParams,
534 $initialPostParams,
535 $addedQueryParams,
536 $addedParsedBody,
537 $modifiedGetParams,
538 $modifiedPostParams,
539 $expectedQueryParams,
540 $expectedParsedBody
541 ) {
542 GeneralUtility::flushInternalRuntimeCaches();
543 $_SERVER['REQUEST_METHOD'] = 'POST';
544 $_SERVER['HTTP_HOST'] = 'https://www.example.com/my/path/';
545 $_GET = $initialGetParams;
546 $_POST = $initialPostParams;
547 $request = ServerRequestFactory::fromGlobals();
548 $request = $request->withAttribute('_originalGetParameters', $initialGetParams);
549 $request = $request->withAttribute('_originalPostParameters', $initialPostParams);
550
551 // Now enriching the request object with other GET / POST parameters
552 $queryParams = $request->getQueryParams();
553 $queryParams = array_replace_recursive($queryParams, $addedQueryParams);
554 $request = $request->withQueryParams($queryParams);
555 $parsedBody = $request->getParsedBody() ?? [];
556 $parsedBody = array_replace_recursive($parsedBody, $addedParsedBody);
557 $request = $request->withParsedBody($parsedBody);
558
559 // Now overriding GET and POST parameters
560 $_GET = $modifiedGetParams;
561 $_POST = $modifiedPostParams;
562
563 $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
564 $subject->_set('timeTracker', new TimeTracker(false));
565 $resultRequest = $subject->_call('addModifiedGlobalsToIncomingRequest', $request);
566 $this->assertEquals($expectedQueryParams, $resultRequest->getQueryParams());
567 $this->assertEquals($expectedParsedBody, $resultRequest->getParsedBody());
568 }
569
570 /**
571 * Test if the method is called, and the globals are still the same after calling the method
572 *
573 * @test
574 */
575 public function resetGlobalsToCurrentRequestDoesNotModifyAnything()
576 {
577 $getVars = ['outside' => '1'];
578 $postVars = ['world' => 'yo'];
579 GeneralUtility::flushInternalRuntimeCaches();
580 $_SERVER['REQUEST_METHOD'] = 'POST';
581 $_SERVER['HTTP_HOST'] = 'https://www.example.com/my/path/';
582 $_GET = $getVars;
583 $_POST = $postVars;
584 $request = ServerRequestFactory::fromGlobals();
585
586 $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
587 $subject->_call('resetGlobalsToCurrentRequest', $request);
588 $this->assertEquals($_GET, $getVars);
589 $this->assertEquals($_POST, $postVars);
590 }
591
592 /**
593 * Test if the method is called, and the globals are still the same after calling the method
594 *
595 * @test
596 */
597 public function resetGlobalsToCurrentRequestWithModifiedRequestOverridesGlobals()
598 {
599 $getVars = ['typical' => '1'];
600 $postVars = ['mixtape' => 'wheels'];
601 $modifiedGetVars = ['typical' => 1, 'dont-stop' => 'the-music'];
602 $modifiedPostVars = ['mixtape' => 'wheels', 'tx_blogexample_pi1' => ['uid' => 13]];
603 GeneralUtility::flushInternalRuntimeCaches();
604 $_SERVER['REQUEST_METHOD'] = 'POST';
605 $_SERVER['HTTP_HOST'] = 'https://www.example.com/my/path/';
606 $_GET = $getVars;
607 $_POST = $postVars;
608 $request = ServerRequestFactory::fromGlobals();
609 $request = $request->withQueryParams($modifiedGetVars);
610 $request = $request->withParsedBody($modifiedPostVars);
611
612 $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
613 $subject->_call('resetGlobalsToCurrentRequest', $request);
614 $this->assertEquals($_GET, $modifiedGetVars);
615 $this->assertEquals($_POST, $modifiedPostVars);
616 }
617 }