1906 lines
78 KiB
PHP
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'];
|
|
}
|
|
|
|
}
|