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