Files

1906 lines
78 KiB
PHP

<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Page;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locale;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\DirectiveHashCollection;
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceDoesNotExistException;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\StaticResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\UriResource;
use TYPO3\CMS\Core\Type\DocType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* TYPO3 pageRender class
* This class render the HTML of a webpage, usable for BE and FE
*/
class PageRenderer implements SingletonInterface
{
// Constants for the part to be rendered
protected const PART_COMPLETE = 0;
protected const PART_HEADER = 1;
protected const PART_FOOTER = 2;
protected bool $moveJsFromHeaderToFooter = false;
/**
* The locale, used for the <html> tag (depending on the DocType) and possible translation files.
*/
protected Locale $locale;
// Arrays containing associative arrays for the included files
/**
* @var array<string, array>
*/
protected array $jsFiles = [];
protected array $jsLibs = [];
/**
* @var array<string, array>
*/
protected array $cssFiles = [];
/**
* @var array<string, array>
*/
protected array $cssLibs = [];
protected string $title = '';
protected string $favIcon = '';
// Static header blocks
protected string $xmlPrologAndDocType = '';
protected array $inlineComments = [];
protected array $headerData = [];
protected array $footerData = [];
protected string $titleTag = '<title>|</title>';
protected string $htmlTag = '<html>';
protected string $headTag = '<head>';
protected string $iconMimeType = '';
protected string $shortcutTag = '<link rel="icon" href="%1$s"%2$s />';
// Static inline code blocks
/**
* @var array<string, array>
*/
protected array $jsInline = [];
/**
* @var array<string, array>
*/
protected array $cssInline = [];
protected string $bodyContent = '';
protected string $templateFile = 'PKG:typo3/cms-core:Resources/Private/Templates/PageRenderer.html';
protected array $inlineLanguageLabels = [];
protected array $inlineLanguageLabelFiles = [];
protected array $inlineSettings = [];
/**
* Is empty string for HTML and ' /' for XHTML rendering
*/
protected string $endingSlash = '';
protected JavaScriptRenderer $javaScriptRenderer;
protected ?ConsumableNonce $nonce = null;
protected DocType $docType = DocType::html5;
protected bool $applyNonceHint = false;
public function __construct(
protected readonly Context $context,
#[Autowire(service: 'cache.assets')]
protected readonly FrontendInterface $assetsCache,
protected readonly MarkerBasedTemplateService $templateService,
protected readonly MetaTagManagerRegistry $metaTagRegistry,
protected readonly AssetRenderer $assetRenderer,
protected readonly AssetCollector $assetCollector,
protected readonly RelativeCssPathFixer $relativeCssPathFixer,
protected readonly LanguageServiceFactory $languageServiceFactory,
protected readonly ResponseFactoryInterface $responseFactory,
protected readonly StreamFactoryInterface $streamFactory,
protected readonly IconRegistry $iconRegistry,
protected readonly SystemResourcePublisherInterface $resourcePublisher,
protected readonly SystemResourceFactory $systemResourceFactory,
protected readonly ResourceHashCollection $resourceHashCollection,
protected readonly DirectiveHashCollection $directiveHashCollection,
) {
$this->locale = new Locale();
$this->docType = DocType::html5;
$this->xmlPrologAndDocType = DocType::html5->getDoctypeDeclaration();
$htmlTagAttributes = ['lang' => 'en'];
$backendUserAspect = $this->context->getAspect('backend.user');
if ($backendUserAspect->isLoggedIn()) {
// If a backend user is logged in, we assume BE context and add a default html tag
// with theme and color scheme attributes for this backend user.
// In case this is FE context, FE RequestHandler will later override with final html tag attributes again.
// This is done here for BE b/w compat reasons. Assuming BE context is done to prevent
// accessing Request in __construct() and application type is only available as Request attribute.
$themeAndColorSchemeAttributes = $this->getThemeAndColorSchemeHtmlTagAttributes($this->getBackendUser());
$htmlTagAttributes = array_merge($htmlTagAttributes, $themeAndColorSchemeAttributes);
}
$this->htmlTag = '<html ' . GeneralUtility::implodeAttributes($htmlTagAttributes, true) . '>';
$this->javaScriptRenderer = JavaScriptRenderer::create('EXT:core/Resources/Public/JavaScript/java-script-item-handler.js');
$this->setMetaTag('name', 'generator', 'TYPO3 CMS');
}
/**
* @internal
*/
public function updateState(array $state): void
{
foreach ($state as $var => $value) {
switch ($var) {
case 'assetsCache':
case 'assetRenderer':
case 'assetCollector':
case 'context':
case 'templateService':
case 'relativeCssPathFixer':
case 'languageServiceFactory':
case 'responseFactory':
case 'streamFactory':
case 'iconRegistry':
case 'resourcePublisher':
case 'systemResourceFactory':
case 'resourceHashCollection':
case 'directiveHashCollection':
case 'nonce':
break;
case 'metaTagRegistry':
$this->metaTagRegistry->updateState($value);
break;
case 'javaScriptRenderer':
$this->javaScriptRenderer->updateState($value);
break;
default:
$this->{$var} = $value;
break;
}
}
}
/**
* @internal
*/
public function getState(): array
{
$state = [];
foreach (get_object_vars($this) as $var => $value) {
switch ($var) {
case 'assetsCache':
case 'assetRenderer':
case 'context':
case 'templateService':
case 'relativeCssPathFixer':
case 'languageServiceFactory':
case 'responseFactory':
case 'streamFactory':
case 'iconRegistry':
case 'resourcePublisher':
case 'nonce':
case 'systemResourceFactory':
case 'resourceHashCollection':
case 'directiveHashCollection':
break;
case 'metaTagRegistry':
$state[$var] = $this->metaTagRegistry->getState();
break;
case 'javaScriptRenderer':
$state[$var] = $this->javaScriptRenderer->getState();
break;
default:
$state[$var] = $value;
break;
}
}
return $state;
}
/**
* BE only, FE uses a different approach to register JS
*/
public function getJavaScriptRenderer(): JavaScriptRenderer
{
return $this->javaScriptRenderer;
}
/**
* Content of <title> tag in <html><head>
*/
public function setTitle(string $title): void
{
$this->title = $title;
}
/**
* Sets xml prolog and docType.
* FE only, BE is hard coded html5.
*
* @param string $xmlPrologAndDocType Complete tags for xml prolog and docType
*/
public function setXmlPrologAndDocType(string $xmlPrologAndDocType): void
{
$this->xmlPrologAndDocType = $xmlPrologAndDocType;
}
/**
* Sets language
*/
public function setLanguage(Locale $locale, ServerRequestInterface $request): void
{
$this->locale = $locale;
$this->setDefaultHtmlTag($request);
}
/**
* Sets html tag
* FE only, BE is hard coded html5.
*/
public function setHtmlTag(string $htmlTag): void
{
$this->htmlTag = $htmlTag;
}
/**
* Sets HTML head tag
* FE only.
*/
public function setHeadTag(string $headTag): void
{
$this->headTag = $headTag;
}
/**
* Sets favicon
*/
public function setFavIcon(string $favIcon): void
{
$this->favIcon = $favIcon;
}
/**
* Sets icon mime type
*/
public function setIconMimeType(string $iconMimeType): void
{
$this->iconMimeType = $iconMimeType;
}
/**
* Sets template file
* FE only.
*/
public function setTemplateFile(string $file): void
{
$this->templateFile = $file;
}
/**
* Sets Content for Body
* BE only, FE uses addBodyContent
*/
public function setBodyContent(string $content): void
{
$this->bodyContent = $content;
}
/**
* BE only?
*/
public function setApplyNonceHint(bool $applyNonceHint): void
{
$this->applyNonceHint = $applyNonceHint;
}
/**
* FE only
*/
public function enableMoveJsFromHeaderToFooter(): void
{
$this->moveJsFromHeaderToFooter = true;
}
/**
* FE only, unused.
*/
public function disableMoveJsFromHeaderToFooter(): void
{
$this->moveJsFromHeaderToFooter = false;
}
public function setNonce(?ConsumableNonce $nonce): void
{
$this->nonce = $nonce;
}
/**
* FE only, BE is hard coded HTML5
*/
public function setDocType(DocType $docType, ServerRequestInterface $request): void
{
$this->docType = $docType;
$this->xmlPrologAndDocType = $docType->getDoctypeDeclaration();
$this->setDefaultHtmlTag($request);
}
/**
* Sets a given meta tag
*
* @param string $type The type of the meta tag. Allowed values are property, name or http-equiv
* @param string $name The name of the property to add
* @param string $content The content of the meta tag
* @param array $subProperties Subproperties of the meta tag (like e.g. og:image:width)
* @param bool $replace Replace earlier set meta tag
*/
public function setMetaTag(string $type, string $name, string $content, array $subProperties = [], bool $replace = true): void
{
// Lowercase all the things
$type = strtolower($type);
$name = strtolower($name);
if (!in_array($type, ['property', 'name', 'http-equiv'], true)) {
throw new \InvalidArgumentException(
'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.',
1496402460
);
}
$manager = $this->metaTagRegistry->getManagerForProperty($name);
$manager->addProperty($name, $content, $subProperties, $replace, $type);
}
/**
* Adds inline HTML comment.
* FE only.
*/
public function addInlineComment(string $comment): void
{
if (!in_array($comment, $this->inlineComments)) {
$this->inlineComments[] = $comment;
}
}
/**
* Adds header data
*
* @param string $data Free header data for HTML header
*/
public function addHeaderData(string $data): void
{
if (!in_array($data, $this->headerData)) {
$this->headerData[] = $data;
}
}
/**
* Adds footer data
*
* @param string $data Free footer data for HTML footer before closing body tag
*/
public function addFooterData(string $data): void
{
if (!in_array($data, $this->footerData)) {
$this->footerData[] = $data;
}
}
/**
* Adds JS Library. JS Library block is rendered on top of the JS files.
*
* @param string $name Arbitrary identifier
* @param string|StaticResourceInterface $file File name
* @param string|null $type Content Type
* @param bool $forceOnTop Flag if added library should be inserted at begin of this block
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
* @param array<string, string> $tagAttributes Key => value list of tag attributes
*/
public function addJsLibrary($name, $file, $type = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false, array $tagAttributes = []): void
{
$resource = $this->handleAddedResource($file);
$isUriResource = $resource instanceof UriResource;
if ($type === null) {
$type = $this->docType === DocType::html5 ? '' : 'text/javascript';
}
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
if (!isset($this->jsLibs[strtolower($name)])) {
$this->jsLibs[strtolower($name)] = [
'file' => (string)$resource,
'type' => $type,
'section' => self::PART_HEADER,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'async' => $async,
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds JS Library to Footer. JS Library block is rendered on top of the Footer JS files.
*
* @param string $name Arbitrary identifier
* @param string|StaticResourceInterface $file File name
* @param string|null $type Content Type
* @param bool $forceOnTop Flag if added library should be inserted at begin of this block
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
* @param array<string, string> $tagAttributes Key => value list of tag attributes
*/
public function addJsFooterLibrary($name, $file, $type = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false, array $tagAttributes = []): void
{
$resource = $this->handleAddedResource($file);
$isUriResource = $resource instanceof UriResource;
if ($type === null) {
$type = $this->docType === DocType::html5 ? '' : 'text/javascript';
}
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
$name .= '_jsFooterLibrary';
if (!isset($this->jsLibs[strtolower($name)])) {
$this->jsLibs[strtolower($name)] = [
'file' => (string)$resource,
'type' => $type,
'section' => self::PART_FOOTER,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'async' => $async,
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds JS file
*
* @param string|StaticResourceInterface $file File name
* @param string|null $type Content Type
* @param bool $forceOnTop
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
* @param array<string, string> $tagAttributes Key => value list of tag attributes
*/
public function addJsFile($file, $type = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false, array $tagAttributes = []): void
{
$resource = $this->handleAddedResource($file);
$resourceIdentifier = (string)$resource;
$isUriResource = $resource instanceof UriResource;
if ($type === null) {
$type = $this->docType === DocType::html5 ? '' : 'text/javascript';
}
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
if (!isset($this->jsFiles[$resourceIdentifier])) {
$this->jsFiles[$resourceIdentifier] = [
'file' => $resourceIdentifier,
'type' => $type,
'section' => self::PART_HEADER,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'async' => $async,
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds JS file to footer
*
* @param string|StaticResourceInterface $file File name
* @param string|null $type Content Type
* @param bool $forceOnTop
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
* @param array<string, string> $tagAttributes Key => value list of tag attributes
*/
public function addJsFooterFile($file, $type = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false, array $tagAttributes = []): void
{
$resource = $this->handleAddedResource($file);
$resourceIdentifier = (string)$resource;
$isUriResource = $resource instanceof UriResource;
if ($type === null) {
$type = $this->docType === DocType::html5 ? '' : 'text/javascript';
}
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
if (!isset($this->jsFiles[$resourceIdentifier])) {
$this->jsFiles[$resourceIdentifier] = [
'file' => $resourceIdentifier,
'type' => $type,
'section' => self::PART_FOOTER,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'async' => $async,
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds JS inline code
* FE only.
*
* @param string $name
* @param string $block
* @param bool $forceOnTop
*/
public function addJsInlineCode($name, $block, mixed $_ = null, $forceOnTop = false, bool $csp = false): void
{
if (!isset($this->jsInline[$name]) && !empty($block)) {
$this->jsInline[$name] = [
'code' => $block . LF,
'section' => self::PART_HEADER,
'forceOnTop' => $forceOnTop,
'csp' => $csp,
];
}
}
/**
* Adds JS inline code to footer
* FE only.
*
* @param string $name
* @param string $block
* @param bool $forceOnTop
*/
public function addJsFooterInlineCode($name, $block, mixed $_ = null, $forceOnTop = false, bool $csp = false): void
{
if (!isset($this->jsInline[$name]) && !empty($block)) {
$this->jsInline[$name] = [
'code' => $block . LF,
'section' => self::PART_FOOTER,
'forceOnTop' => $forceOnTop,
'csp' => $csp,
];
}
}
/**
* Adds CSS file
*
* @param string|StaticResourceInterface $file
* @param string $rel
* @param string $media
* @param string $title
* @param bool $forceOnTop
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $inline
* @param array<string, string> $tagAttributes Key => value list of tag attributes
* @param string $integrity Subresource Integrity (SRI)
* @param string $crossorigin CORS settings attribute
*/
public function addCssFile($file, $rel = 'stylesheet', $media = 'all', $title = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $inline = false, array $tagAttributes = [], string $integrity = '', string $crossorigin = ''): void
{
$resource = $this->handleAddedResource($file);
$isUriResource = $resource instanceof UriResource;
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
$resourceIdentifier = (string)$resource;
if (!isset($this->cssFiles[$resourceIdentifier])) {
$this->cssFiles[$resourceIdentifier] = [
'file' => $resourceIdentifier,
'rel' => $rel,
'media' => $media,
'title' => $title,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'inline' => $inline,
'integrity' => $integrity,
'crossorigin' => $crossorigin,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds CSS library
*
* @param string|StaticResourceInterface $file
* @param string $rel
* @param string $media
* @param string $title
* @param bool $forceOnTop
* @param string $allWrap
* @param string $splitChar The char used to split the allWrap value, default is "|"
* @param bool $inline
* @param array<string, string> $tagAttributes Key => value list of tag attributes
* @param string $integrity Subresource Integrity (SRI)
* @param string $crossorigin CORS settings attribute
*/
public function addCssLibrary($file, $rel = 'stylesheet', $media = 'all', $title = '', mixed $_ = null, $forceOnTop = false, $allWrap = '', mixed $__ = null, $splitChar = '|', $inline = false, array $tagAttributes = [], string $integrity = '', string $crossorigin = ''): void
{
$resource = $this->handleAddedResource($file);
$isUriResource = $resource instanceof UriResource;
if ($integrity === ResourceHashCollection::AUTO) {
$integrity = $this->resourceHashCollection->fetchResourceHash($resource)?->export() ?? '';
}
if ($crossorigin === '' && $integrity !== '' && $isUriResource) {
$crossorigin = 'anonymous';
}
$resourceIdentifier = (string)$resource;
if (!isset($this->cssLibs[$resourceIdentifier])) {
$this->cssLibs[$resourceIdentifier] = [
'file' => $resourceIdentifier,
'rel' => $rel,
'media' => $media,
'title' => $title,
'forceOnTop' => $forceOnTop,
'allWrap' => $allWrap,
'splitChar' => $splitChar,
'inline' => $inline,
'integrity' => $integrity,
'crossorigin' => $crossorigin,
'tagAttributes' => $tagAttributes,
];
}
}
/**
* Adds CSS inline code
*
* @param string $name
* @param string $block
* @param bool $forceOnTop
*/
public function addCssInlineBlock($name, $block, mixed $_ = null, $forceOnTop = false, bool $csp = false): void
{
if (!isset($this->cssInline[$name]) && !empty($block)) {
$this->cssInline[$name] = [
'code' => $block,
'forceOnTop' => $forceOnTop,
'csp' => $csp,
];
}
}
/**
* Includes an ES6/ES11 compatible JavaScript module by
* resolving the specifier to an import-mapped filename.
*
* @param string $specifier Bare module identifier like @my/package/filename.js
*/
public function loadJavaScriptModule(string $specifier): void
{
$this->javaScriptRenderer->addJavaScriptModuleInstruction(
JavaScriptModuleInstruction::create($specifier)
);
}
/**
* Adds Javascript Inline Label. This will occur in TYPO3.lang - object
* The label can be used in scripts with TYPO3.lang.<key>
* BE only.
*
* @param string $key
* @param string $value
*/
public function addInlineLanguageLabel($key, $value): void
{
$this->inlineLanguageLabels[$key] = $value;
}
/**
* Adds Javascript Inline Label Array. This will occur in TYPO3.lang - object
* The label can be used in scripts with TYPO3.lang.<key>
* Array will be merged with existing array.
* BE only.
*/
public function addInlineLanguageLabelArray(array $array): void
{
$this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $array);
}
/**
* Gets labels to be used in JavaScript fetched from a locallang file.
*
* @param string $fileRef Input is a file-reference (see GeneralUtility::getFileAbsFileName). That file is expected to be a 'locallang.xlf' file containing a valid XML TYPO3 language structure.
* @param string $selectionPrefix Prefix to select the correct labels (default: '')
* @param string $stripFromSelectionName String to be removed from the label names in the output. (default: '')
*/
public function addInlineLanguageLabelFile($fileRef, $selectionPrefix = '', $stripFromSelectionName = ''): void
{
$index = md5($fileRef . $selectionPrefix . $stripFromSelectionName);
if ($fileRef && !isset($this->inlineLanguageLabelFiles[$index])) {
$this->inlineLanguageLabelFiles[$index] = [
'fileRef' => $fileRef,
'selectionPrefix' => $selectionPrefix,
'stripFromSelectionName' => $stripFromSelectionName,
];
}
}
/**
* Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
* The label can be used in scripts with TYPO3.setting.<key>
*
* @param string|null $namespace
* @param string $key
* @param mixed $value
*/
public function addInlineSetting($namespace, $key, $value): void
{
if ($namespace !== null && $namespace !== '') {
if (strpos($namespace, '.')) {
$parts = explode('.', $namespace);
$a = &$this->inlineSettings;
foreach ($parts as $part) {
$a = &$a[$part];
}
$a[$key] = $value;
} else {
$this->inlineSettings[$namespace][$key] = $value;
}
} else {
$this->inlineSettings[$key] = $value;
}
}
/**
* Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
* The label can be used in scripts with TYPO3.setting.<key>
* Array will be merged with existing array.
*
* @param string $namespace
*/
public function addInlineSettingArray($namespace, array $array): void
{
if ($namespace) {
if (strpos($namespace, '.')) {
$parts = explode('.', $namespace);
$a = &$this->inlineSettings;
foreach ($parts as $part) {
$a = &$a[$part];
}
$a = array_merge((array)$a, $array);
} else {
$this->inlineSettings[$namespace] = array_merge((array)($this->inlineSettings[$namespace] ?? []), $array);
}
} else {
$this->inlineSettings = array_merge($this->inlineSettings, $array);
}
}
/**
* Adds content to body content
*/
public function addBodyContent(string $content): void
{
$this->bodyContent .= $content;
}
/**
* Render the page.
* BE only.
*
* @return string Content of rendered page
*/
public function render(ServerRequestInterface $request): string
{
$this->prepareRendering();
[$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs] = $this->renderJavaScriptAndCss($request);
$metaTags = implode(LF, $this->renderMetaTagsFromAPI($this->docType));
$markerArray = [
'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
'HTMLTAG' => $this->htmlTag,
'HEADTAG' => $this->headTag,
'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
'CSS_LIBS' => $cssLibs,
'CSS_INCLUDE' => $cssFiles,
'CSS_INLINE' => $cssInline,
'JS_INLINE' => $jsInline,
'JS_INCLUDE' => $jsFiles,
'JS_LIBS' => $jsLibs,
'TITLE' => $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '',
'META' => $metaTags,
'HEADERDATA' => $this->headerData ? implode(LF, $this->headerData) : '',
'FOOTERDATA' => $this->footerData ? implode(LF, $this->footerData) : '',
'JS_LIBS_FOOTER' => $jsFooterLibs,
'JS_INCLUDE_FOOTER' => $jsFooterFiles,
'JS_INLINE_FOOTER' => $jsFooterInline,
'BODY' => $this->bodyContent,
// @internal
'TRAILING_SLASH_FOR_SELF_CLOSING_TAG' => $this->endingSlash ? ' ' . $this->endingSlash : '',
];
$markerArray = array_map(trim(...), $markerArray);
$template = $this->getTemplate();
// The page renderer needs a full reset when the page was rendered
$this->reset($request);
return trim($this->templateService->substituteMarkerArray($template, $markerArray, '###|###'));
}
/**
* Render the page for frontend output.
* FE only.
*
* @internal Not part of the public API. Only for use in TYPO3 frontend rendering.
*/
public function renderFrontendPage(ServerRequestInterface $request): string
{
$this->prepareRendering();
[$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs] = $this->renderJavaScriptAndCss($request);
$metaTags = implode(LF, $this->renderMetaTagsFromAPI($this->docType));
$markerArray = [
'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
'HTMLTAG' => $this->htmlTag,
'HEADTAG' => $this->headTag,
'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
'CSS_LIBS' => $cssLibs,
'CSS_INCLUDE' => $cssFiles,
'CSS_INLINE' => $cssInline,
'JS_INLINE' => $jsInline,
'JS_INCLUDE' => $jsFiles,
'JS_LIBS' => $jsLibs,
'TITLE' => $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '',
'META' => $metaTags,
'HEADERDATA' => $this->headerData ? implode(LF, $this->headerData) : '',
'FOOTERDATA' => $this->footerData ? implode(LF, $this->footerData) : '',
'JS_LIBS_FOOTER' => $jsFooterLibs,
'JS_INCLUDE_FOOTER' => $jsFooterFiles,
'JS_INLINE_FOOTER' => $jsFooterInline,
'BODY' => $this->bodyContent,
// @internal
'TRAILING_SLASH_FOR_SELF_CLOSING_TAG' => $this->endingSlash ? ' ' . $this->endingSlash : '',
];
$markerArray = array_map(trim(...), $markerArray);
$template = $this->getTemplate();
$this->reset($request);
return trim($this->templateService->substituteMarkerArray($template, $markerArray, '###|###'));
}
/**
* BE only.
*/
public function renderResponse(
ServerRequestInterface $request,
int $code = 200,
string $reasonPhrase = '',
): ResponseInterface {
$stream = $this->streamFactory->createStream($this->render($request));
return $this->responseFactory->createResponse($code, $reasonPhrase)
->withHeader('Content-Type', 'text/html; charset=utf-8')
->withBody($stream);
}
/**
* Frontend related rendering of the main page HTML scaffold with placeholders
* for dynamic sections finished by uncached element ("INT") processing later.
* The result of this method is cached as content in page cache.
* FE only.
*
* @param string $substituteHash The hash that is used for the placeholder markers
* @internal Never use in extensions.
*/
public function renderPageWithUncachedObjects(string $substituteHash): string
{
$this->prepareRendering();
$markerArray = [
'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
'HTMLTAG' => $this->htmlTag,
'HEADTAG' => $this->headTag,
'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
'META' => '<!-- ###META' . $substituteHash . '### -->',
'BODY' => $this->bodyContent,
'TITLE' => '<!-- ###TITLE' . $substituteHash . '### -->',
'CSS_LIBS' => '<!-- ###CSS_LIBS' . $substituteHash . '### -->',
'CSS_INCLUDE' => '<!-- ###CSS_INCLUDE' . $substituteHash . '### -->',
'CSS_INLINE' => '<!-- ###CSS_INLINE' . $substituteHash . '### -->',
'JS_INLINE' => '<!-- ###JS_INLINE' . $substituteHash . '### -->',
'JS_INCLUDE' => '<!-- ###JS_INCLUDE' . $substituteHash . '### -->',
'JS_LIBS' => '<!-- ###JS_LIBS' . $substituteHash . '### -->',
'HEADERDATA' => '<!-- ###HEADERDATA' . $substituteHash . '### -->',
'FOOTERDATA' => '<!-- ###FOOTERDATA' . $substituteHash . '### -->',
'JS_LIBS_FOOTER' => '<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->',
'JS_INCLUDE_FOOTER' => '<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->',
'JS_INLINE_FOOTER' => '<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->',
// @internal
'TRAILING_SLASH_FOR_SELF_CLOSING_TAG' => $this->endingSlash ? ' ' . $this->endingSlash : '',
];
// Reset body content to empty string so the content is not cached twice since it is
// already cached as 'content' section next to the other PageRenderer state.
$this->bodyContent = '';
$markerArray = array_map(trim(...), $markerArray);
$template = $this->getTemplate();
// Note in contrast to render(), this method does *not* call $this->reset() since the PageRenderer state
// is serialized and cached for uncached element processing.
return trim($this->templateService->substituteMarkerArray($template, $markerArray, '###|###'));
}
/**
* Renders the JavaScript and CSS files that have been added during processing
* of uncached content objects (USER_INT, COA_INT)
* FE only.
*
* @param string $substituteHash The hash that is used for the variables
* @internal Never use in extensions.
*/
public function renderJavaScriptAndCssForProcessingOfUncachedContentObjects(ServerRequestInterface $request, string $cachedPageContent, string $substituteHash): string
{
$this->prepareRendering();
// bodyContent is reset to empty string in FE both after render() and renderPageWithUncachedObjects().
// $this->bodyContent is set to the "cached with placeholder" string here for renderJavaScriptAndCss()
// hook to consistently receive bodyContent, otherwise it wouldn't be needed to do this here.
$this->bodyContent = $cachedPageContent;
[$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs] = $this->renderJavaScriptAndCss($request);
$title = $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '';
$markerArray = [
'<!-- ###TITLE' . $substituteHash . '### -->' => $title,
'<!-- ###CSS_LIBS' . $substituteHash . '### -->' => $cssLibs,
'<!-- ###CSS_INCLUDE' . $substituteHash . '### -->' => $cssFiles,
'<!-- ###CSS_INLINE' . $substituteHash . '### -->' => $cssInline,
'<!-- ###JS_INLINE' . $substituteHash . '### -->' => $jsInline,
'<!-- ###JS_INCLUDE' . $substituteHash . '### -->' => $jsFiles,
'<!-- ###JS_LIBS' . $substituteHash . '### -->' => $jsLibs,
'<!-- ###META' . $substituteHash . '### -->' => implode(LF, $this->renderMetaTagsFromAPI($this->docType)),
'<!-- ###HEADERDATA' . $substituteHash . '### -->' => implode(LF, $this->headerData),
'<!-- ###FOOTERDATA' . $substituteHash . '### -->' => implode(LF, $this->footerData),
'<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->' => $jsFooterLibs,
'<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->' => $jsFooterFiles,
'<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->' => $jsFooterInline,
];
foreach ($markerArray as $placeHolder => $content) {
$cachedPageContent = str_replace($placeHolder, $content, $cachedPageContent);
}
$this->reset($request);
return $cachedPageContent;
}
/**
* Reset all vars to initial values
*/
protected function reset(ServerRequestInterface $request): void
{
$this->locale = new Locale();
$this->setDocType(DocType::html5, $request);
$this->templateFile = 'PKG:typo3/cms-core:Resources/Private/Templates/PageRenderer.html';
$this->bodyContent = '';
$this->jsFiles = [];
$this->jsInline = [];
$this->jsLibs = [];
$this->cssFiles = [];
$this->cssInline = [];
$this->inlineComments = [];
$this->headerData = [];
$this->footerData = [];
$this->javaScriptRenderer = JavaScriptRenderer::create('EXT:core/Resources/Public/JavaScript/java-script-item-handler.js');
}
/**
* Internal method to set a basic <html> tag when in HTML5 with the proper language/locale and "dir" attributes.
*/
protected function setDefaultHtmlTag(ServerRequestInterface $request): void
{
if ($this->docType === DocType::html5) {
$attributes = [
'lang' => $this->locale->getName(),
];
if ($this->locale->isRightToLeftLanguageDirection()) {
$attributes['dir'] = 'rtl';
}
// @todo: build an API to add HTML attributes cleanly
if ($this->getApplicationType($request) === 'BE') {
$backendUser = $this->context->getAspect('backend.user');
if ($backendUser->isLoggedIn()) {
$attributes = array_merge($attributes, $this->getThemeAndColorSchemeHtmlTagAttributes($this->getBackendUser()));
}
}
$this->setHtmlTag('<html ' . GeneralUtility::implodeAttributes($attributes, true) . '>');
}
}
private function getThemeAndColorSchemeHtmlTagAttributes(BackendUserAuthentication $backendUser): array
{
$attributes = [];
$userTS = $backendUser->getTSConfig();
$themeDisabled = $userTS['setup.']['fields.']['theme.']['disabled'] ?? '0';
$theme = $backendUser->uc['theme'] ?? $userTS['setup.']['fields.']['theme'] ?? 'fresh';
if ($themeDisabled === '1') {
$theme = $userTS['setup.']['fields.']['theme'] ?? 'fresh';
}
if ($theme !== 'modern') {
$attributes['data-theme'] = $theme;
}
$colorSchemeDisabled = $userTS['setup.']['fields.']['colorScheme.']['disabled'] ?? '0';
$colorScheme = $backendUser->uc['colorScheme'] ?? $userTS['setup.']['fields.']['colorScheme'] ?? 'auto';
if ($colorSchemeDisabled === '1') {
$colorScheme = $userTS['setup.']['fields.']['colorScheme'] ?? 'light';
}
if ($colorScheme !== 'auto') {
$attributes['data-color-scheme'] = $colorScheme;
}
return $attributes;
}
/**
* Renders metaTags based on tags added via the API
*/
protected function renderMetaTagsFromAPI(DocType $docType): array
{
$metaTags = [];
$metaTagManagers = $this->metaTagRegistry->getAllManagers();
foreach ($metaTagManagers as $managerObject) {
// @todo: Reflect $docType argument in MetaTagManagerInterface
$properties = $managerObject->renderAllProperties($docType); // @phpstan-ignore arguments.count
if (!empty($properties)) {
$metaTags[] = $properties;
}
}
return $metaTags;
}
/**
* Remove ending slashes from static header block
* if the page is being rendered as html (not xhtml)
* and define property $this->endingSlash for further use
*/
protected function prepareRendering(): void
{
if ($this->docType->isXmlCompliant()) {
$this->endingSlash = ' /';
} else {
$this->shortcutTag = str_replace(' />', '>', $this->shortcutTag);
$this->endingSlash = '';
}
}
/**
* Renders all JavaScript and CSS
*
* @return string[]
*/
protected function renderJavaScriptAndCss(ServerRequestInterface $request): array
{
$this->executePreRenderHook();
$mainJsLibs = $this->renderMainJavaScriptLibraries($request);
$this->executeRenderPostTransformHook();
$cssLibs = $this->renderCssLibraries($request);
$cssFiles = $this->renderCssFiles($request);
$cssInline = $this->renderCssInline();
[$jsLibs, $jsFooterLibs] = $this->renderAdditionalJavaScriptLibraries($request);
[$jsFiles, $jsFooterFiles] = $this->renderJavaScriptFiles($request);
[$jsInline, $jsFooterInline] = $this->renderInlineJavaScript();
$jsLibs = $mainJsLibs . $jsLibs;
if ($this->moveJsFromHeaderToFooter) {
$jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
$jsLibs = '';
$jsFooterFiles = $jsFiles . LF . $jsFooterFiles;
$jsFiles = '';
$jsFooterInline = $jsInline . LF . $jsFooterInline;
$jsInline = '';
}
// Use AssetRenderer to inject all JavaScripts and CSS files
$jsInline .= $this->assetRenderer->renderInlineJavaScript(true, $this->nonce);
$jsFooterInline .= $this->assetRenderer->renderInlineJavaScript(false, $this->nonce);
$jsFiles .= $this->assetRenderer->renderJavaScript(true, $this->nonce);
$jsFooterFiles .= $this->assetRenderer->renderJavaScript(false, $this->nonce);
$cssInline .= $this->assetRenderer->renderInlineStyleSheets(true, $this->nonce);
// append inline CSS to footer (as there is no cssFooterInline)
$jsFooterFiles .= $this->assetRenderer->renderInlineStyleSheets(false, $this->nonce);
$cssLibs .= $this->assetRenderer->renderStyleSheets(true, $this->endingSlash, $this->nonce);
$cssFiles .= $this->assetRenderer->renderStyleSheets(false, $this->endingSlash, $this->nonce);
$this->executePostRenderHook($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs);
return [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs];
}
/**
* Reads the template file and returns the requested part as string
*/
protected function getTemplate(): string
{
$templateResource = $this->systemResourceFactory->createResource($this->templateFile);
try {
if ($templateResource instanceof SystemResourceInterface) {
return $templateResource->getContents();
}
} catch (SystemResourceDoesNotExistException) {
}
return '';
}
/**
* Helper function for render the main JavaScript libraries
*
* @return string Content with JavaScript libraries
*/
protected function renderMainJavaScriptLibraries(ServerRequestInterface $request): string
{
$out = '';
foreach ($this->assetCollector->getJavaScriptModules() as $module) {
$this->loadJavaScriptModule($module);
}
// adds a nonce hint/work-around for lit-elements (which is only applied automatically in ShadowDOM)
// see https://lit.dev/docs/api/ReactiveElement/#ReactiveElement.styles)
if ($this->applyNonceHint && $this->nonce !== null) {
$this->javaScriptRenderer->addGlobalAssignment(['litNonce' => $this->nonce->consumeInline(Directive::ScriptSrcElem)]);
}
$sitePath = $request->getAttribute('normalizedParams')->getSitePath();
$useNonce = $this->getApplicationType($request) === 'BE';
$out .= $this->javaScriptRenderer->renderImportMap(
$sitePath,
$useNonce ? $this->nonce : null,
);
$this->loadJavaScriptLanguageStrings();
if ($this->getApplicationType($request) === 'BE') {
$noBackendUserLoggedIn = empty($GLOBALS['BE_USER']->user['uid']);
$this->addAjaxUrlsToInlineSettings($noBackendUserLoggedIn);
$this->addGlobalCSSUrlsToInlineSettings($request);
$this->inlineSettings['cache']['iconCacheIdentifier'] = sha1($this->iconRegistry->getBackendIconsCacheIdentifier());
}
$assignments = array_filter([
'settings' => $this->inlineSettings,
'lang' => $this->parseLanguageLabelsForJavaScript(),
]);
if ($assignments !== []) {
if ($this->getApplicationType($request) === 'BE') {
$this->javaScriptRenderer->addGlobalAssignment(['TYPO3' => $assignments]);
} else {
$out .= $this->wrapInlineScript(
sprintf(
"var TYPO3 = Object.assign(TYPO3 || {}, %s);\r\n",
// filter potential prototype pollution
sprintf(
'Object.fromEntries(Object.entries(%s).filter((entry) => '
. "!['__proto__', 'prototype', 'constructor'].includes(entry[0])))",
json_encode($assignments)
)
),
$this->nonce !== null ? ['nonce' => $this->nonce->consumeInline(Directive::ScriptSrcElem)] : []
);
}
}
$out .= $this->javaScriptRenderer->render($this->nonce, $sitePath);
return $out;
}
/**
* Converts the language labels for usage in JavaScript
*/
protected function parseLanguageLabelsForJavaScript(): array
{
if (empty($this->inlineLanguageLabels)) {
return [];
}
$labels = [];
foreach ($this->inlineLanguageLabels as $key => $translationUnit) {
if (is_array($translationUnit)) {
$translationUnit = current($translationUnit);
$labels[$key] = $translationUnit['target'] ?? $translationUnit['source'];
} else {
$labels[$key] = $translationUnit;
}
}
return $labels;
}
/**
* Load the language strings into JavaScript
*/
protected function loadJavaScriptLanguageStrings(): void
{
foreach ($this->inlineLanguageLabelFiles as $languageLabelFile) {
$selectionPrefix = $languageLabelFile['selectionPrefix'];
$stripFromSelectionName = $languageLabelFile['stripFromSelectionName'];
$labelsFromFile = [];
$allLabels = $this->readLLfile($languageLabelFile['fileRef']);
// Iterate through all labels from the language file
foreach ($allLabels as $label => $value) {
// If $selectionPrefix is set, only respect labels that start with $selectionPrefix
if ($selectionPrefix === '' || str_starts_with($label, $selectionPrefix)) {
// Remove substring $stripFromSelectionName from label
$label = str_replace($stripFromSelectionName, '', $label);
$labelsFromFile[$label] = $value;
}
}
$this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $labelsFromFile);
}
$this->inlineLanguageLabelFiles = [];
}
/**
* Make URLs to all backend ajax handlers available as inline setting.
*/
protected function addAjaxUrlsToInlineSettings(bool $publicRoutesOnly = false): void
{
$ajaxUrls = [];
// Add the ajax-based routes
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$router = GeneralUtility::makeInstance(Router::class);
foreach ($router->getRoutes() as $routeIdentifier => $route) {
if ($publicRoutesOnly && $route->getOption('access') !== 'public') {
continue;
}
if ($route->getOption('ajax')) {
$uri = (string)$uriBuilder->buildUriFromRoute($routeIdentifier);
// use the shortened value in order to use this in JavaScript
if (str_starts_with($routeIdentifier, 'ajax_')) {
$routeIdentifier = substr($routeIdentifier, 5);
}
$ajaxUrls[$routeIdentifier] = $uri;
}
}
$this->inlineSettings['ajaxUrls'] = $ajaxUrls;
}
protected function addGlobalCSSUrlsToInlineSettings(ServerRequestInterface $request): void
{
$this->inlineSettings['cssUrls'] = [
'backend' => $this->getPublicUrlForFile('EXT:backend/Resources/Public/Css/backend.css', $request),
];
}
/**
* Render CSS library files
*/
protected function renderCssLibraries(ServerRequestInterface $request): string
{
$cssFiles = '';
if (!empty($this->cssLibs)) {
foreach ($this->cssLibs as $properties) {
$tag = $this->createCssTag($properties, $properties['file'], $request);
if ($properties['forceOnTop'] ?? false) {
$cssFiles = $tag . $cssFiles;
} else {
$cssFiles .= $tag;
}
}
}
return $cssFiles;
}
/**
* Render CSS files
*/
protected function renderCssFiles(ServerRequestInterface $request): string
{
$cssFiles = '';
if (!empty($this->cssFiles)) {
foreach ($this->cssFiles as $properties) {
$tag = $this->createCssTag($properties, $properties['file'], $request);
if ($properties['forceOnTop'] ?? false) {
$cssFiles = $tag . $cssFiles;
} else {
$cssFiles .= $tag;
}
}
}
return $cssFiles;
}
/**
* Adds a CSP hash for a static file to the hash collection.
* Resolves PKG:, EXT: and relative public paths via SystemResourceFactory.
* Silently skips URI resources (http/https) and unresolvable paths.
*/
private function addFileHashToCollection(Directive $directive, string $file): void
{
$resource = $this->systemResourceFactory->createResource($file);
if ($resource instanceof SystemResourceInterface) {
$this->directiveHashCollection->addResourceHash($directive, $resource);
}
}
/**
* Create link (inline=0) or style (inline=1) tag
*/
private function createCssTag(array $properties, string $file, ServerRequestInterface $request): string
{
$includeInline = $properties['inline'] ?? false;
$resource = $includeInline ? $this->systemResourceFactory->createResource($file) : null;
if ($resource instanceof SystemResourceInterface) {
$tag = $this->createInlineCssTagFromFile($resource, $properties, $request);
} else {
// collect CSP hash - use integrity attribute if given, else hash file content
$integrity = $properties['integrity'] ?? '';
if ($integrity !== '') {
try {
$this->directiveHashCollection->addGenericHashValue(Directive::StyleSrcElem, $integrity);
} catch (\LogicException) {
// integrity format not recognized, skip
}
} else {
$this->addFileHashToCollection(Directive::StyleSrcElem, $file);
}
$tagAttributes = [];
if ($properties['rel'] ?? false) {
$tagAttributes['rel'] = $properties['rel'];
}
$tagAttributes['href'] = $this->getPublicUrlForFile($file, $request);
if ($properties['media'] ?? false) {
$tagAttributes['media'] = $properties['media'];
}
if ($properties['title'] ?? false) {
$tagAttributes['title'] = $properties['title'];
}
if ($properties['integrity'] ?? false) {
$tagAttributes['integrity'] = $properties['integrity'];
}
if ($properties['crossorigin'] ?? false) {
$tagAttributes['crossorigin'] = $properties['crossorigin'];
}
// use nonce if given
if ($this->nonce !== null) {
$tagAttributes['nonce'] = $this->nonce->consumeStatic(Directive::StyleSrcElem);
}
$tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
$tag = '<link ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . $this->endingSlash . '>';
}
if ($properties['allWrap'] ?? false) {
$wrapArr = explode(($properties['splitChar'] ?? false) ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1];
}
$tag .= LF;
return $tag;
}
/**
* Render inline CSS
*/
protected function renderCssInline(): string
{
if (empty($this->cssInline)) {
return '';
}
$cssItems = [0 => [], 1 => []];
foreach ($this->cssInline as $name => $properties) {
$useCsp = !empty($properties['csp']);
$nonceKey = (int)$useCsp;
$cssCode = '/*' . htmlspecialchars($name) . '*/' . LF . ($properties['code'] ?? '') . LF;
if ($properties['forceOnTop'] ?? false) {
array_unshift($cssItems[$nonceKey], $cssCode);
} else {
$cssItems[$nonceKey][] = $cssCode;
}
}
$cssItems = array_filter($cssItems);
foreach ($cssItems as $useCsp => $items) {
$assembledContent = implode('', $items);
if ($useCsp) {
// Hash the full assembled content as it appears inside the <style> tag
$this->directiveHashCollection->addInlineHash(Directive::StyleSrcElem, LF . $assembledContent . LF);
}
$attributes = $useCsp && $this->nonce !== null ? ['nonce' => $this->nonce->consumeInline(Directive::StyleSrcElem)] : [];
$cssItems[$useCsp] = $this->wrapInlineStyle($assembledContent, $attributes);
}
return implode(LF, $cssItems);
}
/**
* Render JavaScript libraries
*
* @return string[] jsLibs and jsFooterLibs strings
*/
protected function renderAdditionalJavaScriptLibraries(ServerRequestInterface $request): array
{
$jsLibs = '';
$jsFooterLibs = '';
if (!empty($this->jsLibs)) {
foreach ($this->jsLibs as $properties) {
// collect CSP hash - use integrity attribute if given, else hash file content
$integrity = $properties['integrity'] ?? '';
if ($integrity !== '') {
try {
$this->directiveHashCollection->addGenericHashValue(Directive::ScriptSrcElem, $integrity);
} catch (\LogicException) {
// integrity format not recognized, skip
}
} else {
$this->addFileHashToCollection(Directive::ScriptSrcElem, $properties['file']);
}
$tagAttributes = [];
$tagAttributes['src'] = $this->getPublicUrlForFile($properties['file'], $request);
if ($properties['type'] ?? false) {
$tagAttributes['type'] = $properties['type'];
}
if ($properties['async'] ?? false) {
$tagAttributes['async'] = 'async';
}
if ($properties['defer'] ?? false) {
$tagAttributes['defer'] = 'defer';
}
if ($properties['nomodule'] ?? false) {
$tagAttributes['nomodule'] = 'nomodule';
}
if ($properties['integrity'] ?? false) {
$tagAttributes['integrity'] = $properties['integrity'];
}
if ($properties['crossorigin'] ?? false) {
$tagAttributes['crossorigin'] = $properties['crossorigin'];
}
// use nonce if given
if ($this->nonce !== null) {
$tagAttributes['nonce'] = $this->nonce->consumeStatic(Directive::ScriptSrcElem);
}
$tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
$tag = '<script ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '></script>';
if ($properties['allWrap'] ?? false) {
$wrapArr = explode(($properties['splitChar'] ?? false) ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1];
}
$tag .= LF;
if ($properties['forceOnTop'] ?? false) {
if (($properties['section'] ?? 0) === self::PART_HEADER) {
$jsLibs = $tag . $jsLibs;
} else {
$jsFooterLibs = $tag . $jsFooterLibs;
}
} elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
$jsLibs .= $tag;
} else {
$jsFooterLibs .= $tag;
}
}
}
if ($this->moveJsFromHeaderToFooter) {
$jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
$jsLibs = '';
}
return [$jsLibs, $jsFooterLibs];
}
/**
* Render JavaScript files
*
* @return string[] jsFiles and jsFooterFiles strings
*/
protected function renderJavaScriptFiles(ServerRequestInterface $request): array
{
$jsFiles = '';
$jsFooterFiles = '';
if (!empty($this->jsFiles)) {
foreach ($this->jsFiles as $properties) {
// collect CSP hash - use integrity attribute if given, else hash file content
$integrity = $properties['integrity'] ?? '';
if ($integrity !== '') {
try {
$this->directiveHashCollection->addGenericHashValue(Directive::ScriptSrcElem, $integrity);
} catch (\LogicException) {
// integrity format not recognized, skip
}
} else {
$this->addFileHashToCollection(Directive::ScriptSrcElem, $properties['file']);
}
$tagAttributes = [];
$tagAttributes['src'] = $this->getPublicUrlForFile($properties['file'], $request);
if ($properties['type'] ?? false) {
$tagAttributes['type'] = $properties['type'];
}
if ($properties['async'] ?? false) {
$tagAttributes['async'] = 'async';
}
if ($properties['defer'] ?? false) {
$tagAttributes['defer'] = 'defer';
}
if ($properties['nomodule'] ?? false) {
$tagAttributes['nomodule'] = 'nomodule';
}
if ($properties['integrity'] ?? false) {
$tagAttributes['integrity'] = $properties['integrity'];
}
if ($properties['crossorigin'] ?? false) {
$tagAttributes['crossorigin'] = $properties['crossorigin'];
}
// use nonce if given
if ($this->nonce !== null) {
$tagAttributes['nonce'] = $this->nonce->consumeStatic(Directive::ScriptSrcElem);
}
$tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
$tag = '<script ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '></script>';
if ($properties['allWrap'] ?? false) {
$wrapArr = explode(($properties['splitChar'] ?? false) ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1];
}
$tag .= LF;
if ($properties['forceOnTop'] ?? false) {
if (($properties['section'] ?? 0) === self::PART_HEADER) {
$jsFiles = $tag . $jsFiles;
} else {
$jsFooterFiles = $tag . $jsFooterFiles;
}
} elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
$jsFiles .= $tag;
} else {
$jsFooterFiles .= $tag;
}
}
}
if ($this->moveJsFromHeaderToFooter) {
$jsFooterFiles = $jsFiles . $jsFooterFiles;
$jsFiles = '';
}
return [$jsFiles, $jsFooterFiles];
}
/**
* Render inline JavaScript (must not apply `nonce="..."` if defined).
*
* @return string[] jsInline and jsFooterInline string
*/
protected function renderInlineJavaScript(): array
{
if (empty($this->jsInline)) {
return ['', ''];
}
$regularItems = [0 => [], 1 => []];
$footerItems = [0 => [], 1 => []];
foreach ($this->jsInline as $name => $properties) {
$useCsp = !empty($properties['csp']);
$nonceKey = (int)$useCsp;
$jsCode = '/*' . htmlspecialchars($name) . '*/' . LF . ($properties['code'] ?? '') . LF;
if ($properties['forceOnTop'] ?? false) {
if (($properties['section'] ?? 0) === self::PART_HEADER) {
array_unshift($regularItems[$nonceKey], $jsCode);
} else {
array_unshift($footerItems[$nonceKey], $jsCode);
}
} elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
$regularItems[$nonceKey][] = $jsCode;
} else {
$footerItems[$nonceKey][] = $jsCode;
}
}
$regularItems = array_filter($regularItems);
$footerItems = array_filter($footerItems);
foreach ($regularItems as $useCsp => $items) {
$assembledContent = implode('', $items);
if ($useCsp) {
// Hash the full assembled content as it appears inside the <script> tag
$this->directiveHashCollection->addInlineHash(Directive::ScriptSrcElem, LF . $assembledContent . LF);
}
$attributes = $useCsp && $this->nonce !== null ? ['nonce' => $this->nonce->consumeInline(Directive::ScriptSrcElem)] : [];
$regularItems[$useCsp] = $this->wrapInlineScript($assembledContent, $attributes);
}
foreach ($footerItems as $useCsp => $items) {
$assembledContent = implode('', $items);
if ($useCsp) {
// Hash the full assembled content as it appears inside the <script> tag
$this->directiveHashCollection->addInlineHash(Directive::ScriptSrcElem, LF . $assembledContent . LF);
}
$attributes = $useCsp && $this->nonce !== null ? ['nonce' => $this->nonce->consumeInline(Directive::ScriptSrcElem)] : [];
$footerItems[$useCsp] = $this->wrapInlineScript($assembledContent, $attributes);
}
$regularCode = implode(LF, $regularItems);
$footerCode = implode(LF, $footerItems);
if ($this->moveJsFromHeaderToFooter) {
$footerCode = $regularCode . $footerCode;
$regularCode = '';
}
return [$regularCode, $footerCode];
}
/**
* Reads a locallang file.
*
* @param string $fileRef Reference to a relative filename to include.
* @return array Returns the $LOCAL_LANG array found in the file. If no array found, returns empty array.
*/
protected function readLLfile(string $fileRef): array
{
$languageService = $this->languageServiceFactory->create($this->locale);
return $languageService->getLabelsFromResource($fileRef);
}
private function handleAddedResource(string|StaticResourceInterface $potentialResource): StaticResourceInterface
{
if ($potentialResource instanceof StaticResourceInterface) {
return $potentialResource;
}
return $this->systemResourceFactory->createResource($potentialResource);
}
/**
* This function acts as a wrapper to allow relative and paths starting with EXT: to be dealt with
* in this very case to always return the "absolute web path" to be included directly before output.
*
* This is mainly added so the EXT: syntax can be resolved for PageRenderer in one central place,
* and hopefully removed in the future by one standard API call.
*
* The file is also prepared as version numbered file and prefixed as absolute webpath
*
* @param string $file the filename to process
*/
protected function getPublicUrlForFile(string $file, ServerRequestInterface $request): string
{
$resource = $this->systemResourceFactory->createPublicResource($file);
return (string)$this->resourcePublisher->generateUri($resource, $request);
}
/**
* Execute PreRenderHook for possible manipulation
*/
protected function executePreRenderHook(): void
{
$hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-preProcess'] ?? false;
if (!$hooks) {
return;
}
// @todo: $jsFooterFiles and $jsFooterInline and $jsFooterLibs can be removed once the hook is adapted / replaced
$jsFooterFiles = $jsFooterLibs = $jsFooterInline = [];
$params = [
'jsLibs' => &$this->jsLibs,
'jsFooterLibs' => &$jsFooterLibs,
'jsFiles' => &$this->jsFiles,
'jsFooterFiles' => &$jsFooterFiles,
'cssLibs' => &$this->cssLibs,
'cssFiles' => &$this->cssFiles,
'headerData' => &$this->headerData,
'footerData' => &$this->footerData,
'jsInline' => &$this->jsInline,
'jsFooterInline' => &$jsFooterInline,
'cssInline' => &$this->cssInline,
];
foreach ($hooks as $hook) {
GeneralUtility::callUserFunction($hook, $params, $this);
}
}
/**
* PostTransform for possible manipulation of concatenated and compressed files
*/
protected function executeRenderPostTransformHook(): void
{
$hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postTransform'] ?? false;
if (!$hooks) {
return;
}
// @todo: $jsFooterFiles and $jsFooterInline and $jsFooterLibs can be removed once the hook is adapted / replaced
$jsFooterFiles = $jsFooterLibs = $jsFooterInline = [];
$params = [
'jsLibs' => &$this->jsLibs,
'jsFooterLibs' => &$jsFooterLibs,
'jsFiles' => &$this->jsFiles,
'jsFooterFiles' => &$jsFooterFiles,
'cssLibs' => &$this->cssLibs,
'cssFiles' => &$this->cssFiles,
'headerData' => &$this->headerData,
'footerData' => &$this->footerData,
'jsInline' => &$this->jsInline,
'jsFooterInline' => &$jsFooterInline,
'cssInline' => &$this->cssInline,
];
foreach ($hooks as $hook) {
GeneralUtility::callUserFunction($hook, $params, $this);
}
}
/**
* Execute postRenderHook for possible manipulation
*
* @param string $jsLibs
* @param string $jsFiles
* @param string $jsFooterFiles
* @param string $cssLibs
* @param string $cssFiles
* @param string $jsInline
* @param string $cssInline
* @param string $jsFooterInline
* @param string $jsFooterLibs
*/
protected function executePostRenderHook(&$jsLibs, &$jsFiles, &$jsFooterFiles, &$cssLibs, &$cssFiles, &$jsInline, &$cssInline, &$jsFooterInline, &$jsFooterLibs): void
{
$hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postProcess'] ?? false;
if (!$hooks) {
return;
}
$params = [
'jsLibs' => &$jsLibs,
'jsFiles' => &$jsFiles,
'jsFooterFiles' => &$jsFooterFiles,
'cssLibs' => &$cssLibs,
'cssFiles' => &$cssFiles,
'headerData' => &$this->headerData,
'footerData' => &$this->footerData,
'jsInline' => &$jsInline,
'cssInline' => &$cssInline,
'xmlPrologAndDocType' => &$this->xmlPrologAndDocType,
'htmlTag' => &$this->htmlTag,
'headTag' => &$this->headTag,
'shortcutTag' => &$this->shortcutTag,
'inlineComments' => &$this->inlineComments,
'favIcon' => &$this->favIcon,
'iconMimeType' => &$this->iconMimeType,
'titleTag' => &$this->titleTag,
'title' => &$this->title,
'jsFooterInline' => &$jsFooterInline,
'jsFooterLibs' => &$jsFooterLibs,
'bodyContent' => &$this->bodyContent,
];
foreach ($hooks as $hook) {
GeneralUtility::callUserFunction($hook, $params, $this);
}
}
/**
* Creates a CSS inline tag
*
* @param SystemResourceInterface $resource the resource to process
*/
protected function createInlineCssTagFromFile(SystemResourceInterface $resource, array $properties, ServerRequestInterface $request): string
{
try {
$cssInline = $resource->getContents();
} catch (SystemResourceDoesNotExistException) {
return '';
}
$cssInlineFix = $this->relativeCssPathFixer->fixRelativeUrlPaths($cssInline, PathUtility::dirname($resource->getResourceIdentifier()) . '/', $request);
// collect CSP hash - covers the content as it appears inside the <style> tag
$this->directiveHashCollection->addInlineHash(Directive::StyleSrcElem, LF . $cssInlineFix . LF);
$tagAttributes = [];
if ($properties['media'] ?? false) {
$tagAttributes['media'] = $properties['media'];
}
if ($properties['title'] ?? false) {
$tagAttributes['title'] = $properties['title'];
}
// use nonce if given - special case, since content is created from a static file
if ($this->nonce !== null) {
$tagAttributes['nonce'] = $this->nonce->consumeInline(Directive::StyleSrcElem);
}
$tagAttributes = array_merge($tagAttributes, $properties['tagAttributes'] ?? []);
return $this->wrapInlineStyle($cssInlineFix, $tagAttributes);
}
protected function wrapInlineStyle(string $content, array $attributes = []): string
{
$styleTag = "<style%s>\n%s\n</style>\n";
if ($this->docType !== DocType::html5 || $this->docType->isXmlCompliant()) {
$styleTag = "<style%s>\n/*<![CDATA[*/\n<!-- \n%s-->\n/*]]>*/\n</style>\n";
}
$attributesList = GeneralUtility::implodeAttributes($attributes, true);
return sprintf(
$styleTag,
$attributesList !== '' ? ' ' . $attributesList : '',
$content
);
}
protected function wrapInlineScript(string $content, array $attributes = []): string
{
$scriptTag = "<script%s>\n%s\n</script>\n";
// * Whenever HTML5 is used, remove the "text/javascript" type from the wrap
// since this is not needed and may lead to validation errors in the future.
// * Whenever XHTML gets disabled, remove the "text/javascript" type from the wrap
// since this is not needed and may lead to validation errors in the future.
if ($this->docType !== DocType::html5 || $this->docType->isXmlCompliant()) {
$attributes['type'] = 'text/javascript';
$scriptTag = "<script%s>\n/*<![CDATA[*/\n%s/*]]>*/\n</script>\n";
}
$attributesList = GeneralUtility::implodeAttributes($attributes, true);
return sprintf(
$scriptTag,
$attributesList !== '' ? ' ' . $attributesList : '',
$content
);
}
/**
* String 'FE' if in FrontendApplication, 'BE' otherwise (also in CLI without request object)
*/
protected function getApplicationType(ServerRequestInterface $request): string
{
if (ApplicationType::fromRequest($request)->isFrontend()) {
return 'FE';
}
return 'BE';
}
private function getBackendUser(): BackendUserAuthentication
{
if (!$GLOBALS['BE_USER'] instanceof BackendUserAuthentication) {
throw new \RuntimeException('No backend user found.', 1765402790);
}
return $GLOBALS['BE_USER'];
}
}