, done via page.10, page.20 etc. * - Everything around. * * If the content has been built together within the cache (cache_pages), it is fetched directly, and * any so-called "uncached" content is generated again. * * Some further hooks allow to post-processing the content. * * Then the right HTTP response headers are compiled together and sent as well. */ class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface { /** * Instance of the timetracker * @var TimeTracker */ protected $timeTracker; /** * Handles a frontend request * * @param ServerRequestInterface $request * @return ResponseInterface */ public function handleRequest(ServerRequestInterface $request): ResponseInterface { return $this->handle($request); } /** * Puts parameters that have been added or removed from the global _GET or _POST arrays * into the given request (however, the PSR-7 request information takes precedence). * * @param ServerRequestInterface $request * @return ServerRequestInterface */ protected function addModifiedGlobalsToIncomingRequest(ServerRequestInterface $request): ServerRequestInterface { $originalGetParameters = $request->getAttribute('_originalGetParameters', null); if ($originalGetParameters !== null && !empty($_GET) && $_GET !== $originalGetParameters) { // Find out what has been changed. $modifiedGetParameters = ArrayUtility::arrayDiffAssocRecursive($_GET ?? [], $originalGetParameters); if (!empty($modifiedGetParameters)) { $queryParams = array_replace_recursive($modifiedGetParameters, $request->getQueryParams()); $request = $request->withQueryParams($queryParams); $GLOBALS['TYPO3_REQUEST'] = $request; $this->timeTracker->setTSlogMessage('GET parameters have been modified during Request building in a hook.'); } } // do same for $_POST if the request is a POST request $originalPostParameters = $request->getAttribute('_originalPostParameters', null); if ($request->getMethod() === 'POST' && $originalPostParameters !== null && !empty($_POST) && $_POST !== $originalPostParameters) { // Find out what has been changed $modifiedPostParameters = ArrayUtility::arrayDiffAssocRecursive($_POST ?? [], $originalPostParameters); if (!empty($modifiedPostParameters)) { $parsedBody = array_replace_recursive($modifiedPostParameters, $request->getParsedBody()); $request = $request->withParsedBody($parsedBody); $GLOBALS['TYPO3_REQUEST'] = $request; $this->timeTracker->setTSlogMessage('POST parameters have been modified during Request building in a hook.'); } } return $request; } /** * Sets the global GET and POST to the values, so if people access $_GET and $_POST * Within hooks starting NOW (e.g. cObject), they get the "enriched" data from query params. * * This needs to be run after the request object has been enriched with modified GET/POST variables. * * @param ServerRequestInterface $request * @internal this safety net will be removed in TYPO3 v10.0. */ protected function resetGlobalsToCurrentRequest(ServerRequestInterface $request) { if ($request->getQueryParams() !== $_GET) { $queryParams = $request->getQueryParams(); $_GET = $queryParams; $GLOBALS['HTTP_GET_VARS'] = $_GET; } if ($request->getMethod() === 'POST') { $parsedBody = $request->getParsedBody(); if (is_array($parsedBody) && $parsedBody !== $_POST) { $_POST = $parsedBody; $GLOBALS['HTTP_POST_VARS'] = $_POST; } } } /** * Handles a frontend request, after finishing running middlewares * * @param ServerRequestInterface $request * @return ResponseInterface|null */ public function handle(ServerRequestInterface $request): ResponseInterface { // Fetch the initialized time tracker object $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class); /** @var TypoScriptFrontendController $controller */ $controller = $GLOBALS['TSFE']; // safety net, will be removed in TYPO3 v10.0. Aligns $_GET/$_POST to the incoming request. $request = $this->addModifiedGlobalsToIncomingRequest($request); $this->resetGlobalsToCurrentRequest($request); // Generate page if ($controller->isGeneratePage()) { $this->timeTracker->push('Page generation'); $controller->generatePage_preProcessing(); $controller->preparePageContentGeneration($request); // Content generation $this->timeTracker->incStackPointer(); $this->timeTracker->push($controller->sPre, 'PAGE'); // If 'disableAllHeaderCode' is set, all the header-code is discarded if ($controller->config['config']['disableAllHeaderCode'] ?? false) { $controller->content = $this->generatePageContent($controller); } else { $controller->content = $this->generatePageContentWithHeader($controller, $request->getAttribute('language', null)); } $this->timeTracker->pull($this->timeTracker->LR ? $controller->content : ''); $this->timeTracker->decStackPointer(); $controller->setAbsRefPrefix(); $controller->generatePage_postProcessing(); $this->timeTracker->pull(); } $controller->releaseLocks(); // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation if ($controller->isINTincScript()) { if (!$controller->isGeneratePage()) { // When page was generated, this was already called. Avoid calling this twice. $controller->preparePageContentGeneration($request); } $this->timeTracker->push('Non-cached objects'); $controller->INTincScript(); $this->timeTracker->pull(); } // Create a Response object when sending content $response = new Response(); // Output content $isOutputting = $controller->isOutputting(); if ($isOutputting) { $this->timeTracker->push('Print Content'); $response = $controller->applyHttpHeadersToResponse($response); $controller->processContentForOutput(); $this->timeTracker->pull(); } // Store session data for fe_users $controller->fe_user->storeSessionData(); // @deprecated since TYPO3 v9.3, will be removed in TYPO3 v10.0. $redirectResponse = $controller->redirectToExternalUrl(true); if ($redirectResponse instanceof ResponseInterface) { $controller->sendHttpHeadersDirectly(); return $redirectResponse; } // Statistics $GLOBALS['TYPO3_MISC']['microtime_end'] = microtime(true); if ($isOutputting && ($controller->config['config']['debug'] ?? !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']))) { $response = $response->withHeader('X-TYPO3-Parsetime', $this->timeTracker->getParseTime() . 'ms'); } // Preview info // @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. $controller->previewInfo(true); // Hook for "end-of-frontend" $_params = ['pObj' => &$controller]; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_eofe'] ?? [] as $_funcRef) { GeneralUtility::callUserFunction($_funcRef, $_params, $controller); } // Finish time tracking (started in TYPO3\CMS\Frontend\Middleware\TimeTrackerInitialization) $this->timeTracker->pull(); if ($isOutputting) { $response->getBody()->write($controller->content); } return $isOutputting ? $response : new NullResponse(); } /** * Generates the main content part within tags (except JS files/CSS files). * * @param TypoScriptFrontendController $controller * @return string */ protected function generatePageContent(TypoScriptFrontendController $controller): string { $pageContent = $controller->cObj->cObjGet($controller->pSetup) ?: ''; if ($controller->pSetup['wrap'] ?? false) { $pageContent = $controller->cObj->wrap($pageContent, $controller->pSetup['wrap']); } if ($controller->pSetup['stdWrap.'] ?? false) { $pageContent = $controller->cObj->stdWrap($pageContent, $controller->pSetup['stdWrap.']); } return $pageContent; } /** * Rendering normal HTML-page with header by wrapping the generated content ($pageContent) in body-tags and setting the header accordingly. * Render HTML page with header parts ( tag content and wrap around tag) - this is done * after the "main" page Content, since some JS may be inserted at that point. * * @param TypoScriptFrontendController $controller * @param SiteLanguage|null $siteLanguage * @return string */ protected function generatePageContentWithHeader(TypoScriptFrontendController $controller, ?SiteLanguage $siteLanguage): string { // Generate the page content, this has to be first, as some additional TSFE-related code could have been written $pageContent = $this->generatePageContent($controller); $pageRenderer = $this->getPageRenderer(); if ($controller->config['config']['moveJsFromHeaderToFooter'] ?? false) { $pageRenderer->enableMoveJsFromHeaderToFooter(); } if ($controller->config['config']['pageRendererTemplateFile'] ?? false) { try { $file = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($controller->config['config']['pageRendererTemplateFile']); $pageRenderer->setTemplateFile($file); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { // do nothing } } $headerComment = trim($controller->config['config']['headerComment'] ?? ''); if ($headerComment) { $pageRenderer->addInlineComment("\t" . str_replace(LF, LF . "\t", $headerComment) . LF); } // Setting charset: $theCharset = $controller->metaCharset; // Reset the content variables: $controller->content = ''; $htmlTagAttributes = []; $htmlLang = $controller->config['config']['htmlTag_langKey'] ?? ($controller->sys_language_isocode ?: 'en'); $direction = $controller->config['config']['htmlTag_dir'] ?? null; if ($siteLanguage !== null) { $direction = $siteLanguage->getDirection(); $htmlLang = $siteLanguage->getHreflang(); } if ($direction) { $htmlTagAttributes['dir'] = htmlspecialchars($direction); } // Setting document type: $docTypeParts = []; $xmlDocument = true; // Part 1: XML prologue switch ((string)($controller->config['config']['xmlprologue'] ?? '')) { case 'none': $xmlDocument = false; break; case 'xml_10': $docTypeParts[] = ''; break; case 'xml_11': $docTypeParts[] = ''; break; case '': if ($controller->xhtmlVersion) { $docTypeParts[] = ''; } else { $xmlDocument = false; } break; default: $docTypeParts[] = $controller->config['config']['xmlprologue']; } // Part 2: DTD $doctype = $controller->config['config']['doctype'] ?? null; if ($doctype) { switch ($doctype) { case 'xhtml_trans': $docTypeParts[] = ''; break; case 'xhtml_strict': $docTypeParts[] = ''; break; case 'xhtml_basic': $docTypeParts[] = ''; break; case 'xhtml_11': $docTypeParts[] = ''; break; case 'xhtml+rdfa_10': $docTypeParts[] = ''; break; case 'html5': $docTypeParts[] = ''; if ($xmlDocument) { $pageRenderer->setMetaCharsetTag(''); } else { $pageRenderer->setMetaCharsetTag(''); } break; case 'none': break; default: $docTypeParts[] = $doctype; } } else { $docTypeParts[] = ''; if ($xmlDocument) { $pageRenderer->setMetaCharsetTag(''); } else { $pageRenderer->setMetaCharsetTag(''); } } if ($controller->xhtmlVersion) { $htmlTagAttributes['xml:lang'] = $htmlLang; } if ($controller->xhtmlVersion < 110 || $doctype === 'html5') { $htmlTagAttributes['lang'] = $htmlLang; } if ($controller->xhtmlVersion || $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($controller->config['config']['namespaces.'])) { foreach ($controller->config['config']['namespaces.'] as $prefix => $uri) { // $uri gets htmlspecialchared later $htmlTagAttributes['xmlns:' . htmlspecialchars($prefix)] = $uri; } } } // Swap XML and doctype order around (for MSIE / Opera standards compliance) if ($controller->config['config']['doctypeSwitch'] ?? false) { $docTypeParts = array_reverse($docTypeParts); } // Adding doctype parts: if (!empty($docTypeParts)) { $pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts)); } // Begin header section: $htmlTag = $this->generateHtmlTag($htmlTagAttributes, $controller->config['config'] ?? [], $controller->cObj); $pageRenderer->setHtmlTag($htmlTag); // Head tag: $headTag = $controller->pSetup['headTag'] ?? ''; if (isset($controller->pSetup['headTag.'])) { $headTag = $controller->cObj->stdWrap($headTag, $controller->pSetup['headTag.']); } $pageRenderer->setHeadTag($headTag); // Setting charset meta tag: $pageRenderer->setCharSet($theCharset); $pageRenderer->addInlineComment(' This website is powered by TYPO3 - inspiring people to share! TYPO3 is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL. TYPO3 is copyright ' . TYPO3_copyright_year . ' of Kasper Skaarhoj. Extensions are copyright of their respective owners. Information and contribution at ' . TYPO3_URL_GENERAL . ' '); if ($controller->baseUrl) { $pageRenderer->setBaseUrl($controller->baseUrl); } if ($controller->pSetup['shortcutIcon'] ?? false) { try { $favIcon = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($controller->pSetup['shortcutIcon']); $iconFileInfo = GeneralUtility::makeInstance(ImageInfo::class, Environment::getPublicPath() . '/' . $favIcon); if ($iconFileInfo->isFile()) { $iconMimeType = $iconFileInfo->getMimeType(); if ($iconMimeType) { $iconMimeType = ' type="' . $iconMimeType . '"'; $pageRenderer->setIconMimeType($iconMimeType); } $pageRenderer->setFavIcon(PathUtility::getAbsoluteWebPath($controller->absRefPrefix . $favIcon)); } } catch (\TYPO3\CMS\Core\Resource\Exception $e) { // do nothing } } // Including CSS files if (isset($controller->tmpl->setup['plugin.']) && is_array($controller->tmpl->setup['plugin.'])) { $stylesFromPlugins = ''; foreach ($controller->tmpl->setup['plugin.'] as $key => $iCSScode) { if (is_array($iCSScode)) { if ($iCSScode['_CSS_DEFAULT_STYLE'] && empty($controller->config['config']['removeDefaultCss'])) { if (isset($iCSScode['_CSS_DEFAULT_STYLE.'])) { $cssDefaultStyle = $controller->cObj->stdWrap($iCSScode['_CSS_DEFAULT_STYLE'], $iCSScode['_CSS_DEFAULT_STYLE.']); } else { $cssDefaultStyle = $iCSScode['_CSS_DEFAULT_STYLE']; } $stylesFromPlugins .= '/* default styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssDefaultStyle . LF; } if ($iCSScode['_CSS_PAGE_STYLE'] && empty($controller->config['config']['removePageCss'])) { $cssPageStyle = implode(LF, $iCSScode['_CSS_PAGE_STYLE']); if (isset($iCSScode['_CSS_PAGE_STYLE.'])) { $cssPageStyle = $controller->cObj->stdWrap($cssPageStyle, $iCSScode['_CSS_PAGE_STYLE.']); } $cssPageStyle = '/* specific page styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssPageStyle; $this->addCssToPageRenderer($controller, $cssPageStyle, true, 'InlinePageCss'); } } } if (!empty($stylesFromPlugins)) { $this->addCssToPageRenderer($controller, $stylesFromPlugins, false, 'InlineDefaultCss'); } } /**********************************************************************/ /* config.includeCSS / config.includeCSSLibs /**********************************************************************/ if (isset($controller->pSetup['includeCSS.']) && is_array($controller->pSetup['includeCSS.'])) { foreach ($controller->pSetup['includeCSS.'] as $key => $CSSfile) { if (!is_array($CSSfile)) { $cssFileConfig = &$controller->pSetup['includeCSS.'][$key . '.']; if (isset($cssFileConfig['if.']) && !$controller->cObj->checkIf($cssFileConfig['if.'])) { continue; } if ($cssFileConfig['external']) { $ss = $CSSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($CSSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { if ($cssFileConfig['import']) { if (!$cssFileConfig['external'] && $ss[0] !== '/') { // To fix MSIE 6 that cannot handle these as relative paths (according to Ben v Ende) $ss = GeneralUtility::dirname(GeneralUtility::getIndpEnv('SCRIPT_NAME')) . '/' . $ss; } $cssMedia = !empty($cssFileConfig['media']) ? ' ' . htmlspecialchars($cssFileConfig['media']) : ''; $pageRenderer->addCssInlineBlock('import_' . $key, '@import url("' . htmlspecialchars($ss) . '")' . $cssMedia . ';', empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop']); } else { $pageRenderer->addCssFile( $ss, $cssFileConfig['alternate'] ? 'alternate stylesheet' : 'stylesheet', $cssFileConfig['media'] ?: 'all', $cssFileConfig['title'] ?: '', $cssFileConfig['external'] || (bool)$cssFileConfig['inline'] ? false : empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop'], $cssFileConfig['allWrap'], (bool)$cssFileConfig['excludeFromConcatenation'] || (bool)$cssFileConfig['inline'], $cssFileConfig['allWrap.']['splitChar'], $cssFileConfig['inline'] ); unset($cssFileConfig); } } } } } if (isset($controller->pSetup['includeCSSLibs.']) && is_array($controller->pSetup['includeCSSLibs.'])) { foreach ($controller->pSetup['includeCSSLibs.'] as $key => $CSSfile) { if (!is_array($CSSfile)) { $cssFileConfig = &$controller->pSetup['includeCSSLibs.'][$key . '.']; if (isset($cssFileConfig['if.']) && !$controller->cObj->checkIf($cssFileConfig['if.'])) { continue; } if ($cssFileConfig['external']) { $ss = $CSSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($CSSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { if ($cssFileConfig['import']) { if (!$cssFileConfig['external'] && $ss[0] !== '/') { // To fix MSIE 6 that cannot handle these as relative paths (according to Ben v Ende) $ss = GeneralUtility::dirname(GeneralUtility::getIndpEnv('SCRIPT_NAME')) . '/' . $ss; } $cssMedia = !empty($cssFileConfig['media']) ? ' ' . htmlspecialchars($cssFileConfig['media']) : ''; $pageRenderer->addCssInlineBlock('import_' . $key, '@import url("' . htmlspecialchars($ss) . '")' . $cssMedia . ';', empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop']); } else { $pageRenderer->addCssLibrary( $ss, $cssFileConfig['alternate'] ? 'alternate stylesheet' : 'stylesheet', $cssFileConfig['media'] ?: 'all', $cssFileConfig['title'] ?: '', $cssFileConfig['external'] || (bool)$cssFileConfig['inline'] ? false : empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop'], $cssFileConfig['allWrap'], (bool)$cssFileConfig['excludeFromConcatenation'] || (bool)$cssFileConfig['inline'], $cssFileConfig['allWrap.']['splitChar'], $cssFileConfig['inline'] ); unset($cssFileConfig); } } } } } // CSS_inlineStyle from TS $style = trim($controller->pSetup['CSS_inlineStyle'] ?? ''); $style .= $controller->cObj->cObjGet($controller->pSetup['cssInline.'] ?? null, 'cssInline.'); if (trim($style)) { $this->addCssToPageRenderer($controller, $style, true, 'additionalTSFEInlineStyle'); } // Javascript Libraries if (isset($controller->pSetup['javascriptLibs.']) && is_array($controller->pSetup['javascriptLibs.'])) { // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, the setting page.javascriptLibs has been deprecated and will be removed in TYPO3 v10.0. trigger_error('The setting page.javascriptLibs will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); // Include jQuery into the page renderer if (!empty($controller->pSetup['javascriptLibs.']['jQuery'])) { // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, the setting page.javascriptLibs.jQuery has been deprecated and will be removed in TYPO3 v10.0. trigger_error('The setting page.javascriptLibs.jQuery will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); $jQueryTS = $controller->pSetup['javascriptLibs.']['jQuery.']; // Check if version / source is set, if not set variable to "NULL" to use the default of the page renderer $version = $jQueryTS['version'] ?? null; $source = $jQueryTS['source'] ?? null; // When "noConflict" is not set or "1" enable the default jQuery noConflict mode, otherwise disable the namespace if (!isset($jQueryTS['noConflict']) || !empty($jQueryTS['noConflict'])) { $namespace = 'noConflict'; } else { $namespace = PageRenderer::JQUERY_NAMESPACE_NONE; } $pageRenderer->loadJquery($version, $source, $namespace, true); } } // JavaScript library files if (isset($controller->pSetup['includeJSLibs.']) && is_array($controller->pSetup['includeJSLibs.'])) { foreach ($controller->pSetup['includeJSLibs.'] as $key => $JSfile) { if (!is_array($JSfile)) { if (isset($controller->pSetup['includeJSLibs.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSLibs.'][$key . '.']['if.'])) { continue; } if ($controller->pSetup['includeJSLibs.'][$key . '.']['external']) { $ss = $JSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { $jsFileConfig = &$controller->pSetup['includeJSLibs.'][$key . '.']; $type = $jsFileConfig['type']; if (!$type) { $type = 'text/javascript'; } $crossOrigin = $jsFileConfig['crossorigin']; if (!$crossOrigin && $jsFileConfig['integrity'] && $jsFileConfig['external']) { $crossOrigin = 'anonymous'; } $pageRenderer->addJsLibrary( $key, $ss, $type, $jsFileConfig['external'] ? false : empty($jsFileConfig['disableCompression']), (bool)$jsFileConfig['forceOnTop'], $jsFileConfig['allWrap'], (bool)$jsFileConfig['excludeFromConcatenation'], $jsFileConfig['allWrap.']['splitChar'], (bool)$jsFileConfig['async'], $jsFileConfig['integrity'], (bool)$jsFileConfig['defer'], $crossOrigin ); unset($jsFileConfig); } } } } if (isset($controller->pSetup['includeJSFooterlibs.']) && is_array($controller->pSetup['includeJSFooterlibs.'])) { foreach ($controller->pSetup['includeJSFooterlibs.'] as $key => $JSfile) { if (!is_array($JSfile)) { if (isset($controller->pSetup['includeJSFooterlibs.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSFooterlibs.'][$key . '.']['if.'])) { continue; } if ($controller->pSetup['includeJSFooterlibs.'][$key . '.']['external']) { $ss = $JSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { $jsFileConfig = &$controller->pSetup['includeJSFooterlibs.'][$key . '.']; $type = $jsFileConfig['type']; if (!$type) { $type = 'text/javascript'; } $crossorigin = $jsFileConfig['crossorigin']; if (!$crossorigin && $jsFileConfig['integrity'] && $jsFileConfig['external']) { $crossorigin = 'anonymous'; } $pageRenderer->addJsFooterLibrary( $key, $ss, $type, $jsFileConfig['external'] ? false : empty($jsFileConfig['disableCompression']), (bool)$jsFileConfig['forceOnTop'], $jsFileConfig['allWrap'], (bool)$jsFileConfig['excludeFromConcatenation'], $jsFileConfig['allWrap.']['splitChar'], (bool)$jsFileConfig['async'], $jsFileConfig['integrity'], (bool)$jsFileConfig['defer'], $crossorigin ); unset($jsFileConfig); } } } } // JavaScript files if (isset($controller->pSetup['includeJS.']) && is_array($controller->pSetup['includeJS.'])) { foreach ($controller->pSetup['includeJS.'] as $key => $JSfile) { if (!is_array($JSfile)) { if (isset($controller->pSetup['includeJS.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJS.'][$key . '.']['if.'])) { continue; } if ($controller->pSetup['includeJS.'][$key . '.']['external']) { $ss = $JSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { $jsConfig = &$controller->pSetup['includeJS.'][$key . '.']; $type = $jsConfig['type']; if (!$type) { $type = 'text/javascript'; } $crossorigin = $jsConfig['crossorigin']; if (!$crossorigin && $jsConfig['integrity'] && $jsConfig['external']) { $crossorigin = 'anonymous'; } $pageRenderer->addJsFile( $ss, $type, $jsConfig['external'] ? false : empty($jsConfig['disableCompression']), (bool)$jsConfig['forceOnTop'], $jsConfig['allWrap'], (bool)$jsConfig['excludeFromConcatenation'], $jsConfig['allWrap.']['splitChar'], (bool)$jsConfig['async'], $jsConfig['integrity'], (bool)$jsConfig['defer'], $crossorigin ); unset($jsConfig); } } } } if (isset($controller->pSetup['includeJSFooter.']) && is_array($controller->pSetup['includeJSFooter.'])) { foreach ($controller->pSetup['includeJSFooter.'] as $key => $JSfile) { if (!is_array($JSfile)) { if (isset($controller->pSetup['includeJSFooter.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSFooter.'][$key . '.']['if.'])) { continue; } if ($controller->pSetup['includeJSFooter.'][$key . '.']['external']) { $ss = $JSfile; } else { try { $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile); } catch (\TYPO3\CMS\Core\Resource\Exception $e) { $ss = null; } } if ($ss) { $jsConfig = &$controller->pSetup['includeJSFooter.'][$key . '.']; $type = $jsConfig['type']; if (!$type) { $type = 'text/javascript'; } $crossorigin = $jsConfig['crossorigin']; if (!$crossorigin && $jsConfig['integrity'] && $jsConfig['external']) { $crossorigin = 'anonymous'; } $pageRenderer->addJsFooterFile( $ss, $type, $jsConfig['external'] ? false : empty($jsConfig['disableCompression']), (bool)$jsConfig['forceOnTop'], $jsConfig['allWrap'], (bool)$jsConfig['excludeFromConcatenation'], $jsConfig['allWrap.']['splitChar'], (bool)$jsConfig['async'], $jsConfig['integrity'], (bool)$jsConfig['defer'], $crossorigin ); unset($jsConfig); } } } } // Headerdata if (isset($controller->pSetup['headerData.']) && is_array($controller->pSetup['headerData.'])) { $pageRenderer->addHeaderData($controller->cObj->cObjGet($controller->pSetup['headerData.'], 'headerData.')); } // Footerdata if (isset($controller->pSetup['footerData.']) && is_array($controller->pSetup['footerData.'])) { $pageRenderer->addFooterData($controller->cObj->cObjGet($controller->pSetup['footerData.'], 'footerData.')); } $controller->generatePageTitle(); // @internal hook for EXT:seo, will be gone soon, do not use it in your own extensions $_params = ['page' => $controller->page]; $_ref = ''; foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) { GeneralUtility::callUserFunction($_funcRef, $_params, $_ref); } $this->generateMetaTagHtml( $controller->pSetup['meta.'] ?? [], $controller->cObj ); unset($controller->additionalHeaderData['JSCode']); if (isset($controller->config['INTincScript']) && is_array($controller->config['INTincScript'])) { $controller->additionalHeaderData['JSCode'] = $controller->JSCode; // Storing the JSCode vars... $controller->config['INTincScript_ext']['divKey'] = $controller->uniqueHash(); $controller->config['INTincScript_ext']['additionalHeaderData'] = $controller->additionalHeaderData; // Storing the header-data array $controller->config['INTincScript_ext']['additionalFooterData'] = $controller->additionalFooterData; // Storing the footer-data array $controller->config['INTincScript_ext']['additionalJavaScript'] = $controller->additionalJavaScript; // Storing the JS-data array $controller->config['INTincScript_ext']['additionalCSS'] = $controller->additionalCSS; // Storing the Style-data array $controller->additionalHeaderData = ['']; // Clearing the array $controller->additionalFooterData = ['']; // Clearing the array $controller->divSection .= ''; } else { $controller->INTincScript_loadJSCode(); } $scriptJsCode = ''; if ($controller->spamProtectEmailAddresses && $controller->spamProtectEmailAddresses !== 'ascii') { $scriptJsCode = ' /* decrypt helper function */ function decryptCharcode(n,start,end,offset) { n = n + offset; if (offset > 0 && n > end) { n = start + (n - end - 1); } else if (offset < 0 && n < start) { n = end - (start - n - 1); } return String.fromCharCode(n); } /* decrypt string */ function decryptString(enc,offset) { var dec = ""; var len = enc.length; for(var i=0; i < len; i++) { var n = enc.charCodeAt(i); if (n >= 0x2B && n <= 0x3A) { dec += decryptCharcode(n,0x2B,0x3A,offset); /* 0-9 . , - + / : */ } else if (n >= 0x40 && n <= 0x5A) { dec += decryptCharcode(n,0x40,0x5A,offset); /* A-Z @ */ } else if (n >= 0x61 && n <= 0x7A) { dec += decryptCharcode(n,0x61,0x7A,offset); /* a-z */ } else { dec += enc.charAt(i); } } return dec; } /* decrypt spam-protected emails */ function linkTo_UnCryptMailto(s) { location.href = decryptString(s,' . $controller->spamProtectEmailAddresses * -1 . '); } '; } // Add inline JS $inlineJS = ''; // defined in php if (is_array($controller->inlineJS)) { foreach ($controller->inlineJS as $key => $val) { if (!is_array($val)) { $inlineJS .= LF . $val . LF; } } } // defined in TS with page.inlineJS // Javascript inline code $inline = $controller->cObj->cObjGet($controller->pSetup['jsInline.'] ?? null, 'jsInline.'); if ($inline) { $inlineJS .= LF . $inline . LF; } // Javascript inline code for Footer $inlineFooterJs = $controller->cObj->cObjGet($controller->pSetup['jsFooterInline.'] ?? null, 'jsFooterInline.'); // Should minify? if ($controller->config['config']['compressJs'] ?? false) { $pageRenderer->enableCompressJavascript(); $minifyErrorScript = ($minifyErrorInline = ''); $scriptJsCode = GeneralUtility::minifyJavaScript($scriptJsCode, $minifyErrorScript); if ($minifyErrorScript) { $this->timeTracker->setTSlogMessage($minifyErrorScript, 3); } if ($inlineJS) { $inlineJS = GeneralUtility::minifyJavaScript($inlineJS, $minifyErrorInline); if ($minifyErrorInline) { $this->timeTracker->setTSlogMessage($minifyErrorInline, 3); } } if ($inlineFooterJs) { $inlineFooterJs = GeneralUtility::minifyJavaScript($inlineFooterJs, $minifyErrorInline); if ($minifyErrorInline) { $this->timeTracker->setTSlogMessage($minifyErrorInline, 3); } } } if (!isset($controller->config['config']['removeDefaultJS']) || !$controller->config['config']['removeDefaultJS']) { // include default and inlineJS if ($scriptJsCode) { $pageRenderer->addJsInlineCode('_scriptCode', $scriptJsCode, $controller->config['config']['compressJs']); } if ($inlineJS) { $pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS, $controller->config['config']['compressJs']); } if ($inlineFooterJs) { $pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs, $controller->config['config']['compressJs']); } } elseif ($controller->config['config']['removeDefaultJS'] === 'external') { /* * 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 so moving these placeholders to external files would * a) break the JS file (syntax errors due to the placeholders) * b) 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) { $pageRenderer->addJsInlineCode('TS_inlineJSint', $inlineJSint, $controller->config['config']['compressJs']); } if (trim($scriptJsCode . $inlineJS)) { $pageRenderer->addJsFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($scriptJsCode . $inlineJS), 'text/javascript', $controller->config['config']['compressJs']); } if ($inlineFooterJs) { $inlineFooterJSint = ''; $this->stripIntObjectPlaceholder($inlineFooterJs, $inlineFooterJSint); if ($inlineFooterJSint) { $pageRenderer->addJsFooterInlineCode('TS_inlineFooterJSint', $inlineFooterJSint, $controller->config['config']['compressJs']); } $pageRenderer->addJsFooterFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineFooterJs), 'text/javascript', $controller->config['config']['compressJs']); } } else { // Include only inlineJS if ($inlineJS) { $pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS, $controller->config['config']['compressJs']); } if ($inlineFooterJs) { $pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs, $controller->config['config']['compressJs']); } } if (isset($controller->pSetup['inlineLanguageLabelFiles.']) && is_array($controller->pSetup['inlineLanguageLabelFiles.'])) { foreach ($controller->pSetup['inlineLanguageLabelFiles.'] as $key => $languageFile) { if (is_array($languageFile)) { continue; } $languageFileConfig = &$controller->pSetup['inlineLanguageLabelFiles.'][$key . '.']; if (isset($languageFileConfig['if.']) && !$controller->cObj->checkIf($languageFileConfig['if.'])) { continue; } $pageRenderer->addInlineLanguageLabelFile( $languageFile, $languageFileConfig['selectionPrefix'] ?: '', $languageFileConfig['stripFromSelectionName'] ?: '' ); } } if (isset($controller->pSetup['inlineSettings.']) && is_array($controller->pSetup['inlineSettings.'])) { $pageRenderer->addInlineSettingArray('TS', $controller->pSetup['inlineSettings.']); } // Compression and concatenate settings if ($controller->config['config']['compressCss'] ?? false) { $pageRenderer->enableCompressCss(); } if ($controller->config['config']['compressJs'] ?? false) { $pageRenderer->enableCompressJavascript(); } if ($controller->config['config']['concatenateCss'] ?? false) { $pageRenderer->enableConcatenateCss(); } if ($controller->config['config']['concatenateJs'] ?? false) { $pageRenderer->enableConcatenateJavascript(); } // Backward compatibility for old configuration // @deprecated - remove this option in TYPO3 v10.0. if ($controller->config['config']['concatenateJsAndCss'] ?? false) { trigger_error('Setting config.concatenateJsAndCss is deprecated in favor of config.concatenateJs and config.concatenateCss, and will have no effect anymore in TYPO3 v10.0.', E_USER_DEPRECATED); $pageRenderer->enableConcatenateCss(); $pageRenderer->enableConcatenateJavascript(); } // Add header data block if ($controller->additionalHeaderData) { $pageRenderer->addHeaderData(implode(LF, $controller->additionalHeaderData)); } // Add footer data block if ($controller->additionalFooterData) { $pageRenderer->addFooterData(implode(LF, $controller->additionalFooterData)); } // Header complete, now add content // Bodytag: if ($controller->config['config']['disableBodyTag'] ?? false) { $bodyTag = ''; } else { $defBT = (isset($controller->pSetup['bodyTagCObject']) && $controller->pSetup['bodyTagCObject']) ? $controller->cObj->cObjGetSingle($controller->pSetup['bodyTagCObject'], $controller->pSetup['bodyTagCObject.'], 'bodyTagCObject') : ''; $bodyTag = (isset($controller->pSetup['bodyTag']) && $controller->pSetup['bodyTag']) ? $controller->pSetup['bodyTag'] : $defBT; if (trim($controller->pSetup['bodyTagAdd'] ?? '')) { $bodyTag = preg_replace('/>$/', '', trim($bodyTag)) . ' ' . trim($controller->pSetup['bodyTagAdd']) . '>'; } } $pageRenderer->addBodyContent(LF . $bodyTag); // Div-sections if ($controller->divSection) { $pageRenderer->addBodyContent(LF . $controller->divSection); } // Page content $pageRenderer->addBodyContent(LF . $pageContent); if (!empty($controller->config['INTincScript']) && is_array($controller->config['INTincScript'])) { // Store the serialized pageRenderer in configuration $controller->config['INTincScript_ext']['pageRenderer'] = serialize($pageRenderer); // Render complete page, keep placeholders for JavaScript and CSS $pageContent = $pageRenderer->renderPageWithUncachedObjects($controller->config['INTincScript_ext']['divKey']); } else { // Render complete page $pageContent = $pageRenderer->render(); } return $pageContent ?: ''; } /************************* * * Helper functions * *************************/ /** * 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 = 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 (e.g. $GLOBALS['TSFE']->pSetup['meta.']) * @param ContentObjectRenderer $cObj */ protected function generateMetaTagHtml(array $metaTagTypoScript, ContentObjectRenderer $cObj) { $pageRenderer = $this->getPageRenderer(); $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class); $conf = $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']; $replace = false; } } 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 ?? '') !== '') { $pageRenderer->setMetaTag($attribute, $key, $subValue, [], $replace); } } } } /** * @return PageRenderer */ protected function getPageRenderer(): PageRenderer { return GeneralUtility::makeInstance(PageRenderer::class); } /** * Adds inline CSS code, by respecting the inlineStyle2TempFile option * * @param TypoScriptFrontendController $controller * @param string $cssStyles the inline CSS styling * @param bool $excludeFromConcatenation option to see if it should be concatenated * @param string $inlineBlockName the block name to add it */ protected function addCssToPageRenderer(TypoScriptFrontendController $controller, string $cssStyles, bool $excludeFromConcatenation, string $inlineBlockName) { if (empty($controller->config['config']['inlineStyle2TempFile'] ?? false)) { $this->getPageRenderer()->addCssInlineBlock($inlineBlockName, $cssStyles, !empty($controller->config['config']['compressCss'] ?? false)); } else { $this->getPageRenderer()->addCssFile( GeneralUtility::writeStyleSheetContentToTemporaryFile($cssStyles), 'stylesheet', 'all', '', (bool)($controller->config['config']['compressCss'] ?? false), false, '', $excludeFromConcatenation ); } } /** * Generates the tag by evaluting 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) { $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; } /** * This request handler can handle any frontend request. * * @param ServerRequestInterface $request * @return bool If the request is not an eID request, TRUE otherwise FALSE */ public function canHandleRequest(ServerRequestInterface $request): bool { return true; } /** * Returns the priority - how eager the handler is to actually handle the * request. * * @return int The priority of the request handler. */ public function getPriority(): int { return 50; } }