getAttribute('nonce'); $nonce = $nonce instanceof ConsumableNonce ? $nonce : null; $this->pageRenderer->setNonce($nonce); $policyBag = $request->getAttribute('csp.policyBag'); $policyBag = $policyBag instanceof PolicyBag ? $policyBag : null; // Make sure all FAL resources are prefixed with absPrefPrefix $this->listenerProvider->addListener( GeneratePublicUrlForResourceEvent::class, PublicUrlPrefixer::class, 'prefixWithAbsRefPrefix' ); $pageParts = $request->getAttribute('frontend.page.parts'); if (!$pageParts instanceof PageParts) { throw new \RuntimeException('Attribute frontend.page.parts must be an instance of PageParts at this point', 1761829876); } $content = $pageParts->getContent(); if (!$pageParts->hasPageContentBeenLoadedFromCache()) { $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); $pageInformation = $request->getAttribute('frontend.page.information'); $this->timeTracker->push('Page generation'); $docType = DocType::createFromConfigurationKey($typoScriptConfigArray['doctype'] ?? ''); $this->pageRenderer->setDocType($docType, $request); // Content generation $this->timeTracker->incStackPointer(); $this->timeTracker->push('Page generation PAGE object'); $content = $this->generatePageContent($request); $this->timeTracker->pull($this->timeTracker->LR ? $content : ''); $this->timeTracker->decStackPointer(); // In case the nonce value was actually consumed during the rendering process, add a // permanent substitution of the current value (that will be cached), with a future // value (that will be generated and issued in the HTTP CSP header). // Side-note: Nonce values that are consumed in non-cacheable parts (USER_INT/COA_INT) // are not handled here, since it would require writing the caches at the very end of // the whole frontend rendering process. if ($nonce !== null) { // prepare the policy in any case (even if nonce was not consumed) // (`AvoidContentSecurityPolicyNonceEventListener` adjusts the behavior) if ($policyBag !== null) { $this->policyProvider->prepare($policyBag, $request, $content); } // register nonce substitution if explicitly enabled, otherwise (if undefined) // use it if nonce value was consumed or any non-cached content elements exist if ($policyBag?->behavior->useNonce ?? (count($nonce) > 0 || $pageParts->hasNotCachedContentElements()) ) { $pageParts->addNotCachedContentElement([ 'substKey' => null, 'target' => NonceValueSubstitution::class . '->substituteNonce', 'parameters' => ['nonce' => $nonce->value], 'permanent' => true, ]); } if ($policyBag?->behavior->useNonce === false) { $content = $this->responseService->dropNonceFromHtml($content, $nonce); } } $event = new AfterCacheableContentIsGeneratedEvent($request, $content, $cacheDataCollector->getPageCacheIdentifier(), $cacheInstruction->isCachingAllowed()); $event = $this->eventDispatcher->dispatch($event); $content = $event->getContent(); // Write page cache if allowed if ($event->isCachingEnabled()) { $pageId = $pageInformation->getId(); $pageRecord = $pageInformation->getPageRecord(); $lifetime = $this->cacheLifetimeCalculator->calculateLifetimeForPage($pageInformation->getId(), $pageInformation->getPageRecord(), $typoScriptConfigArray, $this->context); $cacheDataCollector->addCacheTags(new CacheTag('pageId_' . $pageId, $lifetime)); if ($pageId !== $pageInformation->getContentFromPid()) { // Respect the page cache when content from different pid is shown $cacheDataCollector->addCacheTags(new CacheTag('pageId_' . $pageInformation->getContentFromPid(), $lifetime)); } if ((int)($pageRecord['_LOCALIZED_UID'] ?? 0) > 0) { // Respect the translation page id on translated pages $cacheDataCollector->addCacheTags(new CacheTag('pageId_' . $pageRecord['_LOCALIZED_UID'], $lifetime)); } if (!empty($pageRecord['cache_tags'])) { $tags = GeneralUtility::trimExplode(',', $pageRecord['cache_tags'], true); array_walk($tags, fn(string $tag) => $cacheDataCollector->addCacheTags(new CacheTag($tag, $lifetime))); } $cacheData = [ 'page_id' => $pageId, 'content' => $content, 'contentType' => $pageParts->getHttpContentType(), 'INTincScript' => $pageParts->getNotCachedContentElementRegistry(), 'pageRendererSubstitutionHash' => $pageParts->getPageRendererSubstitutionHash(), 'pageRendererState' => serialize($this->pageRenderer->getState()), 'assetCollectorState' => serialize($this->assetCollector->getState()), 'pageTitleCache' => $pageParts->getPageTitle(), 'pageCacheGeneratedTimestamp' => $GLOBALS['EXEC_TIME'], 'metaDataState' => $this->metaDataState->getState(), ]; $cacheDataCollector->enqueueCacheEntry( new CacheEntry( identifier: 'tsfe-page-cache', content: $cacheData, persist: function (ServerRequestInterface $request, string $identifier, mixed $content) { $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); $cacheTimeout = $cacheDataCollector->resolveLifetime(); $pageCacheTags = array_map(fn(CacheTag $cacheTag) => $cacheTag->name, $cacheDataCollector->getCacheTags()); $content['cacheTags'] = $pageCacheTags; $content['pageCacheExpireTimestamp'] = $GLOBALS['EXEC_TIME'] + $cacheTimeout; $this->pageCache->set($cacheDataCollector->getPageCacheIdentifier(), $content, $pageCacheTags, $cacheTimeout); // Event for cache post processing (eg. writing static files) $this->eventDispatcher->dispatch( new AfterCachedPageIsPersistedEvent($request, $cacheDataCollector->getPageCacheIdentifier(), $content, $cacheTimeout) ); } ) ); } $this->updateSysLastChangedInPageRecord($request); $this->timeTracker->pull(); } // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation if ($pageParts->hasNotCachedContentElements()) { $this->timeTracker->push('Non-cached objects'); $content = $this->calculateNonCachedElements($request, $content); $this->timeTracker->pull(); } $content = $this->displayPreviewInfoMessage($request, $content); // Create a default Response object and add headers and body to it $response = new Response(); $response = $this->addHttpHeadersToResponse($request, $response, $content); $response->getBody()->write($content); return $response; } /** * Generates the main body part for the page, and if "config.disableAllHeaderCode" is not active, triggers * pageRenderer to evaluate includeCSS, headTag etc. TypoScript processing to populate the pageRenderer. */ protected function generatePageContent(ServerRequestInterface $request): string { // Generate the main content between the tags // This has to be done first, as some additional frontend related code could have been written?! $pageContent = $this->generatePageBodyContent($request); // If 'disableAllHeaderCode' is set, all the pageRenderer settings are not evaluated $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); if ($typoScriptConfigArray['disableAllHeaderCode'] ?? false) { return $pageContent; } // Now, populate pageRenderer with all additional data $this->processHtmlBasedRenderingSettings($request); // Add previously generated page content within the tag afterwards $this->pageRenderer->addBodyContent(LF . $pageContent); $pageParts = $request->getAttribute('frontend.page.parts'); if ($pageParts->hasNotCachedContentElements()) { // Render complete page, keep placeholders for JavaScript and CSS return $this->pageRenderer->renderPageWithUncachedObjects($pageParts->getPageRendererSubstitutionHash()); } // Render complete page return $this->pageRenderer->renderFrontendPage($request); } /** * Generates the main content part within tags (except JS files/CSS files), this means: * render everything that can be cached, otherwise put placeholders for COA_INT/USER_INT objects * in the content that is processed later-on. */ protected function generatePageBodyContent(ServerRequestInterface $request): string { $typoScriptPageSetupArray = $request->getAttribute('frontend.typoscript')->getPageArray(); $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $contentObjectRenderer->setRequest($request); $contentObjectRenderer->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages'); $pageContent = $contentObjectRenderer->cObjGet($typoScriptPageSetupArray) ?: ''; if ($typoScriptPageSetupArray['wrap'] ?? false) { $pageContent = $contentObjectRenderer->wrap($pageContent, $typoScriptPageSetupArray['wrap']); } if ($typoScriptPageSetupArray['stdWrap.'] ?? false) { $pageContent = $contentObjectRenderer->stdWrap($pageContent, $typoScriptPageSetupArray['stdWrap.']); } return $pageContent; } /** * Calculate non cached elements and inline to given cacheable content. */ protected function calculateNonCachedElements(ServerRequestInterface $request, string $content): string { $content = $this->recursivelyReplaceIntPlaceholdersInContent($request, $content); $this->timeTracker->push('Substitute header section'); $titleTagContent = $this->generatePageTitle($request); $this->pageRenderer->setTitle($titleTagContent); $pageParts = $request->getAttribute('frontend.page.parts'); $content = $this->pageRenderer->renderJavaScriptAndCssForProcessingOfUncachedContentObjects($request, $content, $pageParts->getPageRendererSubstitutionHash()); // Replace again, because header and footer data and page renderer replacements may introduce additional placeholders (see #44825) $content = $this->recursivelyReplaceIntPlaceholdersInContent($request, $content); $this->timeTracker->pull(); return $content; } /** * At this point, the cacheable content has just been generated: Content is available but hasn't been added * to PageRenderer yet. The method is called after the "main" page content, since some JS may be inserted at that point * that has been registered by cacheable plugins. * PageRenderer is now populated with all data and additional JavaScript/CSS/FooterData/HeaderData that can be cached. * Once finished, the content is added to the >addBodyContent() functionality. */ protected function processHtmlBasedRenderingSettings(ServerRequestInterface $request): void { $typoScript = $request->getAttribute('frontend.typoscript'); $typoScriptSetupArray = $typoScript->getSetupArray(); $typoScriptConfigArray = $typoScript->getConfigArray(); $typoScriptPageArray = $typoScript->getPageArray(); if ($typoScriptConfigArray['moveJsFromHeaderToFooter'] ?? false) { $this->pageRenderer->enableMoveJsFromHeaderToFooter(); } if ($typoScriptConfigArray['pageRendererTemplateFile'] ?? false) { try { $resource = $this->systemResourceFactory->createResource($typoScriptConfigArray['pageRendererTemplateFile']); $this->pageRenderer->setTemplateFile((string)$resource); } catch (SystemResourceException) { // Custom template is not set if createResource() throws } } $headerComment = trim($typoScriptConfigArray['headerComment'] ?? ''); if ($headerComment) { $this->pageRenderer->addInlineComment("\t" . str_replace(LF, LF . "\t", $headerComment) . LF); } $htmlTagAttributes = []; // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request. $siteLanguage = $request->getAttribute('language') ?? $request->getAttribute('site')->getDefaultLanguage(); if ($siteLanguage->getLocale()->isRightToLeftLanguageDirection()) { $htmlTagAttributes['dir'] = 'rtl'; } $docType = DocType::createFromConfigurationKey($typoScriptConfigArray['doctype'] ?? ''); // Set document type $docTypeParts = []; $xmlDocument = true; // XML prologue $xmlPrologue = (string)($typoScriptConfigArray['xmlprologue'] ?? ''); switch ($xmlPrologue) { case 'none': $xmlDocument = false; break; case 'xml_10': case 'xml_11': case '': if ($docType->isXmlCompliant()) { $docTypeParts[] = $docType->getXmlPrologue(); } else { $xmlDocument = false; } break; default: $docTypeParts[] = $xmlPrologue; } // DTD if ($docType->getDoctypeDeclaration() !== '') { $docTypeParts[] = $docType->getDoctypeDeclaration(); } if (!empty($docTypeParts)) { $this->pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts)); } // See https://www.w3.org/International/questions/qa-html-language-declarations.en.html#attributes // and https://datatracker.ietf.org/doc/html/rfc5646 $htmlTagAttributes[$docType->isXmlCompliant() ? 'xml:lang' : 'lang'] = $siteLanguage->getHreflang(); if ($docType->isXmlCompliant() || $docType === DocType::html5 && $xmlDocument) { // We add this to HTML5 to achieve a slightly better backwards compatibility $htmlTagAttributes['xmlns'] = 'http://www.w3.org/1999/xhtml'; if (is_array($typoScriptConfigArray['namespaces.'] ?? false)) { foreach ($typoScriptConfigArray['namespaces.'] as $prefix => $uri) { // $uri gets htmlspecialchared later $htmlTagAttributes['xmlns:' . htmlspecialchars($prefix)] = $uri; } } } $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $contentObjectRenderer->setRequest($request); $contentObjectRenderer->start($request->getAttribute('frontend.page.information')->getPageRecord(), 'pages'); $this->pageRenderer->setHtmlTag($this->generateHtmlTag($htmlTagAttributes, $typoScriptConfigArray, $contentObjectRenderer)); $headTag = $typoScriptPageArray['headTag'] ?? ''; if (isset($typoScriptPageArray['headTag.'])) { $headTag = $contentObjectRenderer->stdWrap($headTag, $typoScriptPageArray['headTag.']); } $this->pageRenderer->setHeadTag($headTag); $this->pageRenderer->addInlineComment($this->typo3Information->getInlineHeaderComment()); if ($typoScriptPageArray['shortcutIcon'] ?? false) { try { $favIconResource = $this->systemResourceFactory->createPublicResource($typoScriptPageArray['shortcutIcon']); if ($favIconResource instanceof SystemResourceInterface) { $this->pageRenderer->setIconMimeType(' type="' . $favIconResource->getMimeType() . '"'); } $this->pageRenderer->setFavIcon((string)$this->resourcePublisher->generateUri($favIconResource, $request)); } catch (SystemResourceException) { // FavIcon is not set if sanitize() throws } } // Inline CSS from plugins, files, libraries and inline if (is_array($typoScriptSetupArray['plugin.'] ?? false)) { $stylesFromPlugins = ''; foreach ($typoScriptSetupArray['plugin.'] as $key => $iCSScode) { if (is_array($iCSScode)) { if (($iCSScode['_CSS_DEFAULT_STYLE'] ?? false) && empty($typoScriptConfigArray['removeDefaultCss'])) { $cssDefaultStyle = $contentObjectRenderer->stdWrapValue('_CSS_DEFAULT_STYLE', $iCSScode); $stylesFromPlugins .= '/* default styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssDefaultStyle . LF; } } } if (!empty($stylesFromPlugins)) { $this->addCssToPageRenderer($request, $stylesFromPlugins, 'InlineDefaultCss'); } } if (is_array($typoScriptPageArray['includeCSS.'] ?? false)) { foreach ($typoScriptPageArray['includeCSS.'] as $key => $cssResource) { if (is_array($cssResource)) { continue; } $cssResourceConfig = $additionalAttributes = $typoScriptPageArray['includeCSS.'][$key . '.'] ?? []; if (isset($cssResourceConfig['if.']) && !$contentObjectRenderer->checkIf($cssResourceConfig['if.'])) { continue; } try { $cssResource = $this->systemResourceFactory->createResource($cssResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($cssResourceConfig['crossorigin'] ?? ''); $additionalAttributes = $this->cleanupAdditionalAttributeKeys($additionalAttributes, 'css'); $this->pageRenderer->addCssFile( $cssResource, ($cssResourceConfig['alternate'] ?? false) ? 'alternate stylesheet' : 'stylesheet', ($cssResourceConfig['media'] ?? false) ?: 'all', ($cssResourceConfig['title'] ?? false) ?: '', null, (bool)($cssResourceConfig['forceOnTop'] ?? false), $cssResourceConfig['allWrap'] ?? '', null, $cssResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($cssResourceConfig['inline'] ?? false), $additionalAttributes, $cssResourceConfig['integrity'] ?? '', $crossOrigin ); } } if (is_array($typoScriptPageArray['includeCSSLibs.'] ?? false)) { foreach ($typoScriptPageArray['includeCSSLibs.'] as $key => $cssResource) { if (is_array($cssResource)) { continue; } $cssResourceConfig = $additionalAttributes = $typoScriptPageArray['includeCSSLibs.'][$key . '.'] ?? []; if (isset($cssResourceConfig['if.']) && !$contentObjectRenderer->checkIf($cssResourceConfig['if.'])) { continue; } try { $cssResource = $this->systemResourceFactory->createResource($cssResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($cssResourceConfig['crossorigin'] ?? ''); $additionalAttributes = $this->cleanupAdditionalAttributeKeys($additionalAttributes, 'css'); $this->pageRenderer->addCssLibrary( $cssResource, ($cssResourceConfig['alternate'] ?? false) ? 'alternate stylesheet' : 'stylesheet', ($cssResourceConfig['media'] ?? false) ?: 'all', ($cssResourceConfig['title'] ?? false) ?: '', null, (bool)($cssResourceConfig['forceOnTop'] ?? false), $cssResourceConfig['allWrap'] ?? '', null, $cssResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($cssResourceConfig['inline'] ?? false), $additionalAttributes, $cssResourceConfig['integrity'] ?? '', $crossOrigin ); } } $style = $contentObjectRenderer->cObjGet($typoScriptPageArray['cssInline.'] ?? null, 'cssInline.'); if (trim($style)) { $this->addCssToPageRenderer($request, $style, 'additionalTSFEInlineStyle'); } // JavaScript includes if (is_array($typoScriptPageArray['includeJSLibs.'] ?? false)) { foreach ($typoScriptPageArray['includeJSLibs.'] as $key => $jsResource) { if (is_array($jsResource)) { continue; } $jsResourceConfig = $additionalAttributes = $typoScriptPageArray['includeJSLibs.'][$key . '.'] ?? []; if (isset($jsResourceConfig['if.']) && !$contentObjectRenderer->checkIf($jsResourceConfig['if.'])) { continue; } try { $jsResource = $this->systemResourceFactory->createResource($jsResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? ''); $additionalAttributes = $this->cleanupAdditionalAttributeKeys($additionalAttributes, 'js'); $this->pageRenderer->addJsLibrary( $key, $jsResource, $jsResourceConfig['type'] ?? null, null, (bool)($jsResourceConfig['forceOnTop'] ?? false), $jsResourceConfig['allWrap'] ?? '', null, $jsResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($jsResourceConfig['async'] ?? false), $jsResourceConfig['integrity'] ?? '', (bool)($jsResourceConfig['defer'] ?? false), $crossOrigin, (bool)($jsResourceConfig['nomodule'] ?? false), $additionalAttributes ); } } if (is_array($typoScriptPageArray['includeJSFooterlibs.'] ?? false)) { foreach ($typoScriptPageArray['includeJSFooterlibs.'] as $key => $jsResource) { if (is_array($jsResource)) { continue; } $jsResourceConfig = $additionalAttributes = $typoScriptPageArray['includeJSFooterlibs.'][$key . '.'] ?? []; if (isset($jsResourceConfig['if.']) && !$contentObjectRenderer->checkIf($jsResourceConfig['if.'])) { continue; } try { $jsResource = $this->systemResourceFactory->createResource($jsResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? ''); $additionalAttributes = $this->cleanupAdditionalAttributeKeys($additionalAttributes, 'js'); $this->pageRenderer->addJsFooterLibrary( $key, $jsResource, $jsResourceConfig['type'] ?? null, null, (bool)($jsResourceConfig['forceOnTop'] ?? false), $jsResourceConfig['allWrap'] ?? '', null, $jsResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($jsResourceConfig['async'] ?? false), $jsResourceConfig['integrity'] ?? '', (bool)($jsResourceConfig['defer'] ?? false), $crossOrigin, (bool)($jsResourceConfig['nomodule'] ?? false), $additionalAttributes ); } } if (is_array($typoScriptPageArray['includeJS.'] ?? false)) { foreach ($typoScriptPageArray['includeJS.'] as $key => $jsResource) { if (is_array($jsResource)) { continue; } $jsResourceConfig = $typoScriptPageArray['includeJS.'][$key . '.'] ?? []; if (isset($jsResourceConfig['if.']) && !$contentObjectRenderer->checkIf($jsResourceConfig['if.'])) { continue; } try { $jsResource = $this->systemResourceFactory->createResource($jsResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? ''); $this->pageRenderer->addJsFile( $jsResource, $jsResourceConfig['type'] ?? null, null, (bool)($jsResourceConfig['forceOnTop'] ?? false), $jsResourceConfig['allWrap'] ?? '', null, $jsResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($jsResourceConfig['async'] ?? false), $jsResourceConfig['integrity'] ?? '', (bool)($jsResourceConfig['defer'] ?? false), $crossOrigin, (bool)($jsResourceConfig['nomodule'] ?? false), // @todo: This does not use the same logic as with "additionalAttributes" above. Also not documented correctly. $jsResourceConfig['data.'] ?? [] ); } } if (is_array($typoScriptPageArray['includeJSFooter.'] ?? false)) { foreach ($typoScriptPageArray['includeJSFooter.'] as $key => $jsResource) { if (is_array($jsResource)) { continue; } $jsResourceConfig = $typoScriptPageArray['includeJSFooter.'][$key . '.'] ?? []; if (isset($jsResourceConfig['if.']) && !$contentObjectRenderer->checkIf($jsResourceConfig['if.'])) { continue; } try { $jsResource = $this->systemResourceFactory->createResource($jsResource); } catch (SystemResourceException) { continue; } $crossOrigin = (string)($jsResourceConfig['crossorigin'] ?? ''); $this->pageRenderer->addJsFooterFile( $jsResource, $jsResourceConfig['type'] ?? null, null, (bool)($jsResourceConfig['forceOnTop'] ?? false), $jsResourceConfig['allWrap'] ?? '', null, $jsResourceConfig['allWrap.']['splitChar'] ?? '|', (bool)($jsResourceConfig['async'] ?? false), $jsResourceConfig['integrity'] ?? '', (bool)($jsResourceConfig['defer'] ?? false), $crossOrigin, (bool)($jsResourceConfig['nomodule'] ?? false), // @todo: This does not use the same logic as with "additionalAttributes" above. Also not documented correctly. $jsResourceConfig['data.'] ?? [] ); } } // Header and footer data if (is_array($typoScriptPageArray['headerData.'] ?? false)) { $this->pageRenderer->addHeaderData($contentObjectRenderer->cObjGet($typoScriptPageArray['headerData.'], 'headerData.')); } if (is_array($typoScriptPageArray['footerData.'] ?? false)) { $this->pageRenderer->addFooterData($contentObjectRenderer->cObjGet($typoScriptPageArray['footerData.'], 'footerData.')); } $titleTagContent = $this->generatePageTitle($request); $this->pageRenderer->setTitle($titleTagContent); // @internal hook for EXT:seo, will be gone soon, do not use it in your own extensions $_params = ['request' => $request]; $_ref = null; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) { GeneralUtility::callUserFunction($_funcRef, $_params, $_ref); } $this->generateHrefLangTags($request); $this->generateMetaTagHtml($typoScriptPageArray['meta.'] ?? [], $contentObjectRenderer); // Javascript inline and inline footer code $inlineJS = implode(LF, $contentObjectRenderer->cObjGetSeparated($typoScriptPageArray['jsInline.'] ?? null, 'jsInline.')); $inlineFooterJs = implode(LF, $contentObjectRenderer->cObjGetSeparated($typoScriptPageArray['jsFooterInline.'] ?? null, 'jsFooterInline.')); if (($typoScriptConfigArray['removeDefaultJS'] ?? 'external') === 'external') { // "removeDefaultJS" is "external" by default // This keeps inlineJS from *_INT Objects from being moved to external files. // At this point in frontend rendering *_INT Objects only have placeholders instead // of actual content. Moving these placeholders to external files would break the JS file with // syntax errors due to the placeholders, and the needed JS would never get included to the page. // Therefore, inlineJS from *_INT Objects must not be moved to external files but kept internal. $inlineJSint = ''; $this->stripIntObjectPlaceholder($inlineJS, $inlineJSint); if ($inlineJSint) { $this->pageRenderer->addJsInlineCode('TS_inlineJSint', $inlineJSint); } if (trim($inlineJS)) { $this->pageRenderer->addJsFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineJS), null); } if ($inlineFooterJs) { $inlineFooterJSint = ''; $this->stripIntObjectPlaceholder($inlineFooterJs, $inlineFooterJSint); if ($inlineFooterJSint) { $this->pageRenderer->addJsFooterInlineCode('TS_inlineFooterJSint', $inlineFooterJSint); } $this->pageRenderer->addJsFooterFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineFooterJs), null); } } else { // Include only inlineJS if ($inlineJS) { $this->pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS); } if ($inlineFooterJs) { $this->pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs); } } if (is_array($typoScriptPageArray['inlineLanguageLabelFiles.'] ?? false)) { foreach ($typoScriptPageArray['inlineLanguageLabelFiles.'] as $key => $languageFile) { if (is_array($languageFile)) { continue; } $languageFileConfig = $typoScriptPageArray['inlineLanguageLabelFiles.'][$key . '.'] ?? []; if (isset($languageFileConfig['if.']) && !$contentObjectRenderer->checkIf($languageFileConfig['if.'])) { continue; } $this->pageRenderer->addInlineLanguageLabelFile( $languageFile, ($languageFileConfig['selectionPrefix'] ?? false) ? $languageFileConfig['selectionPrefix'] : '', ($languageFileConfig['stripFromSelectionName'] ?? false) ? $languageFileConfig['stripFromSelectionName'] : '' ); } } if (is_array($typoScriptPageArray['inlineSettings.'] ?? false)) { $this->pageRenderer->addInlineSettingArray('TS', $typoScriptPageArray['inlineSettings.']); } // Header complete, now the body tag is added so the regular content can be applied later-on if ($typoScriptConfigArray['disableBodyTag'] ?? false) { $this->pageRenderer->addBodyContent(LF); } else { $bodyTag = ''; if ($typoScriptPageArray['bodyTag'] ?? false) { $bodyTag = $typoScriptPageArray['bodyTag']; } elseif ($typoScriptPageArray['bodyTagCObject'] ?? false) { $bodyTag = $contentObjectRenderer->cObjGetSingle($typoScriptPageArray['bodyTagCObject'], $typoScriptPageArray['bodyTagCObject.'] ?? [], 'bodyTagCObject'); } if (trim($typoScriptPageArray['bodyTagAdd'] ?? '')) { $bodyTag = preg_replace('/>$/', '', trim($bodyTag)) . ' ' . trim($typoScriptPageArray['bodyTagAdd']) . '>'; } $this->pageRenderer->addBodyContent(LF . $bodyTag); } } /** * Searches for placeholder created from *_INT cObjects, removes them from * $searchString and merges them to $intObjects * * @param string $searchString The String which should be cleaned from int-object markers * @param string $intObjects The String the found int-placeholders are moved to (for further processing) */ protected function stripIntObjectPlaceholder(&$searchString, &$intObjects) { $tempArray = []; preg_match_all('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', $searchString, $tempArray); $searchString = (string)preg_replace('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', '', $searchString); $intObjects = implode('', $tempArray[0]); } /** * Generate meta tags from meta tag TypoScript * * @param array $metaTagTypoScript TypoScript configuration for meta tags */ protected function generateMetaTagHtml(array $metaTagTypoScript, ContentObjectRenderer $cObj) { $conf = $this->typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript); foreach ($conf as $key => $properties) { $replace = false; if (is_array($properties)) { $nodeValue = $properties['_typoScriptNodeValue'] ?? ''; $value = trim((string)$cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.'])); if ($value === '' && !empty($properties['value'])) { $value = $properties['value']; } } else { $value = $properties; } $attribute = 'name'; if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') { $attribute = 'http-equiv'; } if (is_array($properties) && !empty($properties['attribute'])) { $attribute = $properties['attribute']; } if (is_array($properties) && !empty($properties['replace'])) { $replace = true; } if (!is_array($value)) { $value = (array)$value; } foreach ($value as $subValue) { if (trim($subValue ?? '') !== '') { $this->pageRenderer->setMetaTag($attribute, $key, $subValue, [], $replace); } } } } /** * Adds inline CSS code, by respecting the inlineStyle2TempFile option * * @param string $cssStyles the inline CSS styling * @param string $inlineBlockName the block name to add it */ protected function addCssToPageRenderer(ServerRequestInterface $request, string $cssStyles, string $inlineBlockName): void { $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); // This option is enabled by default on purpose if (empty($typoScriptConfigArray['inlineStyle2TempFile'] ?? true)) { $this->pageRenderer->addCssInlineBlock($inlineBlockName, $cssStyles); } else { $this->pageRenderer->addCssFile('PKG:typo3/app:' . Environment::getRelativePublicPath() . GeneralUtility::writeStyleSheetContentToTemporaryFile($cssStyles)); } } /** * Generates the tag by evaluating TypoScript configuration, usually found via: * * - Adding extra attributes in addition to pre-generated ones (e.g. "dir") * config.htmlTag.attributes.no-js = 1 * config.htmlTag.attributes.empty-attribute = * * - Adding one full string (no stdWrap!) to the "" tag * config.htmlTag_setParams = string|"none" * * If config.htmlTag_setParams = none is set, even the pre-generated values are not added at all anymore. * * - "config.htmlTag_stdWrap" always applies over the whole compiled tag. * * @param array $htmlTagAttributes pre-generated attributes by doctype/direction etc. values. * @param array $configuration the TypoScript configuration "config." array * @param ContentObjectRenderer $cObj * @return string the full tag as string */ protected function generateHtmlTag(array $htmlTagAttributes, array $configuration, ContentObjectRenderer $cObj): string { if (is_array($configuration['htmlTag.']['attributes.'] ?? null)) { $attributeString = ''; foreach ($configuration['htmlTag.']['attributes.'] as $attributeName => $value) { if (str_ends_with($attributeName, '.')) { // Skip this one, but only if the default value is set if (isset($configuration['htmlTag.']['attributes.'][rtrim($attributeName, '.')])) { continue; } $attributeName = rtrim($attributeName, '.'); $value = ''; } if (is_array($configuration['htmlTag.']['attributes.'][$attributeName . '.'] ?? null)) { $value = $cObj->stdWrap($value, $configuration['htmlTag.']['attributes.'][$attributeName . '.']); } $attributeString .= ' ' . htmlspecialchars($attributeName) . ($value !== '' ? '="' . htmlspecialchars((string)$value) . '"' : ''); // If e.g. "htmlTag.attributes.dir" is set, make sure it is not added again with "implodeAttributes()" if (isset($htmlTagAttributes[$attributeName])) { unset($htmlTagAttributes[$attributeName]); } } $attributeString = ltrim(GeneralUtility::implodeAttributes($htmlTagAttributes) . $attributeString); } elseif (($configuration['htmlTag_setParams'] ?? '') === 'none') { $attributeString = ''; } elseif (isset($configuration['htmlTag_setParams'])) { $attributeString = $configuration['htmlTag_setParams']; } else { $attributeString = GeneralUtility::implodeAttributes($htmlTagAttributes); } $htmlTag = ''; if (isset($configuration['htmlTag_stdWrap.'])) { $htmlTag = $cObj->stdWrap($htmlTag, $configuration['htmlTag_stdWrap.']); } return $htmlTag; } protected function generateHrefLangTags(ServerRequestInterface $request): void { $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); if ($typoScriptConfigArray['disableHrefLang'] ?? false) { return; } $endingSlash = DocType::createFromConfigurationKey($typoScriptConfigArray['doctype'] ?? '')->isXmlCompliant() ? '/' : ''; $hrefLangs = $this->eventDispatcher->dispatch(new ModifyHrefLangTagsEvent($request))->getHrefLangs(); if (count($hrefLangs) > 1) { $data = []; foreach ($hrefLangs as $hrefLang => $href) { $data[] = sprintf('', GeneralUtility::implodeAttributes([ 'rel' => 'alternate', 'hreflang' => $hrefLang, 'href' => $href, ], true), $endingSlash); } $this->pageRenderer->addHeaderData(implode(LF, $data)); } } /** * Include the preview block in case we're looking at a hidden page in the LIVE workspace */ protected function displayPreviewInfoMessage(ServerRequestInterface $request, string $content): string { $isInWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline', false); $isInPreviewMode = $this->context->hasAspect('frontend.preview') && $this->context->getPropertyFromAspect('frontend.preview', 'isPreview'); $typoScriptConfigArray = $request->getAttribute('frontend.typoscript')->getConfigArray(); if (!$isInPreviewMode || $isInWorkspace || ($typoScriptConfigArray['disablePreviewNotification'] ?? false)) { return $content; } if ($typoScriptConfigArray['message_preview'] ?? '') { $message = $typoScriptConfigArray['message_preview']; } else { $label = $this->getLanguageService()->translate('preview', 'frontend.general'); $styles = []; $styles[] = 'position: fixed'; $styles[] = 'top: 15px'; $styles[] = 'right: 15px'; $styles[] = 'padding: 8px 18px'; $styles[] = 'background: #fff3cd'; $styles[] = 'border: 1px solid #ffeeba'; $styles[] = 'font-family: sans-serif'; $styles[] = 'font-size: .875em'; $styles[] = 'font-weight: bold'; $styles[] = 'color: #856404'; $styles[] = 'z-index: 20000'; $styles[] = 'user-select: none'; $styles[] = 'pointer-events: none'; $styles[] = 'text-align: center'; $styles[] = 'border-radius: 2px'; $message = '
' . htmlspecialchars($label) . '
'; } if (!empty($message)) { $content = str_ireplace('', $message . '', $content); } return $content; } /** * Filter out known TypoScript attributes so that they are NOT passed along * to a or tag as additional attributes. * NOTE: Some keys are unset here even though they are valid attributes to * the or