‪TYPO3CMS  ‪main
RequestHandler.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 Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
23 use Psr\Http\Server\RequestHandlerInterface;
38 use TYPO3\CMS\Core\Type\File\ImageInfo;
48 
70 class ‪RequestHandler implements RequestHandlerInterface
71 {
72  public function ‪__construct(
73  private readonly EventDispatcherInterface $eventDispatcher,
74  private readonly ‪ListenerProvider $listenerProvider,
75  private readonly ‪TimeTracker $timeTracker,
76  private readonly ‪FilePathSanitizer $filePathSanitizer,
77  private readonly ‪TypoScriptService $typoScriptService,
78  private readonly ‪Context $context,
79  ) {}
80 
90  protected function ‪resetGlobalsToCurrentRequest(ServerRequestInterface $request)
91  {
92  if ($request->getQueryParams() !== $_GET) {
93  $queryParams = $request->getQueryParams();
94  $_GET = $queryParams;
95  ‪$GLOBALS['HTTP_GET_VARS'] = $_GET;
96  }
97  if ($request->getMethod() === 'POST') {
98  $parsedBody = $request->getParsedBody();
99  if (is_array($parsedBody) && $parsedBody !== $_POST) {
100  $_POST = $parsedBody;
101  ‪$GLOBALS['HTTP_POST_VARS'] = $_POST;
102  }
103  }
104  ‪$GLOBALS['TYPO3_REQUEST'] = $request;
105  }
106 
110  public function ‪handle(ServerRequestInterface $request): ResponseInterface
111  {
112  $controller = $request->getAttribute('frontend.controller');
113 
114  $this->‪resetGlobalsToCurrentRequest($request);
115 
116  // Generate page
117  if ($controller->isGeneratePage()) {
118  $this->timeTracker->push('Page generation');
119 
120  // forward `ConsumableNonce` containing a nonce to `PageRenderer`
121  $nonce = $request->getAttribute('nonce');
122  $this->‪getPageRenderer()->setNonce($nonce);
123 
124  $controller->preparePageContentGeneration($request);
125 
126  // Make sure all FAL resources are prefixed with absPrefPrefix
127  $this->listenerProvider->addListener(
128  GeneratePublicUrlForResourceEvent::class,
129  PublicUrlPrefixer::class,
130  'prefixWithAbsRefPrefix'
131  );
132 
133  // Content generation
134  $this->timeTracker->incStackPointer();
135  $this->timeTracker->push('Page generation PAGE object');
136 
137  $controller->content = $this->‪generatePageContent($controller, $request);
138 
139  $this->timeTracker->pull($this->timeTracker->LR ? $controller->content : '');
140  $this->timeTracker->decStackPointer();
141 
142  // In case the nonce value was actually consumed during the rendering process, add a
143  // permanent substitution of the current value (that will be cached), with a future
144  // value (that will be generated and issued in the HTTP CSP header).
145  // Besides that, the same handling is triggered in case there are other uncached items
146  // already - this is due to the fact that the `PageRenderer` state has been serialized
147  // before and note executed via `$pageRenderer->render()` and did not consume any nonce values
148  // (see serialization in `generatePageContent()`).
149  if ($nonce instanceof ‪ConsumableNonce && (count($nonce) > 0 || $controller->isINTincScript())) {
150  // nonce was consumed
151  $controller->config['INTincScript'][] = [
152  'target' => NonceValueSubstitution::class . '->substituteNonce',
153  'parameters' => ['nonce' => $nonce->value],
154  'permanent' => true,
155  ];
156  }
157 
158  $controller->generatePage_postProcessing($request);
159  $this->timeTracker->pull();
160  }
161 
162  // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation
163  if ($controller->isINTincScript()) {
164  if (!$controller->isGeneratePage()) {
165  // When the page was generated, this was already called. Avoid calling this twice.
166  $controller->preparePageContentGeneration($request);
167 
168  // Make sure all FAL resources are prefixed with absPrefPrefix
169  $this->listenerProvider->addListener(
170  GeneratePublicUrlForResourceEvent::class,
171  PublicUrlPrefixer::class,
172  'prefixWithAbsRefPrefix'
173  );
174  }
175  $this->timeTracker->push('Non-cached objects');
176  $controller->INTincScript($request);
177  $this->timeTracker->pull();
178  }
179 
180  // Create a default Response object and add headers and body to it
181  $response = new ‪Response();
182  $response = $controller->applyHttpHeadersToResponse($request, $response);
183  $this->‪displayPreviewInfoMessage($request, $controller);
184  $response->getBody()->write($controller->content);
185  return $response;
186  }
187 
192  protected function ‪generatePageContent(‪TypoScriptFrontendController $controller, ServerRequestInterface $request): string
193  {
194  // Generate the main content between the <body> tags
195  // This has to be done first, as some additional TSFE-related code could have been written
196  $pageContent = $this->‪generatePageBodyContent($controller, $request);
197  // If 'disableAllHeaderCode' is set, all the pageRenderer settings are not evaluated
198  $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
199  if ($typoScriptConfigArray['disableAllHeaderCode'] ?? false) {
200  return $pageContent;
201  }
202  // Now, populate pageRenderer with all additional data
203  $this->‪processHtmlBasedRenderingSettings($controller, $request);
204  $pageRenderer = $this->‪getPageRenderer();
205  // Add previously generated page content within the <body> tag afterwards
206  $pageRenderer->addBodyContent(LF . $pageContent);
207  if ($controller->‪isINTincScript()) {
208  // Store the serialized pageRenderer state in configuration
209  $controller->config['INTincScript_ext']['pageRendererState'] = serialize($pageRenderer->getState());
210  // Store the serialized AssetCollector state in configuration
211  $controller->config['INTincScript_ext']['assetCollectorState'] = serialize(GeneralUtility::makeInstance(AssetCollector::class)->getState());
212  // Render complete page, keep placeholders for JavaScript and CSS
213  return $pageRenderer->renderPageWithUncachedObjects($controller->config['INTincScript_ext']['divKey'] ?? '');
214  }
215  // Render complete page
216  return $pageRenderer->render();
217  }
218 
224  protected function ‪generatePageBodyContent(‪TypoScriptFrontendController $controller, ServerRequestInterface $request): string
225  {
226  $typoScriptPageSetupArray = $request->getAttribute('frontend.typoscript')->getPageArray();
227  $pageContent = $controller->cObj->cObjGet($typoScriptPageSetupArray) ?: '';
228  if ($typoScriptPageSetupArray['wrap'] ?? false) {
229  $pageContent = $controller->cObj->wrap($pageContent, $typoScriptPageSetupArray['wrap']);
230  }
231  if ($typoScriptPageSetupArray['stdWrap.'] ?? false) {
232  $pageContent = $controller->cObj->stdWrap($pageContent, $typoScriptPageSetupArray['stdWrap.']);
233  }
234  return $pageContent;
235  }
236 
244  protected function ‪processHtmlBasedRenderingSettings(‪TypoScriptFrontendController $controller, ServerRequestInterface $request): void
245  {
246  $pageRenderer = $this->‪getPageRenderer();
247  $typoScript = $request->getAttribute('frontend.typoscript');
248  $typoScriptSetupArray = $typoScript->getSetupArray();
249  $typoScriptConfigArray = $typoScript->getConfigArray();
250  $typoScriptPageArray = $typoScript->getPageArray();
251 
252  if ($typoScriptConfigArray['moveJsFromHeaderToFooter'] ?? false) {
253  $pageRenderer->enableMoveJsFromHeaderToFooter();
254  }
255  if ($typoScriptConfigArray['pageRendererTemplateFile'] ?? false) {
256  try {
257  $file = $this->filePathSanitizer->sanitize($typoScriptConfigArray['pageRendererTemplateFile'], true);
258  $pageRenderer->setTemplateFile($file);
259  } catch (‪Exception) {
260  // Custom template is not set if sanitize() throws
261  }
262  }
263  ‪$headerComment = trim($typoScriptConfigArray['headerComment'] ?? '');
264  if (‪$headerComment) {
265  $pageRenderer->addInlineComment("\t" . str_replace(LF, LF . "\t", ‪$headerComment) . LF);
266  }
267  $htmlTagAttributes = [];
268 
269  // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request.
270  $siteLanguage = $request->getAttribute('language') ?? $request->getAttribute('site')->getDefaultLanguage();
271  if ($siteLanguage->getLocale()->isRightToLeftLanguageDirection()) {
272  $htmlTagAttributes['dir'] = 'rtl';
273  }
274  $docType = $pageRenderer->getDocType();
275  // Set document type
276  $docTypeParts = [];
277  $xmlDocument = true;
278  // XML prologue
279  $xmlPrologue = (string)($typoScriptConfigArray['xmlprologue'] ?? '');
280  switch ($xmlPrologue) {
281  case 'none':
282  $xmlDocument = false;
283  break;
284  case 'xml_10':
285  case 'xml_11':
286  case '':
287  if ($docType->isXmlCompliant()) {
288  $docTypeParts[] = $docType->getXmlPrologue();
289  } else {
290  $xmlDocument = false;
291  }
292  break;
293  default:
294  $docTypeParts[] = $xmlPrologue;
295  }
296  // DTD
297  if ($docType->getDoctypeDeclaration() !== '') {
298  $docTypeParts[] = $docType->getDoctypeDeclaration();
299  }
300  if (!empty($docTypeParts)) {
301  $pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts));
302  }
303  // See https://www.w3.org/International/questions/qa-html-language-declarations.en.html#attributes
304  $htmlTagAttributes[$docType->isXmlCompliant() ? 'xml:lang' : 'lang'] = $siteLanguage->getLocale()->getLanguageCode();
305 
306  if ($docType->isXmlCompliant() || $docType === DocType::html5 && $xmlDocument) {
307  // We add this to HTML5 to achieve a slightly better backwards compatibility
308  $htmlTagAttributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
309  if (is_array($typoScriptConfigArray['namespaces.'] ?? false)) {
310  foreach ($typoScriptConfigArray['namespaces.'] as $prefix => $uri) {
311  // $uri gets htmlspecialchared later
312  $htmlTagAttributes['xmlns:' . htmlspecialchars($prefix)] = $uri;
313  }
314  }
315  }
316 
317  $pageRenderer->setHtmlTag($this->‪generateHtmlTag($htmlTagAttributes, $typoScriptConfigArray, $controller->cObj));
318 
319  $headTag = $typoScriptPageArray['headTag'] ?? '<head>';
320  if (isset($typoScriptPageArray['headTag.'])) {
321  $headTag = $controller->cObj->stdWrap($headTag, $typoScriptPageArray['headTag.']);
322  }
323  $pageRenderer->setHeadTag($headTag);
324 
325  $pageRenderer->addInlineComment(GeneralUtility::makeInstance(Typo3Information::class)->getInlineHeaderComment());
326 
327  if ($typoScriptPageArray['shortcutIcon'] ?? false) {
328  try {
329  $favIcon = $this->filePathSanitizer->sanitize($typoScriptPageArray['shortcutIcon']);
330  $iconFileInfo = GeneralUtility::makeInstance(ImageInfo::class, ‪Environment::getPublicPath() . '/' . $favIcon);
331  if ($iconFileInfo->isFile()) {
332  $iconMimeType = $iconFileInfo->getMimeType();
333  if ($iconMimeType) {
334  $iconMimeType = ' type="' . $iconMimeType . '"';
335  $pageRenderer->setIconMimeType($iconMimeType);
336  }
337  $pageRenderer->setFavIcon(‪PathUtility::getAbsoluteWebPath($controller->absRefPrefix . $favIcon));
338  }
339  } catch (‪Exception) {
340  // FavIcon is not set if sanitize() throws
341  }
342  }
343 
344  // Inline CSS from plugins, files, libraries and inline
345  if (is_array($typoScriptSetupArray['plugin.'] ?? false)) {
346  $stylesFromPlugins = '';
347  foreach ($typoScriptSetupArray['plugin.'] as $key => $iCSScode) {
348  if (is_array($iCSScode)) {
349  if (($iCSScode['_CSS_DEFAULT_STYLE'] ?? false) && empty($typoScriptConfigArray['removeDefaultCss'])) {
350  $cssDefaultStyle = $controller->cObj->stdWrapValue('_CSS_DEFAULT_STYLE', $iCSScode);
351  $stylesFromPlugins .= '/* default styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssDefaultStyle . LF;
352  }
353  }
354  }
355  if (!empty($stylesFromPlugins)) {
356  $this->‪addCssToPageRenderer($request, $stylesFromPlugins, false, 'InlineDefaultCss');
357  }
358  }
359  if (is_array($typoScriptPageArray['includeCSS.'] ?? false)) {
360  foreach ($typoScriptPageArray['includeCSS.'] as $key => $cssResource) {
361  if (is_array($cssResource)) {
362  continue;
363  }
364  $cssResourceConfig = $additionalAttributes = $typoScriptPageArray['includeCSS.'][$key . '.'] ?? [];
365  if (isset($cssResourceConfig['if.']) && !$controller->cObj->checkIf($cssResourceConfig['if.'])) {
366  continue;
367  }
368  if (!($cssResourceConfig['external'] ?? false)) {
369  try {
370  $cssResource = $this->filePathSanitizer->sanitize($cssResource, true);
371  } catch (‪Exception) {
372  continue;
373  }
374  }
375  unset(
376  $additionalAttributes['if.'],
377  $additionalAttributes['alternate'],
378  $additionalAttributes['media'],
379  $additionalAttributes['title'],
380  $additionalAttributes['external'],
381  $additionalAttributes['inline'],
382  $additionalAttributes['disableCompression'],
383  $additionalAttributes['excludeFromConcatenation'],
384  $additionalAttributes['allWrap'],
385  $additionalAttributes['allWrap.'],
386  $additionalAttributes['forceOnTop'],
387  );
388  $pageRenderer->addCssFile(
389  $cssResource,
390  ($cssResourceConfig['alternate'] ?? false) ? 'alternate stylesheet' : 'stylesheet',
391  ($cssResourceConfig['media'] ?? false) ?: 'all',
392  ($cssResourceConfig['title'] ?? false) ?: '',
393  empty($cssResourceConfig['external']) && empty($cssResourceConfig['inline']) && empty($cssResourceConfig['disableCompression']),
394  (bool)($cssResourceConfig['forceOnTop'] ?? false),
395  $cssResourceConfig['allWrap'] ?? '',
396  ($cssResourceConfig['excludeFromConcatenation'] ?? false) || ($cssResourceConfig['inline'] ?? false),
397  $cssResourceConfig['allWrap.']['splitChar'] ?? '|',
398  (bool)($cssResourceConfig['inline'] ?? false),
399  $additionalAttributes
400  );
401  }
402  }
403  if (is_array($typoScriptPageArray['includeCSSLibs.'] ?? false)) {
404  foreach ($typoScriptPageArray['includeCSSLibs.'] as $key => $cssResource) {
405  if (is_array($cssResource)) {
406  continue;
407  }
408  $cssResourceConfig = $additionalAttributes = $typoScriptPageArray['includeCSSLibs.'][$key . '.'] ?? [];
409  if (isset($cssResourceConfig['if.']) && !$controller->cObj->checkIf($cssResourceConfig['if.'])) {
410  continue;
411  }
412  if (!($cssResourceConfig['external'] ?? false)) {
413  try {
414  $cssResource = $this->filePathSanitizer->sanitize($cssResource, true);
415  } catch (‪Exception) {
416  continue;
417  }
418  }
419  unset(
420  $additionalAttributes['if.'],
421  $additionalAttributes['alternate'],
422  $additionalAttributes['media'],
423  $additionalAttributes['title'],
424  $additionalAttributes['external'],
425  $additionalAttributes['internal'],
426  $additionalAttributes['disableCompression'],
427  $additionalAttributes['excludeFromConcatenation'],
428  $additionalAttributes['allWrap'],
429  $additionalAttributes['allWrap.'],
430  $additionalAttributes['forceOnTop'],
431  );
432  $pageRenderer->addCssLibrary(
433  $cssResource,
434  ($cssResourceConfig['alternate'] ?? false) ? 'alternate stylesheet' : 'stylesheet',
435  ($cssResourceConfig['media'] ?? false) ?: 'all',
436  ($cssResourceConfig['title'] ?? false) ?: '',
437  empty($cssResourceConfig['external']) && empty($cssResourceConfig['inline']) && empty($cssResourceConfig['disableCompression']),
438  (bool)($cssResourceConfig['forceOnTop'] ?? false),
439  $cssResourceConfig['allWrap'] ?? '',
440  ($cssResourceConfig['excludeFromConcatenation'] ?? false) || ($cssResourceConfig['inline'] ?? false),
441  $cssResourceConfig['allWrap.']['splitChar'] ?? '|',
442  (bool)($cssResourceConfig['inline'] ?? false),
443  $additionalAttributes
444  );
445  }
446  }
447  $style = $controller->cObj->cObjGet($typoScriptPageArray['cssInline.'] ?? null, 'cssInline.');
448  if (trim($style)) {
449  $this->‪addCssToPageRenderer($request, $style, true, 'additionalTSFEInlineStyle');
450  }
451 
452  // JavaScript includes
453  if (is_array($typoScriptPageArray['includeJSLibs.'] ?? false)) {
454  foreach ($typoScriptPageArray['includeJSLibs.'] as $key => $jsResource) {
455  if (is_array($jsResource)) {
456  continue;
457  }
458  $jsResourceConfig = $additionalAttributes = $typoScriptPageArray['includeJSLibs.'][$key . '.'] ?? [];
459  if (isset($jsResourceConfig['if.']) && !$controller->cObj->checkIf($jsResourceConfig['if.'])) {
460  continue;
461  }
462  if (!($jsResourceConfig['external'] ?? false)) {
463  try {
464  $jsResource = $this->filePathSanitizer->sanitize($jsResource, true);
465  } catch (‪Exception) {
466  continue;
467  }
468  }
469  $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? '');
470  if ($crossOrigin === '' && ($jsResourceConfig['integrity'] ?? false) && ($jsResourceConfig['external'] ?? false)) {
471  $crossOrigin = 'anonymous';
472  }
473  unset(
474  $additionalAttributes['if.'],
475  $additionalAttributes['type'],
476  $additionalAttributes['crossorigin'],
477  $additionalAttributes['integrity'],
478  $additionalAttributes['external'],
479  $additionalAttributes['allWrap'],
480  $additionalAttributes['allWrap.'],
481  $additionalAttributes['disableCompression'],
482  $additionalAttributes['excludeFromConcatenation'],
483  $additionalAttributes['integrity'],
484  $additionalAttributes['defer'],
485  $additionalAttributes['nomodule'],
486  );
487  $pageRenderer->addJsLibrary(
488  $key,
489  $jsResource,
490  $jsResourceConfig['type'] ?? null,
491  empty($jsResourceConfig['external']) && empty($jsResourceConfig['disableCompression']),
492  (bool)($jsResourceConfig['forceOnTop'] ?? false),
493  $jsResourceConfig['allWrap'] ?? '',
494  (bool)($jsResourceConfig['excludeFromConcatenation'] ?? false),
495  $jsResourceConfig['allWrap.']['splitChar'] ?? '|',
496  (bool)($jsResourceConfig['async'] ?? false),
497  $jsResourceConfig['integrity'] ?? '',
498  (bool)($jsResourceConfig['defer'] ?? false),
499  $crossOrigin,
500  (bool)($jsResourceConfig['nomodule'] ?? false),
501  $additionalAttributes
502  );
503  }
504  }
505  if (is_array($typoScriptPageArray['includeJSFooterlibs.'] ?? false)) {
506  foreach ($typoScriptPageArray['includeJSFooterlibs.'] as $key => $jsResource) {
507  if (is_array($jsResource)) {
508  continue;
509  }
510  $jsResourceConfig = $additionalAttributes = $typoScriptPageArray['includeJSFooterlibs.'][$key . '.'] ?? [];
511  if (isset($jsResourceConfig['if.']) && !$controller->cObj->checkIf($jsResourceConfig['if.'])) {
512  continue;
513  }
514  if (!($jsResourceConfig['external'] ?? false)) {
515  try {
516  $jsResource = $this->filePathSanitizer->sanitize($jsResource, true);
517  } catch (‪Exception) {
518  continue;
519  }
520  }
521  $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? '');
522  if ($crossOrigin === '' && ($jsResourceConfig['integrity'] ?? false) && ($jsResourceConfig['external'] ?? false)) {
523  $crossOrigin = 'anonymous';
524  }
525  unset(
526  $additionalAttributes['if.'],
527  $additionalAttributes['type'],
528  $additionalAttributes['crossorigin'],
529  $additionalAttributes['integrity'],
530  $additionalAttributes['external'],
531  $additionalAttributes['allWrap'],
532  $additionalAttributes['allWrap.'],
533  $additionalAttributes['disableCompression'],
534  $additionalAttributes['excludeFromConcatenation'],
535  $additionalAttributes['integrity'],
536  $additionalAttributes['defer'],
537  $additionalAttributes['nomodule'],
538  );
539  $pageRenderer->addJsFooterLibrary(
540  $key,
541  $jsResource,
542  $jsResourceConfig['type'] ?? null,
543  empty($jsResourceConfig['external']) && empty($jsResourceConfig['disableCompression']),
544  (bool)($jsResourceConfig['forceOnTop'] ?? false),
545  $jsResourceConfig['allWrap'] ?? '',
546  (bool)($jsResourceConfig['excludeFromConcatenation'] ?? false),
547  $jsResourceConfig['allWrap.']['splitChar'] ?? '|',
548  (bool)($jsResourceConfig['async'] ?? false),
549  $jsResourceConfig['integrity'] ?? '',
550  (bool)($jsResourceConfig['defer'] ?? false),
551  $crossOrigin,
552  (bool)($jsResourceConfig['nomodule'] ?? false),
553  $additionalAttributes
554  );
555  }
556  }
557  if (is_array($typoScriptPageArray['includeJS.'] ?? false)) {
558  foreach ($typoScriptPageArray['includeJS.'] as $key => $jsResource) {
559  if (is_array($jsResource)) {
560  continue;
561  }
562  $jsResourceConfig = $typoScriptPageArray['includeJS.'][$key . '.'] ?? [];
563  if (isset($jsResourceConfig['if.']) && !$controller->cObj->checkIf($jsResourceConfig['if.'])) {
564  continue;
565  }
566  if (!($jsResourceConfig['external'] ?? false)) {
567  try {
568  $jsResource = $this->filePathSanitizer->sanitize($jsResource, true);
569  } catch (‪Exception) {
570  continue;
571  }
572  }
573  $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? '');
574  if ($crossOrigin === '' && ($jsResourceConfig['integrity'] ?? false) && ($jsResourceConfig['external'] ?? false)) {
575  $crossOrigin = 'anonymous';
576  }
577  $pageRenderer->addJsFile(
578  $jsResource,
579  $jsResourceConfig['type'] ?? null,
580  empty($jsResourceConfig['external']) && empty($jsResourceConfig['disableCompression']),
581  (bool)($jsResourceConfig['forceOnTop'] ?? false),
582  $jsResourceConfig['allWrap'] ?? '',
583  (bool)($jsResourceConfig['excludeFromConcatenation'] ?? false),
584  $jsResourceConfig['allWrap.']['splitChar'] ?? '|',
585  (bool)($jsResourceConfig['async'] ?? false),
586  $jsResourceConfig['integrity'] ?? '',
587  (bool)($jsResourceConfig['defer'] ?? false),
588  $crossOrigin,
589  (bool)($jsResourceConfig['nomodule'] ?? false),
590  // @todo: This does not use the same logic as with "additionalAttributes" above. Also not documented correctly.
591  $jsResourceConfig['data.'] ?? []
592  );
593  }
594  }
595  if (is_array($typoScriptPageArray['includeJSFooter.'] ?? false)) {
596  foreach ($typoScriptPageArray['includeJSFooter.'] as $key => $jsResource) {
597  if (is_array($jsResource)) {
598  continue;
599  }
600  $jsResourceConfig = $typoScriptPageArray['includeJSFooter.'][$key . '.'] ?? [];
601  if (isset($jsResourceConfig['if.']) && !$controller->cObj->checkIf($jsResourceConfig['if.'])) {
602  continue;
603  }
604  if (!($jsResourceConfig['external'] ?? false)) {
605  try {
606  $jsResource = $this->filePathSanitizer->sanitize($jsResource, true);
607  } catch (‪Exception) {
608  continue;
609  }
610  }
611  $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? '');
612  if ($crossOrigin === '' && ($jsResourceConfig['integrity'] ?? false) && ($jsResourceConfig['external'] ?? false)) {
613  $crossOrigin = 'anonymous';
614  }
615  $pageRenderer->addJsFooterFile(
616  $jsResource,
617  $jsResourceConfig['type'] ?? null,
618  empty($jsResourceConfig['external']) && empty($jsResourceConfig['disableCompression']),
619  (bool)($jsResourceConfig['forceOnTop'] ?? false),
620  $jsResourceConfig['allWrap'] ?? '',
621  (bool)($jsResourceConfig['excludeFromConcatenation'] ?? false),
622  $jsResourceConfig['allWrap.']['splitChar'] ?? '|',
623  (bool)($jsResourceConfig['async'] ?? false),
624  $jsResourceConfig['integrity'] ?? '',
625  (bool)($jsResourceConfig['defer'] ?? false),
626  $crossOrigin,
627  (bool)($jsResourceConfig['nomodule'] ?? false),
628  // @todo: This does not use the same logic as with "additionalAttributes" above. Also not documented correctly.
629  $jsResourceConfig['data.'] ?? []
630  );
631  }
632  }
633 
634  // Header and footer data
635  if (is_array($typoScriptPageArray['headerData.'] ?? false)) {
636  $pageRenderer->addHeaderData($controller->cObj->cObjGet($typoScriptPageArray['headerData.'], 'headerData.'));
637  }
638  if (is_array($typoScriptPageArray['footerData.'] ?? false)) {
639  $pageRenderer->addFooterData($controller->cObj->cObjGet($typoScriptPageArray['footerData.'], 'footerData.'));
640  }
641 
642  $controller->‪generatePageTitle($request);
643 
644  // @internal hook for EXT:seo, will be gone soon, do not use it in your own extensions
645  $_params = ['request' => $request];
646  $_ref = null;
647  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) {
648  GeneralUtility::callUserFunction($_funcRef, $_params, $_ref);
649  }
650 
651  $this->‪generateHrefLangTags($controller, $request);
652  $this->‪generateMetaTagHtml($typoScriptPageArray['meta.'] ?? [], $controller->cObj);
653 
654  // Javascript inline and inline footer code
655  $inlineJS = implode(LF, $controller->cObj->cObjGetSeparated($typoScriptPageArray['jsInline.'] ?? null, 'jsInline.'));
656  $inlineFooterJs = implode(LF, $controller->cObj->cObjGetSeparated($typoScriptPageArray['jsFooterInline.'] ?? null, 'jsFooterInline.'));
657 
658  // Needs to be called after all cObjGet() calls in order to get all headerData and footerData and replacements
659  $controller->‪INTincScript_loadJSCode();
660 
661  $compressJs = (bool)($typoScriptConfigArray['compressJs'] ?? false);
662  if (($typoScriptConfigArray['removeDefaultJS'] ?? 'external') === 'external') {
663  // "removeDefaultJS" is "external" by default
664  // This keeps inlineJS from *_INT Objects from being moved to external files.
665  // At this point in frontend rendering *_INT Objects only have placeholders instead
666  // of actual content. Moving these placeholders to external files would break the JS file with
667  // syntax errors due to the placeholders, and the needed JS would never get included to the page.
668  // Therefore, inlineJS from *_INT Objects must not be moved to external files but kept internal.
669  $inlineJSint = '';
670  $this->‪stripIntObjectPlaceholder($inlineJS, $inlineJSint);
671  if ($inlineJSint) {
672  $pageRenderer->addJsInlineCode('TS_inlineJSint', $inlineJSint, $compressJs);
673  }
674  if (trim($inlineJS)) {
675  $pageRenderer->addJsFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineJS), null, $compressJs);
676  }
677  if ($inlineFooterJs) {
678  $inlineFooterJSint = '';
679  $this->‪stripIntObjectPlaceholder($inlineFooterJs, $inlineFooterJSint);
680  if ($inlineFooterJSint) {
681  $pageRenderer->addJsFooterInlineCode('TS_inlineFooterJSint', $inlineFooterJSint, $compressJs);
682  }
683  $pageRenderer->addJsFooterFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineFooterJs), null, $compressJs);
684  }
685  } else {
686  // Include only inlineJS
687  if ($inlineJS) {
688  $pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS, $compressJs);
689  }
690  if ($inlineFooterJs) {
691  $pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs, $compressJs);
692  }
693  }
694  if (is_array($typoScriptPageArray['inlineLanguageLabelFiles.'] ?? false)) {
695  foreach ($typoScriptPageArray['inlineLanguageLabelFiles.'] as $key => $languageFile) {
696  if (is_array($languageFile)) {
697  continue;
698  }
699  $languageFileConfig = $typoScriptPageArray['inlineLanguageLabelFiles.'][$key . '.'] ?? [];
700  if (isset($languageFileConfig['if.']) && !$controller->cObj->checkIf($languageFileConfig['if.'])) {
701  continue;
702  }
703  $pageRenderer->addInlineLanguageLabelFile(
704  $languageFile,
705  ($languageFileConfig['selectionPrefix'] ?? false) ? $languageFileConfig['selectionPrefix'] : '',
706  ($languageFileConfig['stripFromSelectionName'] ?? false) ? $languageFileConfig['stripFromSelectionName'] : ''
707  );
708  }
709  }
710  if (is_array($typoScriptPageArray['inlineSettings.'] ?? false)) {
711  $pageRenderer->addInlineSettingArray('TS', $typoScriptPageArray['inlineSettings.']);
712  }
713  // Compression and concatenate settings
714  if ($typoScriptConfigArray['compressCss'] ?? false) {
715  $pageRenderer->enableCompressCss();
716  }
717  if ($compressJs) {
718  $pageRenderer->enableCompressJavascript();
719  }
720  if ($typoScriptConfigArray['concatenateCss'] ?? false) {
721  $pageRenderer->enableConcatenateCss();
722  }
723  if ($typoScriptConfigArray['concatenateJs'] ?? false) {
724  $pageRenderer->enableConcatenateJavascript();
725  }
726  // Add header data block
727  if ($controller->additionalHeaderData) {
728  $pageRenderer->addHeaderData(implode(LF, $controller->additionalHeaderData));
729  }
730  // Add footer data block
731  if ($controller->additionalFooterData) {
732  $pageRenderer->addFooterData(implode(LF, $controller->additionalFooterData));
733  }
734  // Header complete, now the body tag is added so the regular content can be applied later-on
735  if ($typoScriptConfigArray['disableBodyTag'] ?? false) {
736  $pageRenderer->addBodyContent(LF);
737  } else {
738  $bodyTag = '<body>';
739  if ($typoScriptPageArray['bodyTag'] ?? false) {
740  $bodyTag = $typoScriptPageArray['bodyTag'];
741  } elseif ($typoScriptPageArray['bodyTagCObject'] ?? false) {
742  $bodyTag = $controller->cObj->cObjGetSingle($typoScriptPageArray['bodyTagCObject'], $typoScriptPageArray['bodyTagCObject.'] ?? [], 'bodyTagCObject');
743  }
744  if (trim($typoScriptPageArray['bodyTagAdd'] ?? '')) {
745  $bodyTag = preg_replace('/>$/', '', trim($bodyTag)) . ' ' . trim($typoScriptPageArray['bodyTagAdd']) . '>';
746  }
747  $pageRenderer->addBodyContent(LF . $bodyTag);
748  }
749  }
750 
758  protected function ‪stripIntObjectPlaceholder(&$searchString, &$intObjects)
759  {
760  $tempArray = [];
761  preg_match_all('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', $searchString, $tempArray);
762  $searchString = preg_replace('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', '', $searchString);
763  $intObjects = implode('', $tempArray[0]);
764  }
765 
771  protected function ‪generateMetaTagHtml(array $metaTagTypoScript, ‪ContentObjectRenderer $cObj)
772  {
773  $pageRenderer = $this->‪getPageRenderer();
774  $conf = $this->typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
775  foreach ($conf as $key => $properties) {
776  $replace = false;
777  if (is_array($properties)) {
778  $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
779  $value = trim((string)$cObj->‪stdWrap($nodeValue, $metaTagTypoScript[$key . '.']));
780  if ($value === '' && !empty($properties['value'])) {
781  $value = $properties['value'];
782  $replace = false;
783  }
784  } else {
785  $value = $properties;
786  }
787 
788  $attribute = 'name';
789  if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
790  $attribute = 'http-equiv';
791  }
792  if (is_array($properties) && !empty($properties['attribute'])) {
793  $attribute = $properties['attribute'];
794  }
795  if (is_array($properties) && !empty($properties['replace'])) {
796  $replace = true;
797  }
798 
799  if (!is_array($value)) {
800  $value = (array)$value;
801  }
802  foreach ($value as $subValue) {
803  if (trim($subValue ?? '') !== '') {
804  $pageRenderer->setMetaTag($attribute, $key, $subValue, [], $replace);
805  }
806  }
807  }
808  }
809 
810  protected function ‪getPageRenderer(): ‪PageRenderer
811  {
812  return GeneralUtility::makeInstance(PageRenderer::class);
813  }
814 
822  protected function ‪addCssToPageRenderer(ServerRequestInterface $request, string $cssStyles, bool $excludeFromConcatenation, string $inlineBlockName): void
823  {
824  $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
825  // This option is enabled by default on purpose
826  if (empty($typoScriptConfigArray['inlineStyle2TempFile'] ?? true)) {
827  $this->‪getPageRenderer()->addCssInlineBlock($inlineBlockName, $cssStyles, !empty($typoScriptConfigArray['compressCss'] ?? false));
828  } else {
829  $this->‪getPageRenderer()->addCssFile(
830  GeneralUtility::writeStyleSheetContentToTemporaryFile($cssStyles),
831  'stylesheet',
832  'all',
833  '',
834  (bool)($typoScriptConfigArray['compressCss'] ?? false),
835  false,
836  '',
837  $excludeFromConcatenation
838  );
839  }
840  }
841 
861  protected function ‪generateHtmlTag(array $htmlTagAttributes, array $configuration, ‪ContentObjectRenderer $cObj): string
862  {
863  if (is_array($configuration['htmlTag.']['attributes.'] ?? null)) {
864  $attributeString = '';
865  foreach ($configuration['htmlTag.']['attributes.'] as $attributeName => $value) {
866  $attributeString .= ' ' . htmlspecialchars($attributeName) . ($value !== '' ? '="' . htmlspecialchars((string)$value) . '"' : '');
867  // If e.g. "htmlTag.attributes.dir" is set, make sure it is not added again with "implodeAttributes()"
868  if (isset($htmlTagAttributes[$attributeName])) {
869  unset($htmlTagAttributes[$attributeName]);
870  }
871  }
872  $attributeString = ltrim(GeneralUtility::implodeAttributes($htmlTagAttributes) . $attributeString);
873  } elseif (($configuration['htmlTag_setParams'] ?? '') === 'none') {
874  $attributeString = '';
875  } elseif (isset($configuration['htmlTag_setParams'])) {
876  $attributeString = $configuration['htmlTag_setParams'];
877  } else {
878  $attributeString = GeneralUtility::implodeAttributes($htmlTagAttributes);
879  }
880  $htmlTag = '<html' . ($attributeString ? ' ' . $attributeString : '') . '>';
881  if (isset($configuration['htmlTag_stdWrap.'])) {
882  $htmlTag = $cObj->‪stdWrap($htmlTag, $configuration['htmlTag_stdWrap.']);
883  }
884  return $htmlTag;
885  }
886 
887  protected function ‪generateHrefLangTags(‪TypoScriptFrontendController $controller, ServerRequestInterface $request): void
888  {
889  $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
890  if ($typoScriptConfigArray['disableHrefLang'] ?? false) {
891  return;
892  }
893  $hrefLangs = $this->eventDispatcher->dispatch(new ‪ModifyHrefLangTagsEvent($request))->getHrefLangs();
894  if (count($hrefLangs) > 1) {
895  $data = [];
896  foreach ($hrefLangs as $hrefLang => $href) {
897  $data[] = sprintf('<link %s/>', GeneralUtility::implodeAttributes([
898  'rel' => 'alternate',
899  'hreflang' => $hrefLang,
900  'href' => $href,
901  ], true));
902  }
903  $controller->additionalHeaderData[] = implode(LF, $data);
904  }
905  }
906 
912  protected function ‪displayPreviewInfoMessage(ServerRequestInterface $request, ‪TypoScriptFrontendController $controller)
913  {
914  $isInWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline', false);
915  $isInPreviewMode = $this->context->hasAspect('frontend.preview')
916  && $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
917  $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray();
918  if (!$isInPreviewMode || $isInWorkspace || ($typoScriptConfigArray['disablePreviewNotification'] ?? false)) {
919  return;
920  }
921  if ($typoScriptConfigArray['message_preview'] ?? '') {
922  $message = $typoScriptConfigArray['message_preview'];
923  } else {
924  $label = $this->‪getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_tsfe.xlf:preview');
925  $styles = [];
926  $styles[] = 'position: fixed';
927  $styles[] = 'top: 15px';
928  $styles[] = 'right: 15px';
929  $styles[] = 'padding: 8px 18px';
930  $styles[] = 'background: #fff3cd';
931  $styles[] = 'border: 1px solid #ffeeba';
932  $styles[] = 'font-family: sans-serif';
933  $styles[] = 'font-size: 14px';
934  $styles[] = 'font-weight: bold';
935  $styles[] = 'color: #856404';
936  $styles[] = 'z-index: 20000';
937  $styles[] = 'user-select: none';
938  $styles[] = 'pointer-events: none';
939  $styles[] = 'text-align: center';
940  $styles[] = 'border-radius: 2px';
941  $message = '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . htmlspecialchars($label) . '</div>';
942  }
943  if (!empty($message)) {
944  $controller->content = str_ireplace('</body>', $message . '</body>', $controller->content);
945  }
946  }
947 
949  {
950  return GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences(‪$GLOBALS['BE_USER'] ?? null);
951  }
952 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Page\AssetCollector
Definition: AssetCollector.php:42
‪$headerComment
‪$headerComment
Definition: header-comment.php:41
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Information\Typo3Information
Definition: Typo3Information.php:28
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController\generatePageTitle
‪generatePageTitle(ServerRequestInterface $request)
Definition: TypoScriptFrontendController.php:482
‪TYPO3\CMS\Frontend\Http\RequestHandler\generatePageBodyContent
‪generatePageBodyContent(TypoScriptFrontendController $controller, ServerRequestInterface $request)
Definition: RequestHandler.php:224
‪TYPO3\CMS\Frontend\Http\RequestHandler\__construct
‪__construct(private readonly EventDispatcherInterface $eventDispatcher, private readonly ListenerProvider $listenerProvider, private readonly TimeTracker $timeTracker, private readonly FilePathSanitizer $filePathSanitizer, private readonly TypoScriptService $typoScriptService, private readonly Context $context,)
Definition: RequestHandler.php:72
‪TYPO3\CMS\Frontend\Http
Definition: Application.php:18
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Frontend\Http\RequestHandler\generatePageContent
‪generatePageContent(TypoScriptFrontendController $controller, ServerRequestInterface $request)
Definition: RequestHandler.php:192
‪TYPO3\CMS\Frontend\Resource\FilePathSanitizer
Definition: FilePathSanitizer.php:39
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce
Definition: ConsumableNonce.php:24
‪TYPO3\CMS\Frontend\Http\RequestHandler\processHtmlBasedRenderingSettings
‪processHtmlBasedRenderingSettings(TypoScriptFrontendController $controller, ServerRequestInterface $request)
Definition: RequestHandler.php:244
‪TYPO3\CMS\Frontend\Resource\PublicUrlPrefixer
Definition: PublicUrlPrefixer.php:26
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent
Definition: GeneratePublicUrlForResourceEvent.php:31
‪TYPO3\CMS\Frontend\Cache\NonceValueSubstitution
Definition: NonceValueSubstitution.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\getAbsoluteWebPath
‪static string getAbsoluteWebPath(string $targetPath, bool $prefixWithSitePath=true)
Definition: PathUtility.php:52
‪TYPO3\CMS\Core\Page\PageRenderer
Definition: PageRenderer.php:44
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController\isINTincScript
‪bool isINTincScript()
Definition: TypoScriptFrontendController.php:716
‪TYPO3\CMS\Core\Http\Response
Definition: Response.php:32
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\stdWrap
‪string null stdWrap($content='', $conf=[])
Definition: ContentObjectRenderer.php:1039
‪TYPO3\CMS\Frontend\Http\RequestHandler\stripIntObjectPlaceholder
‪stripIntObjectPlaceholder(&$searchString, &$intObjects)
Definition: RequestHandler.php:758
‪TYPO3\CMS\Frontend\Http\RequestHandler\generateHrefLangTags
‪generateHrefLangTags(TypoScriptFrontendController $controller, ServerRequestInterface $request)
Definition: RequestHandler.php:887
‪TYPO3\CMS\Frontend\Http\RequestHandler\getPageRenderer
‪getPageRenderer()
Definition: RequestHandler.php:810
‪TYPO3\CMS\Frontend\Http\RequestHandler\addCssToPageRenderer
‪addCssToPageRenderer(ServerRequestInterface $request, string $cssStyles, bool $excludeFromConcatenation, string $inlineBlockName)
Definition: RequestHandler.php:822
‪TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent
Definition: ModifyHrefLangTagsEvent.php:27
‪TYPO3\CMS\Core\TypoScript\TypoScriptService
Definition: TypoScriptService.php:27
‪TYPO3\CMS\Frontend\Http\RequestHandler
Definition: RequestHandler.php:71
‪TYPO3\CMS\Frontend\Http\RequestHandler\getLanguageService
‪getLanguageService()
Definition: RequestHandler.php:948
‪TYPO3\CMS\Core\Resource\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:58
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Frontend\Http\RequestHandler\generateHtmlTag
‪string generateHtmlTag(array $htmlTagAttributes, array $configuration, ContentObjectRenderer $cObj)
Definition: RequestHandler.php:861
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Frontend\Http\RequestHandler\displayPreviewInfoMessage
‪displayPreviewInfoMessage(ServerRequestInterface $request, TypoScriptFrontendController $controller)
Definition: RequestHandler.php:912
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController\INTincScript_loadJSCode
‪INTincScript_loadJSCode()
Definition: TypoScriptFrontendController.php:693
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
Definition: ContentObjectRenderer.php:102
‪TYPO3\CMS\Frontend\Http\RequestHandler\handle
‪handle(ServerRequestInterface $request)
Definition: RequestHandler.php:110
‪TYPO3\CMS\Core\Type\DocType
‪DocType
Definition: DocType.php:27
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Frontend\Http\RequestHandler\generateMetaTagHtml
‪generateMetaTagHtml(array $metaTagTypoScript, ContentObjectRenderer $cObj)
Definition: RequestHandler.php:771
‪TYPO3\CMS\Core\EventDispatcher\ListenerProvider
Definition: ListenerProvider.php:30
‪TYPO3\CMS\Core\TimeTracker\TimeTracker
Definition: TimeTracker.php:34
‪TYPO3\CMS\Core\Resource\Exception
Definition: AbstractFileOperationException.php:16
‪TYPO3\CMS\Frontend\Http\RequestHandler\resetGlobalsToCurrentRequest
‪resetGlobalsToCurrentRequest(ServerRequestInterface $request)
Definition: RequestHandler.php:90