TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
/**
* A PSR-14 event fired when sys_template rows have been fetched.
*
* This event is intended to add own rows based on given rows or site resolution.
*/
final class AfterTemplatesHaveBeenDeterminedEvent
{
public function __construct(
private readonly array $rootline,
private readonly ?ServerRequestInterface $request,
private array $templateRows,
) {}
public function getRootline(): array
{
return $this->rootline;
}
public function getRequest(): ?ServerRequestInterface
{
return $this->request;
}
/**
* Convenience method to directly retrieve the Site. May be null though!
*/
public function getSite(): ?SiteInterface
{
return $this->request?->getAttribute('site');
}
public function getTemplateRows(): array
{
return $this->templateRows;
}
public function setTemplateRows(array $templateRows): void
{
$this->templateRows = $templateRows;
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Event;
/**
* Extensions can add global page TSconfig right before they are loaded from other sources
* like the global page.tsconfig file.
*
* Note: The added config should not depend on runtime / request. This is considered static
* config and thus should be identical on every request.
*/
final class BeforeLoadedPageTsConfigEvent
{
public function __construct(private array $tsConfig = []) {}
public function getTsConfig(): array
{
return $this->tsConfig;
}
public function addTsConfig(string $tsConfig): void
{
$this->tsConfig[] = $tsConfig;
}
public function setTsConfig(array $tsConfig): void
{
$this->tsConfig = $tsConfig;
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Event;
/**
* Extensions can add global user TSconfig right before they are loaded from other sources
* like the global user.tsconfig file.
*
* Note: The added config should not depend on runtime / request. This is considered static
* config and thus should be identical on every request.
*/
final class BeforeLoadedUserTsConfigEvent
{
public function __construct(private array $tsConfig = []) {}
public function getTsConfig(): array
{
return $this->tsConfig;
}
public function addTsConfig(string $tsConfig): void
{
$this->tsConfig[] = $tsConfig;
}
public function setTsConfig(array $tsConfig): void
{
$this->tsConfig = $tsConfig;
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Event;
/**
* Extensions can modify page TSconfig entries that can be overridden or added, based on the root line
*/
final class ModifyLoadedPageTsConfigEvent
{
public function __construct(private array $tsConfig, private readonly array $rootLine) {}
public function getTsConfig(): array
{
return $this->tsConfig;
}
public function addTsConfig(string $tsConfig): void
{
$this->tsConfig[] = $tsConfig;
}
public function setTsConfig(array $tsConfig): void
{
$this->tsConfig = $tsConfig;
}
public function getRootLine(): array
{
return $this->rootLine;
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
/**
* Base implementation of condition nodes.
*
* @internal: Internal tree structure.
*/
abstract class AbstractConditionInclude extends AbstractInclude implements IncludeConditionInterface
{
protected Token $conditionValueToken;
protected ?Token $originalConditionValueToken = null;
protected bool $verdict;
/**
* Add the condition token to cache when serialized. See __serialize() of AbstractInclude.
*/
protected function serialize(): array
{
$result = parent::serialize();
$result['conditionValueToken'] = $this->conditionValueToken;
return $result;
}
public function setConditionToken(Token $token): void
{
if ($token->getType() !== TokenType::T_VALUE) {
throw new \LogicException('Token must be of type T_VALUE', 1655977210);
}
$this->conditionValueToken = $token;
}
public function getConditionToken(): Token
{
return $this->conditionValueToken;
}
public function setOriginalConditionToken(Token $token): void
{
if ($token->getType() !== TokenType::T_VALUE) {
throw new \LogicException('Token must be of type T_VALUE', 1655977211);
}
$this->originalConditionValueToken = $token;
}
public function getOriginalConditionToken(): ?Token
{
return $this->originalConditionValueToken;
}
public function isConditionNegated(): bool
{
return false;
}
public function setConditionVerdict(bool $verdict): void
{
$this->verdict = $verdict;
}
public function getConditionVerdict(): bool
{
return $this->verdict;
}
}
@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineInterface;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
/**
* Base implementation of IncludeInterface.
*
* @internal: Internal tree structure.
*/
abstract class AbstractInclude implements IncludeInterface
{
private ?string $identifier = null;
protected string $name = '';
protected string $path = '';
/**
* @var array<int, IncludeInterface>
*/
protected array $children = [];
protected ?LineStream $lineStream = null;
protected ?LineInterface $originalTokenLine = null;
protected bool $isSplit = false;
protected bool $root = false;
protected bool $clear = false;
protected ?int $pid = null;
/**
* When storing to cache, we only store FE relevant properties and skip
* things like "name", "identifier" and friends. We also don't need the
* LineStream when a node is split.
*/
final public function __serialize(): array
{
return $this->serialize();
}
protected function serialize(): array
{
$result['children'] = $this->children;
if ($this->isSplit()) {
$result['isSplit'] = true;
}
if (!$this->isSplit()) {
$result['lineStream'] = $this->lineStream;
}
if ($this->isRoot()) {
$result['root'] = true;
}
if ($this->isClear()) {
$result['clear'] = true;
}
return $result;
}
public function getType(): string
{
$classWithNamespace = static::class;
$lastBackslash = strrpos($classWithNamespace, '\\');
return substr($classWithNamespace, $lastBackslash + 1, -7);
}
public function setIdentifier(string $identifier): void
{
$this->identifier = hash('xxh3', $identifier);
$childCounter = 0;
foreach ($this->getNextChild() as $child) {
$child->setIdentifier($this->identifier . $childCounter);
$childCounter++;
}
}
public function getIdentifier(): string
{
if ($this->identifier === null) {
throw new \RuntimeException(
'Identifier has not been initialized. This happens when getIdentifier() is called on'
. ' trees retrieved from cache. The identifier is not supposed to be used in this context.',
1673634853
);
}
return $this->identifier;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
public function setPath(string $path): void
{
$this->path = $path;
}
public function getPath(): string
{
return $this->path;
}
public function addChild(IncludeInterface $node): void
{
$this->children[] = $node;
}
public function hasChildren(): bool
{
return !empty($this->children);
}
public function getNextChild(): iterable
{
foreach ($this->children as $child) {
yield $child;
}
}
public function isSysTemplateRecord(): bool
{
return false;
}
public function setLineStream(?LineStream $lineStream): void
{
$this->lineStream = $lineStream;
}
public function getLineStream(): ?LineStream
{
return $this->lineStream;
}
public function setOriginalLine(LineInterface $line): void
{
$this->originalTokenLine = $line;
}
public function getOriginalLine(): ?LineInterface
{
return $this->originalTokenLine;
}
public function setSplit(): void
{
$this->isSplit = true;
}
public function isSplit(): bool
{
return $this->isSplit;
}
public function setRoot(bool $root): void
{
$this->root = $root;
}
public function isRoot(): bool
{
return $this->root;
}
public function setClear(bool $clear): void
{
$this->clear = $clear;
}
public function isClear(): bool
{
return $this->clear;
}
public function setPid(int $pid): void
{
$this->pid = $pid;
}
public function getPid(): ?int
{
return $this->pid;
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node representing an "@import" include. The LineStream is set
* to the content of the included source, which can be split again
* if that source contains further conditions or includes.
*
* @internal: Internal tree structure.
*/
final class AtImportInclude extends AbstractInclude {}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node representing the [ELSE] body of a condition:
*
* [foo = bar]
* ...
* [ELSE]
* baz = bazValue
*
* The LineStream is the body of the else block, the condition token
* is set to the token of the condition "[foo = bar]".
*
* @internal: Internal tree structure.
*/
final class ConditionElseInclude extends AbstractConditionInclude
{
public function isConditionNegated(): bool
{
return true;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node representing a condition and its body.
*
* [foo = bar]
* baz = bazValue
* [END]
*
* @internal: Internal tree structure.
*/
final class ConditionInclude extends AbstractConditionInclude {}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A simple include representing [END] and [GLOBAL] lines.
*
* @internal: Internal tree structure.
*/
final class ConditionStopInclude extends AbstractInclude
{
public function addChild(IncludeInterface $node): void
{
throw new \LogicException('ConditionStopInclude can not have children', 1717691734);
}
public function hasChildren(): bool
{
return false;
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node created for "default TypoScript" from globals, content from:
* $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_[constants|setup]'].
*
* @internal: Internal tree structure.
*/
final class DefaultTypoScriptInclude extends AbstractInclude {}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node created for "magic" include from globals, when processing
* $GLOBALS['TYPO3_CONF_VARS ']['FE']['defaultTypoScript_[constants|setup]']
*
* @internal: Internal tree structure.
*/
final class DefaultTypoScriptMagicKeyInclude extends AbstractInclude {}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A node created for "extension static" TypoScript auto-include files:
* EXT:my_extension/ext_typoscript_[constants|setup].typoscript
*
* @internal: Internal tree structure.
*/
final class ExtensionStaticInclude extends AbstractInclude {}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A classic include from sys_template "include_static_file":
* EXT:/My/Path/[constants|setup].[typoscript|ts|txt]
*
* This is always a child of an IncludeStaticFileDatabaseInclude.
*
* @internal: Internal tree structure.
*/
final class FileInclude extends AbstractInclude {}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
/**
* Source streams that contain conditions are split smaller parts
* and each condition creates a Condition node.
*
* This interface is implemented by all conditions nodes. It allows
* "parking" the main condition token to be evaluated during AST building.
*
* @internal: Internal tree structure.
*/
interface IncludeConditionInterface
{
/**
* Set and get the condition token: "[foo = bar]"
*/
public function setConditionToken(Token $token): void;
public function getConditionToken(): Token;
/**
* Conditions may use constants: "[foo = {$bar}]". This getter/setter
* allows storing the original condition token string.
* This is set in backend only in case a constant substitution has taken
* place. Otherwise, the "vanilla" condition token is identical,
* getOriginalConditionToken() returns null and the condition token should
* be fetched from getConditionToken().
*/
public function setOriginalConditionToken(Token $token): void;
public function getOriginalConditionToken(): ?Token;
/**
* True for ConditionElseInclude: The [ELSE] node of a condition.
*/
public function isConditionNegated(): bool;
/**
* When a condition is evaluated, this is set to true of false
* depending on the condition result.
*/
public function setConditionVerdict(bool $verdict): void;
public function getConditionVerdict(): bool;
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineInterface;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
/**
* General interface of IncludeTree tree nodes.
*
* The TreeBuilder classes return a tree of these nodes, with the root node being
* a RootInclude. Each "include type" is represented by an own class: There
* is for instance "SysTemplateInclude" for a node that represents a sys_template
* row, and DefaultTypoScriptInclude for the default TypoScript string included from
* TYPO3_CONF_VARS.
*
* Nodes may have children, and a single stream of lines from the tokenizer
* may be split into multiple children: Each @import creates an own child node,
* and conditions trigger splitting as well.
*
* @internal: Internal tree structure.
*/
interface IncludeInterface
{
/**
* A human-readable string derived from class name - Used in BE template analyzer
*/
public function getType(): string;
/**
* An identifier for this include. Typically, a hash of some kind. This identifier
* is unique within the tree, by being created from the parent identifier plus
* something unique for this level like a counter. This identifier is used in the backend,
* when referencing single includes to be rendered.
* Calculating identifiers is initiated by calling setIdentifier() on RootNode, which
* will recurse the tree. Call this on the final tree, after include calculation finished,
* so include building itself does not need to fiddle with identifier updates.
* Note this value is skipped when persisting to caches since it's a Backend related
* thing that does not use cached context: When retrieving includes from cache
* (e.g. in Frontend), the identifier is null and calling the getter will throw an exception.
*/
public function setIdentifier(string $identifier): void;
public function getIdentifier(): string;
/**
* A human-readable version of the identifier: Used in backend tree rendering.
*/
public function setName(string $name): void;
public function getName(): string;
/**
* This is set to a non-empty string for includes that represent files. The file location
* is stored here, typically something like "EXT:my_extension/path/to/foo.typoscript".
* This is used when resolving file includes relative to a parent include, so a
* potential child node knows where to look relative to its parent path.
* Note this value is skipped when persisting to caches: The parent path
* information is no longer needed when a tree is fetched from cache since
* all children were attached already and don't need to be recalculated
* depending on their parent path value.
*/
public function setPath(string $path): void;
public function getPath(): string;
/**
* Child maintenance methods.
*/
public function addChild(IncludeInterface $node): void;
public function hasChildren(): bool;
/**
* @return iterable<IncludeInterface>
*/
public function getNextChild(): iterable;
/**
* True for IncludeTypoScriptInclude - this node represents a sys_template record.
* When true, methods like isRoot() and isClear() are relevant.
*/
public function isSysTemplateRecord(): bool;
/**
* The source split into single lines by a tokenizer.
*/
public function setLineStream(?LineStream $lineStream): void;
public function getLineStream(): ?LineStream;
/**
* When an imports are handled, such a line is substituted by the included
* content. To be able to still output the original line, it is parked here.
* Relevant in backend tree and source display only.
*/
public function setOriginalLine(LineInterface $line): void;
public function getOriginalLine(): ?LineInterface;
/**
* When included line streams contain conditions or imports, the node is split into
* children that contain single segments of the source. The node itself is then just
* a container and the LineStream attached is irrelevant for further processing.
* This flag is set when a line stream is split and the children fully represent the source.
*/
public function setSplit(): void;
public function isSplit(): bool;
/**
* Set to true for IncludeTypoScriptInclude's (sys_template records) when "root" flag is set.
*/
public function setRoot(bool $root): void;
public function isRoot(): bool;
/**
* Set to true for IncludeTypoScriptInclude's (sys_template records) when "clear constants"
* or "clear setup" is set. Depends on context if currently constants or setup are parsed.
*/
public function setClear(bool $clear): void;
public function isClear(): bool;
/**
* Set to the pid of IncludeTypoScriptInclude's (sys_template records). Relevant in backend
* tree rendering only.
*/
public function setPid(int $pid): void;
public function getPid(): ?int;
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* Main node created for sys_template "include_static_file":
* This has FileInclude or IncludeStaticFileFileInclude children,
* depending on specific string.
*
* @internal: Internal tree structure.
*/
final class IncludeStaticFileDatabaseInclude extends AbstractInclude {}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* Created when a sys_template "static_file_include" includes "include_static_file.txt" files:
* EXT:/My/Path/include_static_file.txt
*
* @internal: Internal tree structure.
*/
final class IncludeStaticFileFileInclude extends AbstractInclude {}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* Root of the IncludeTree. Does not contain LineStreams itself,
* only children do.
*
* @internal: Internal tree structure.
*/
final class RootInclude extends AbstractInclude
{
protected string $name = 'ROOT';
public function setName(string $name): void
{
throw new \LogicException('Can not set name on RootNode', 1656668001);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* When a source stream is split into children because the LineStream contains
* conditions or imports, this node represents TypoScript that is not within
* condition or import context, the "baz = bazValue" part in the example below:
*
* [foo=bar]
* ...
* [END]
* baz = bazValue
*
* @internal: Internal tree structure.
*/
final class SegmentInclude extends AbstractInclude {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* Include node created for includes from Site objects. Only relevant for constants.
*
* @internal: Internal tree structure.
*/
final class SiteInclude extends AbstractInclude {}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* The main node created for TypoScript from site sets
* and %configPath/sites/{constants,setup}.typoscript.
*
* @internal: Internal tree structure.
*/
final class SiteTemplateInclude extends AbstractInclude
{
protected bool $root = true;
protected bool $clear = true;
public function isRoot(): bool
{
return true;
}
public function isClear(): bool
{
return true;
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* A simple include type used by StringTreeBuilder when a single entry
* TypoScript snipped is parsed.
*
* @internal: Internal tree structure.
*/
final class StringInclude extends AbstractInclude {}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* The main node created for sys_template rows.
*
* @internal: Internal tree structure.
*/
final class SysTemplateInclude extends AbstractInclude
{
public function isSysTemplateRecord(): bool
{
return true;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\IncludeNode;
/**
* An include type used by user and pages TsConfig for single TsConfig snippets.
*
* @internal: Internal tree structure.
*/
final class TsConfigInclude extends AbstractInclude {}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\StringInclude;
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
/**
* Parse a single TypoScript string, supporting imports and conditions.
*
* This is a relatively simple "tree" builder: It gets a single TypoScript string
* snippet, tokenizes it and creates a RootInclude "tree". The string is scanned
* for imports and conditions: Those create sub includes, just like the other
* TreeBuilder classes do.
*
* @internal
*/
#[Autoconfigure(public: true)]
final readonly class StringTreeBuilder
{
public function __construct(
private TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
) {}
/**
* Create tree, ready to be traversed. Will cache if $cache is not null.
*
* @param non-empty-string $name A name used as cache identifier, [a-z,A-Z,-] only
*/
public function getTreeFromString(
string $name,
string $typoScriptString,
TokenizerInterface $tokenizer,
?PhpFrontend $cache = null,
): RootInclude {
$lowerCaseName = mb_strtolower($name);
$identifier = 'string-' . $lowerCaseName . '-' . hash('xxh3', $typoScriptString);
if ($cache) {
$includeTree = $cache->require($identifier);
if ($includeTree instanceof RootInclude) {
return $includeTree;
}
}
$includeTree = new RootInclude();
$includeNode = new StringInclude();
$includeNode->setName('[string] ' . $name);
$includeNode->setLineStream($tokenizer->tokenize($typoScriptString));
$this->treeFromTokenStreamBuilder->buildTree($includeNode, 'other', $tokenizer);
$includeTree->addChild($includeNode);
$cache?->set($identifier, $this->prepareTreeForCache($includeTree));
return $includeTree;
}
private function prepareTreeForCache(RootInclude $node): string
{
return 'return unserialize(\'' . addcslashes(serialize($node), '\'\\') . '\');';
}
}
@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\AfterTemplatesHaveBeenDeterminedEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fetch relevant sys_template records from database by given page rootline.
*
* The result sys_template rows are fed to the SysTemplateTreeBuilder for processing.
*
* @internal: Internal structure. There is optimization potential and especially getSysTemplateRowsByRootline() will probably vanish later.
*/
#[Autoconfigure(public: true)]
final readonly class SysTemplateRepository
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private ConnectionPool $connectionPool,
private Context $context,
) {}
/**
* To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages.
* When there are multiple active sys_template rows on a page, we pick the one with the lower sorting
* value.
*
* The query implementation below does that with *one* query for all rootline pages at once, not
* one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but
* the implementation should scale nearly O(1) instead of O(n) with the rootline depth.
*
* @param ServerRequestInterface|null $request Nullable since Request is not a hard dependency ond just convenient for the Event
*
* @todo: It's potentially possible to get rid of this method in the frontend by joining sys_template
* into the Page rootline resolving as soon as it uses a CTE: This would save one query in *all* FE
* requests, even for fully-cached page requests.
*/
public function getSysTemplateRowsByRootline(array $rootline, ?ServerRequestInterface $request = null, ?VisibilityAspect $visibility = null): array
{
if ($rootline === []) {
return [];
}
// Site-root node first!
$rootLinePageIds = array_reverse(array_column($rootline, 'uid'));
$sysTemplateRows = [];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer($visibility));
$queryBuilder->select('sys_template.*')->from('sys_template');
// Build a value list as joined table to have sorting based on list sorting
$valueList = [];
foreach ($rootLinePageIds as $sorting => $rootLinePageId) {
$valueList[] = sprintf(
'%s, %s',
$queryBuilder->expr()->castInt(
$queryBuilder->createNamedParameter($rootLinePageId, Connection::PARAM_INT),
'uid',
),
$queryBuilder->expr()->castInt(
$queryBuilder->createNamedParameter($sorting, Connection::PARAM_INT),
'sorting',
)
);
}
$valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList);
$queryBuilder->getConcreteQueryBuilder()->innerJoin(
$queryBuilder->quoteIdentifier('sys_template'),
sprintf('(%s)', $valueList),
$queryBuilder->quoteIdentifier('pidlist'),
'(' . $queryBuilder->expr()->eq(
'sys_template.pid',
$queryBuilder->quoteIdentifier('pidlist.uid')
) . ')'
);
// Sort by rootline determined depth as sort criteria
$queryBuilder->orderBy('pidlist.sorting', 'ASC')
->addOrderBy('sys_template.root', 'DESC')
->addOrderBy('sys_template.sorting', 'ASC');
$lastPid = null;
$queryResult = $queryBuilder->executeQuery();
while ($sysTemplateRow = $queryResult->fetchAssociative()) {
// We're retrieving *all* templates per pid, but need the first one only. The
// order restriction above at least takes care they're after-each-other per pid.
if ($lastPid === (int)$sysTemplateRow['pid']) {
continue;
}
$lastPid = (int)$sysTemplateRow['pid'];
$sysTemplateRows[] = $sysTemplateRow;
}
$event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $request, $sysTemplateRows);
$this->eventDispatcher->dispatch($event);
return $event->getTemplateRows();
}
/**
* To calculate the TS include tree, we have to find sys_template rows attached to all rootline pages.
* When there are multiple active sys_template rows on a page, we pick the one with the lower sorting
* value.
*
* This variant is tailored for ext:tstemplate use. It allows "overriding" the sys_template uid of
* the deepest page, which is used when multiple sys_template records on one page are managed in the Backend.
*
* The query implementation below does that with *one* query for all rootline pages at once, not
* one query per page. To handle the capabilities mentioned above, the query is a bit nifty, but
* the implementation should scale nearly O(1) instead of O(n) with the rootline depth.
*/
public function getSysTemplateRowsByRootlineWithUidOverride(array $rootline, ?ServerRequestInterface $request, int $templateUidOnDeepestRootline, ?VisibilityAspect $visibility = null): array
{
// Site-root node first!
$rootLinePageIds = array_reverse(array_column($rootline, 'uid'));
$templatePidOnDeepestRootline = array_first($rootline)['uid'];
$sysTemplateRows = [];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer($visibility));
$queryBuilder->select('sys_template.*')->from('sys_template');
if ($templateUidOnDeepestRootline && $templatePidOnDeepestRootline) {
$queryBuilder->andWhere(
$queryBuilder->expr()->or(
$queryBuilder->expr()->neq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, Connection::PARAM_INT)),
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('sys_template.pid', $queryBuilder->createNamedParameter($templatePidOnDeepestRootline, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('sys_template.uid', $queryBuilder->createNamedParameter($templateUidOnDeepestRootline, Connection::PARAM_INT)),
),
),
);
}
// Build a value list as joined table to have sorting based on list sorting
$valueList = [];
foreach ($rootLinePageIds as $sorting => $rootLinePageId) {
$valueList[] = sprintf(
'%s, %s',
$queryBuilder->expr()->castInt(
$queryBuilder->createNamedParameter($rootLinePageId, Connection::PARAM_INT),
'uid',
),
$queryBuilder->expr()->castInt(
$queryBuilder->createNamedParameter($sorting, Connection::PARAM_INT),
'sorting',
),
);
}
$valueList = 'SELECT ' . implode(' UNION ALL SELECT ', $valueList);
$queryBuilder->getConcreteQueryBuilder()->innerJoin(
$queryBuilder->quoteIdentifier('sys_template'),
sprintf('(%s)', $valueList),
$queryBuilder->quoteIdentifier('pidlist'),
'(' . $queryBuilder->expr()->eq(
'sys_template.pid',
$queryBuilder->quoteIdentifier('pidlist.uid')
) . ')'
);
// Sort by rootline determined depth as sort criteria
$queryBuilder->orderBy('pidlist.sorting', 'ASC')
->addOrderBy('sys_template.root', 'DESC')
->addOrderBy('sys_template.sorting', 'ASC');
$lastPid = null;
$queryResult = $queryBuilder->executeQuery();
while ($sysTemplateRow = $queryResult->fetchAssociative()) {
// We're retrieving *all* templates per pid, but need the first one only. The
// order restriction above at least takes care they're after-each-other per pid.
if ($lastPid === (int)$sysTemplateRow['pid']) {
continue;
}
$lastPid = (int)$sysTemplateRow['pid'];
$sysTemplateRows[] = $sysTemplateRow;
}
// @todo: This event should be able to be fired even if the sys_template resolving is
// merged into an early middleware like "SiteResolver" which could join / sub-select
// pages together with sys_template directly, which would be possible if we manage
// to switch away from RootlineUtility usage in SiteResolver by using a CTE instead.
$event = new AfterTemplatesHaveBeenDeterminedEvent($rootline, $request, $sysTemplateRows);
$this->eventDispatcher->dispatch($event);
return $event->getTemplateRows();
}
/**
* Get sys_template record query builder restrictions.
* Allows hidden records if enabled in context.
*/
private function getSysTemplateQueryRestrictionContainer(?VisibilityAspect $visibility = null): DefaultRestrictionContainer
{
$restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
$visibility ??= $this->context->getAspect('visibility');
if ($visibility->includeHiddenContent()) {
$restrictionContainer->removeByType(HiddenRestriction::class);
}
if ($visibility->includeScheduledRecords()) {
$restrictionContainer->removeByType(StartTimeRestriction::class);
$restrictionContainer->removeByType(EndTimeRestriction::class);
}
return $restrictionContainer;
}
}
@@ -0,0 +1,659 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptMagicKeyInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ExtensionStaticInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\FileInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeStaticFileDatabaseInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeStaticFileFileInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SiteInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SiteTemplateInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SysTemplateInclude;
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Create a tree representing all TypoScript includes.
*
* This is the 'middle' part of the TypoScript parsing process: The tokenizers as "lowest"
* structure create line streams from TypoScript, the AST builder as "highest" structure create
* the TypoScript object tree.
*
* This structure gathers all TypoScript snippets that have to be tokenized, and creates a
* tree with include nodes and sub include nodes.
*
* It is called in frontend (and backend "Template" module) with the page rootline, gets all
* attached sys_template records, gets their content and various sub includes and takes care
* of correct include order.
*
* This class together with TreeFromLineStreamBuilder also takes care of conditions and
* imports ("@import"): Those create child nodes in the tree. To evaluate conditions, the
* tree is later traversed, condition verdicts (true / false) are determined, to see if
* condition's child nodes should be considered in AST.
*
* The IncludeTree is "runtime stateless": Constants values and conditions are *not* evaluated
* here, so the tree is always the same for a given rootline. This makes this structure cache-able:
* In frontend, the tree (or sub parts of it) is cached and fetched from cache for next
* call. This means the entire tree-building and tokenizing is suppressed. After that runtime
* information is added: Conditions are evaluated, and the AST is built from given IncludeTree.
*
* @internal: Internal tree structure.
*/
#[Autoconfigure(public: true)]
final class SysTemplateTreeBuilder
{
/**
* Used in 'basedOn' includes to prevent endless loop: Each sys_template row can
* be included only once in 'basedOn'.
*
* @var array<int, int>
*/
private array $includedSysTemplateUids = [];
/** @var 'constants'|'setup' */
private string $type;
private TokenizerInterface $tokenizer;
private ?PhpFrontend $cache = null;
private bool $enableStaticMagicIncludes = false;
public function __construct(
private readonly ConnectionPool $connectionPool,
private readonly PackageManager $packageManager,
private readonly Context $context,
private readonly TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
private readonly SetRegistry $setRegistry,
) {}
/**
* @param 'constants'|'setup' $type
*/
public function getTreeBySysTemplateRowsAndSite(
string $type,
array $sysTemplateRows,
TokenizerInterface $tokenizer,
?SiteInterface $site = null,
?PhpFrontend $cache = null
): RootInclude {
if (!in_array($type, ['constants', 'setup'], true)) {
throw new \RuntimeException('type must be either constants or setup', 1653737656);
}
$this->tokenizer = $tokenizer;
$this->cache = $cache;
$this->type = $type;
$this->includedSysTemplateUids = [];
$rootNode = new RootInclude();
$siteIsTypoScriptRoot = $site instanceof Site ? $site->isTypoScriptRoot() : false;
if ($siteIsTypoScriptRoot) {
$this->enableStaticMagicIncludes = false;
$cacheIdentifier = 'site-template-' . $this->type . '-' . $site->getIdentifier();
$includeNode = $this->cache?->require($cacheIdentifier) ?: null;
$includeNode ??= $this->createSiteTemplateInclude($site, $cacheIdentifier);
$rootNode->addChild($includeNode);
}
if (empty($sysTemplateRows)) {
return $rootNode;
}
$this->enableStaticMagicIncludes = true;
// Convenience code: Usually, at least one sys_template records needs to have 'clear' set. This resets
// the AST and triggers inclusion of "globals" TypoScript. When integrators missed to set the clear flags,
// important globals TypoScript is not loaded, leading to pretty hard to find issues in Frontend
// rendering. Since the details of the 'clear' flags are rather complex anyway, this code scans the given
// sys_template records if the flag is set somewhere and if not, actively sets it dynamically for the
// first templates. As a result, integrators do not need to think about the 'clear' flags at all for
// simple instances, it 'just works'.
$atLeastOneSysTemplateRowHasClearFlag = $siteIsTypoScriptRoot;
if (!$atLeastOneSysTemplateRowHasClearFlag) {
foreach ($sysTemplateRows as $sysTemplateRow) {
if (($this->type === 'constants' && $sysTemplateRow['clear'] & 1) || ($this->type === 'setup' && $sysTemplateRow['clear'] & 2)) {
$atLeastOneSysTemplateRowHasClearFlag = true;
break;
}
}
$firstRow = reset($sysTemplateRows);
$firstRow['clear'] = $this->type === 'constants' ? 1 : 2;
$sysTemplateRows[array_key_first($sysTemplateRows)] = $firstRow;
}
foreach ($sysTemplateRows as $sysTemplateRow) {
$cacheIdentifier = 'sys-template-' . $this->type . '-' . $this->getSysTemplateRowIdentifier($sysTemplateRow, $site);
if ($this->cache) {
// Get from cache if possible
$includeNode = $this->cache->require($cacheIdentifier);
if ($includeNode) {
$rootNode->addChild($includeNode);
continue;
}
}
$includeNode = new SysTemplateInclude();
$name = '[sys_template:' . $sysTemplateRow['uid'] . '] ' . $sysTemplateRow['title'];
$includeNode->setName($name);
$includeNode->setPid((int)$sysTemplateRow['pid']);
if ($this->type === 'constants') {
$includeNode->setLineStream($this->tokenizer->tokenize($sysTemplateRow['constants'] ?? ''));
} else {
$includeNode->setLineStream($this->tokenizer->tokenize($sysTemplateRow['config'] ?? ''));
}
if ($sysTemplateRow['root']) {
$includeNode->setRoot(true);
}
$clear = $sysTemplateRow['clear'];
if (($this->type === 'constants' && $clear & 1) || ($this->type === 'setup' && $clear & 2)) {
$includeNode->setClear(true);
}
$this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site);
$this->treeFromTokenStreamBuilder->buildTree($includeNode, $this->type, $this->tokenizer);
$this->cache?->set($cacheIdentifier, $this->prepareNodeForCache($includeNode));
$rootNode->addChild($includeNode);
}
return $rootNode;
}
private function createSiteTemplateInclude(
Site $site,
string $cacheIdentifier
): SiteTemplateInclude {
$includeNode = new SiteTemplateInclude();
$includeNode->setRoot(true);
$includeNode->setClear(true);
$this->addScopedStaticsFromGlobals($includeNode, 'siteSets');
$this->addContentRenderingFromGlobals($includeNode, 'TYPO3_CONF_VARS defaultContentRendering');
$sets = $this->setRegistry->getSets(...$site->getSets());
if (count($sets) > 0) {
$includeSetInclude = new IncludeStaticFileFileInclude();
$includeSetInclude->setName('site:' . $site->getIdentifier() . ':sets');
$includeSetInclude->setPath('site:' . $site->getIdentifier() . '/');
foreach ($sets as $set) {
if ($set->typoscript === null) {
continue;
}
$this->handleSetInclude($includeSetInclude, rtrim($set->typoscript, '/') . '/', 'set:' . $set->name);
}
$includeNode->addChild($includeSetInclude);
}
if ($this->type === 'constants') {
$this->addDefaultTypoScriptConstantsFromSite($includeNode, $site);
}
$siteTypoScript = $site->getTypoScript();
$content = $this->type === 'constants' ? $siteTypoScript?->constants : $siteTypoScript?->setup;
if ($content !== null) {
$includeNode->setLineStream($this->tokenizer->tokenize($content));
$this->treeFromTokenStreamBuilder->buildTree($includeNode, $this->type, $this->tokenizer, false);
}
$includeNode->setName(sprintf(
'[site:%s%s] %s',
$site->getIdentifier(),
$content === null ? '' : '/' . $this->type . '.typoscript',
$site->getConfiguration()['websiteTitle'] ?? ''
));
$this->cache?->set($cacheIdentifier, $this->prepareNodeForCache($includeNode));
return $includeNode;
}
private function handleSetInclude(IncludeInterface $parentNode, string $path, string $label): void
{
$path = GeneralUtility::getFileAbsFileName($path);
// '/.../my_extension/Configuration/TypoScript/MyStaticInclude/include_static_file.txt'
$includeStaticFileFileIncludePath = $path . 'include_static_file.txt';
if (file_exists($path . 'include_static_file.txt')) {
$includeStaticFileFileInclude = new IncludeStaticFileFileInclude();
$includeStaticFileFileInclude->setName($label . ':include_static_file.txt');
$includeStaticFileFileInclude->setPath($path . 'include_static_file.txt');
$parentNode->addChild($includeStaticFileFileInclude);
$includeStaticFileFileIncludeContent = (string)file_get_contents($includeStaticFileFileIncludePath);
// @todo: There is no array_unique() for DB based include_static_file content?!
$includeStaticFileFileIncludeArray = array_unique(GeneralUtility::trimExplode(',', $includeStaticFileFileIncludeContent, true));
foreach ($includeStaticFileFileIncludeArray as $includeStaticFileFileIncludeString) {
$this->handleSingleIncludeStaticFile($includeStaticFileFileInclude, $includeStaticFileFileIncludeString);
}
}
$fileName = $path . $this->type . '.typoscript';
if (file_exists($fileName)) {
$fileContent = file_get_contents($fileName);
$fileNode = new FileInclude();
$fileNode->setName($label . ':' . $this->type . '.typoscript');
$fileNode->setPath($fileName);
$fileNode->setLineStream($this->tokenizer->tokenize($fileContent));
$this->treeFromTokenStreamBuilder->buildTree($fileNode, $this->type, $this->tokenizer, false);
$parentNode->addChild($fileNode);
}
}
/**
* Add includes defined in a sys_template record.
*/
private function handleSysTemplateRecordInclude(IncludeInterface $parentNode, array $row, ?SiteInterface $site): void
{
$this->includedSysTemplateUids[] = (int)$row['uid'];
$isRoot = (bool)$row['root'];
$clearConstants = (int)$row['clear'] & 1;
$clearSetup = (int)$row['clear'] & 2;
$staticFileMode = (int)($row['static_file_mode']);
$includeStaticAfterBasedOn = (bool)$row['includeStaticAfterBasedOn'];
if ($this->type === 'constants' && $clearConstants) {
$this->addDefaultTypoScriptFromGlobals($parentNode);
$this->addDefaultTypoScriptConstantsFromSite($parentNode, $site);
}
if ($this->type === 'setup' && $clearSetup) {
$this->addDefaultTypoScriptFromGlobals($parentNode);
}
if ($staticFileMode === 3 && $isRoot) {
$this->addExtensionStatics($parentNode);
}
if (!$includeStaticAfterBasedOn) {
$this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']);
}
if (!empty($row['basedOn'])) {
$this->handleIncludeBasedOnTemplates($parentNode, (string)$row['basedOn'], $site);
}
if ($includeStaticAfterBasedOn) {
$this->handleIncludeStaticFileArray($parentNode, (string)$row['include_static_file']);
}
if ($staticFileMode === 1 || ($staticFileMode === 0 && $isRoot)) {
$this->addExtensionStatics($parentNode);
}
}
/**
* Handle includes defined in a sys_template['include_static_file'] row. Extracted as
* methods since it depends on 'includeStaticAfterBasedOn' field if this is included
* *before* or *after* other 'basedOn' includes.
*
* The cache implemented here *does not* take the *content* of files into account.
* This means changing a file *does not* automatically void the cache since that would
* lead to lots of file_exists() and file_get_contents() calls in production.
* Instances in development context should thus set the typoscript-cache to NullFrontend.
* Note this cache-usage is the main-cache that kicks in whenever different sys_template
* records include the same file. For instance, when multiple sites include ext:seo XmlSitemap,
* the cache implementation here takes care the ext:seo subtree is calculated only once.
*/
private function handleIncludeStaticFileArray(IncludeInterface $parentNode, string $includeStaticFileString): void
{
$includeStaticFileIncludeArray = GeneralUtility::trimExplode(',', $includeStaticFileString, true);
foreach ($includeStaticFileIncludeArray as $includeStaticFile) {
$cacheIdentifier = preg_replace('/[^[:alnum:]]/u', '-', mb_strtolower($includeStaticFile)) . '-' . $this->type;
if ($this->cache) {
$node = $this->cache->require($cacheIdentifier);
if ($node) {
$parentNode->addChild($node);
continue;
}
}
$node = new IncludeStaticFileDatabaseInclude();
$node->setName($includeStaticFile);
$this->handleSingleIncludeStaticFile($node, $includeStaticFile);
$this->cache?->set($cacheIdentifier, $this->prepareNodeForCache($node));
$parentNode->addChild($node);
}
}
/**
* Handle includes defined in a sys_template['basedOn'] row.
* Warning: Calls handleSysTemplateRecordInclude() recursive when another basedOn templates
* record includes things again!
*/
private function handleIncludeBasedOnTemplates(IncludeInterface $parentNode, string $basedOnList, ?SiteInterface $site): void
{
$basedOnTemplateUids = GeneralUtility::intExplode(',', $basedOnList, true);
// Filter uids that have been handled already.
$basedOnTemplateUids = array_diff($basedOnTemplateUids, $this->includedSysTemplateUids);
if (empty($basedOnTemplateUids)) {
return;
}
$basedOnTemplateRows = $this->getBasedOnSysTemplateRowsFromDatabase($basedOnTemplateUids);
foreach ($basedOnTemplateUids as $basedOnTemplateUid) {
if (is_array($basedOnTemplateRows[$basedOnTemplateUid] ?? false)) {
$sysTemplateRow = $basedOnTemplateRows[$basedOnTemplateUid];
$this->includedSysTemplateUids[] = (int)$sysTemplateRow['uid'];
$includeNode = new SysTemplateInclude();
$name = '[sys_template:' . $sysTemplateRow['uid'] . '] ' . $sysTemplateRow['title'];
$includeNode->setName($name);
$includeNode->setPid((int)$sysTemplateRow['pid']);
if ($this->type === 'constants') {
$includeNode->setLineStream($this->tokenizer->tokenize($sysTemplateRow['constants'] ?? ''));
} else {
$includeNode->setLineStream($this->tokenizer->tokenize($sysTemplateRow['config'] ?? ''));
}
$this->treeFromTokenStreamBuilder->buildTree($includeNode, $this->type, $this->tokenizer);
if ($sysTemplateRow['root']) {
$includeNode->setRoot(true);
}
$clear = $sysTemplateRow['clear'];
if (($this->type === 'constants' && $clear & 1)
|| ($this->type === 'setup' && $clear & 2)
) {
$includeNode->setClear(true);
}
$parentNode->addChild($includeNode);
$this->handleSysTemplateRecordInclude($includeNode, $sysTemplateRow, $site);
}
}
}
/**
* Handle a single sys_template ['include_static_file'] include.
* Looks up file "EXT:/My/Path/include_static_file.txt' in an extension and includes this.
* Also loads "EXT:/My/Path/[constants|setup].[typoscript|ts|txt].
* Warning: Recursive since an include_static_file.txt file can include other extension's include_static_file.txt again.
* This method has no cache-layer usage on its own: handleSingleIncludeStaticFile() which calls this
* method is the cache layer here.
*/
private function handleSingleIncludeStaticFile(IncludeInterface $parentNode, $includeStaticFileString): void
{
if (!PathUtility::isExtensionPath($includeStaticFileString)) {
// Must start with 'EXT:'
throw new \RuntimeException(
'Single include_static_file does not start with "EXT:": ' . $includeStaticFileString,
1651137904
);
}
// Cut off 'EXT:'
$includeStaticFileWithoutExt = substr($includeStaticFileString, 4);
$includeStaticFileExtKeyAndPath = GeneralUtility::trimExplode('/', $includeStaticFileWithoutExt, true, 2);
if (empty($includeStaticFileExtKeyAndPath[0]) || empty($includeStaticFileExtKeyAndPath[1])) {
throw new \RuntimeException(
'Syntax of static includes is "EXT:extension_key/Path". Usually enforced as such by ExtensionManagementUtility::addStaticFile',
1651138603
);
}
$extensionKey = $includeStaticFileExtKeyAndPath[0];
if (!ExtensionManagementUtility::isLoaded($extensionKey)) {
return;
}
// example: '/.../my_extension/Configuration/TypoScript/MyStaticInclude/'
$pathSegmentWithAppendedSlash = rtrim($includeStaticFileExtKeyAndPath[1]) . '/';
$path = ExtensionManagementUtility::extPath($extensionKey, $pathSegmentWithAppendedSlash);
// '/.../my_extension/Configuration/TypoScript/MyStaticInclude/include_static_file.txt'
$includeStaticFileFileIncludePath = $path . 'include_static_file.txt';
if (file_exists($path . 'include_static_file.txt')) {
$includeStaticFileFileInclude = new IncludeStaticFileFileInclude();
$name = 'EXT:' . $extensionKey . '/' . $pathSegmentWithAppendedSlash . 'include_static_file.txt';
$includeStaticFileFileInclude->setName($name);
$includeStaticFileFileInclude->setPath($includeStaticFileString);
$parentNode->addChild($includeStaticFileFileInclude);
$includeStaticFileFileIncludeContent = (string)file_get_contents($includeStaticFileFileIncludePath);
// @todo: There is no array_unique() for DB based include_static_file content?!
$includeStaticFileFileIncludeArray = array_unique(GeneralUtility::trimExplode(',', $includeStaticFileFileIncludeContent, true));
foreach ($includeStaticFileFileIncludeArray as $includeStaticFileFileIncludeString) {
$this->handleSingleIncludeStaticFile($includeStaticFileFileInclude, $includeStaticFileFileIncludeString);
}
}
$extensions = ['.typoscript', '.ts', '.txt'];
foreach ($extensions as $extension) {
// '/.../my_extension/Configuration/TypoScript/MyStaticInclude/[constants|setup]' plus one of the allowed extensions like '.typoscript'
$fileName = $path . $this->type . $extension;
if (file_exists($fileName)) {
$fileContent = file_get_contents($fileName);
$fileNode = new FileInclude();
$name = 'EXT:' . $extensionKey . '/' . $pathSegmentWithAppendedSlash . $this->type . $extension;
$fileNode->setName($name);
$fileNode->setPath($name);
$fileNode->setLineStream($this->tokenizer->tokenize($fileContent));
$this->treeFromTokenStreamBuilder->buildTree($fileNode, $this->type, $this->tokenizer);
$parentNode->addChild($fileNode);
}
}
if ($this->enableStaticMagicIncludes) {
$extensionKeyWithoutUnderscores = str_replace('_', '', $extensionKey);
$this->addStaticMagicFromGlobals($parentNode, $extensionKeyWithoutUnderscores . '/' . $pathSegmentWithAppendedSlash);
}
}
/**
* Load 'EXT:my_extension/ext_typoscript_[constants|setup].typoscript'
* of *all* loaded extensions if they exist.
*/
private function addExtensionStatics(IncludeInterface $parentNode): void
{
foreach ($this->packageManager->getActivePackages() as $package) {
$extensionKey = $package->getPackageKey();
$extensionKeyWithoutUnderscores = str_replace('_', '', $extensionKey);
$file = $package->getPackagePath() . 'ext_typoscript_' . $this->type . '.typoscript';
if (file_exists($file)) {
$identifier = preg_replace('/[^[:alnum:]]/u', '-', 'ext-' . $extensionKey . '-ext-typoscript-' . $this->type . '-typoscript');
if ($this->cache) {
$node = $this->cache->require($identifier);
if ($node) {
$parentNode->addChild($node);
continue;
}
}
$fileContent = file_get_contents($file);
$this->addStaticMagicFromGlobals($parentNode, $extensionKeyWithoutUnderscores);
$node = new ExtensionStaticInclude();
$node->setName('EXT:' . $extensionKey . '/ext_typoscript_' . $this->type . '.typoscript');
$node->setPath('EXT:' . $extensionKey . '/ext_typoscript_' . $this->type . '.typoscript');
$node->setLineStream($this->tokenizer->tokenize($fileContent));
$this->treeFromTokenStreamBuilder->buildTree($node, $this->type, $this->tokenizer);
$this->cache?->set($identifier, $this->prepareNodeForCache($node));
$parentNode->addChild($node);
}
}
}
/**
* Load default constants TS from $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_[constants|setup]']
* whenever 'root=1' is set for a sys_template.
*/
private function addDefaultTypoScriptFromGlobals(IncludeInterface $parentConstantNode): void
{
$defaultTypoScriptConstants = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $this->type] ?? '';
if (!empty($defaultTypoScriptConstants)) {
$cacheIdentifier = 'globals-defaulttyposcript-' . $this->type . '-' . hash('xxh3', $defaultTypoScriptConstants);
if ($this->cache) {
$node = $this->cache->require($cacheIdentifier);
if ($node) {
$parentConstantNode->addChild($node);
return;
}
}
$node = new DefaultTypoScriptInclude();
$node->setName('TYPO3_CONF_VARS[\'FE\'][\'defaultTypoScript_' . $this->type . '\']');
$node->setLineStream($this->tokenizer->tokenize($defaultTypoScriptConstants));
$this->treeFromTokenStreamBuilder->buildTree($node, $this->type, $this->tokenizer);
$this->cache?->set($cacheIdentifier, $this->prepareNodeForCache($node));
$parentConstantNode->addChild($node);
}
}
/**
* Load default TS constants from site configuration if that page has a site in rootline.
*/
private function addDefaultTypoScriptConstantsFromSite(IncludeInterface $parentConstantNode, ?SiteInterface $site): void
{
if (!$site instanceof Site) {
return;
}
$siteConstants = '';
$siteSettings = $site->getSettings();
if ($siteSettings->isEmpty()) {
return;
}
$cacheIdentifier = 'site-constants-' . hash('xxh3', json_encode($siteSettings, JSON_THROW_ON_ERROR));
if ($this->cache) {
$node = $this->cache->require($cacheIdentifier);
if ($node) {
$parentConstantNode->addChild($node);
return;
}
}
$siteSettings = $siteSettings->getAllFlat();
foreach ($siteSettings as $nodeIdentifier => $value) {
$siteConstants .= $nodeIdentifier . ' = ' . $value . LF;
}
$node = new SiteInclude();
$node->setName('Site constants settings of site "' . $site->getIdentifier() . '"');
$node->setLineStream($this->tokenizer->tokenize($siteConstants));
$this->cache?->set($cacheIdentifier, $this->prepareNodeForCache($node));
$parentConstantNode->addChild($node);
}
private function addScopedStaticsFromGlobals(IncludeInterface $parentNode, string $identifier): void
{
// defaultTypoScript_constants.' or defaultTypoScript_setup.'
$source = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $this->type . '.'][$identifier] ?? null;
if (!empty($source)) {
$node = new DefaultTypoScriptMagicKeyInclude();
$node->setName('TYPO3_CONF_VARS globals_defaultTypoScript_' . $this->type . '.' . $identifier);
$node->setLineStream($this->tokenizer->tokenize($source));
$this->treeFromTokenStreamBuilder->buildTree($node, $this->type, $this->tokenizer);
$parentNode->addChild($node);
}
}
private function addContentRenderingFromGlobals(IncludeInterface $parentNode, string $name): void
{
$source = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $this->type . '.']['defaultContentRendering'] ?? null;
if (!empty($source)) {
$node = new DefaultTypoScriptMagicKeyInclude();
$node->setName($name);
$node->setLineStream($this->tokenizer->tokenize($source));
$this->treeFromTokenStreamBuilder->buildTree($node, $this->type, $this->tokenizer);
$parentNode->addChild($node);
}
}
/**
* A rather weird lookup in $GLOBALS['TYPO3_CONF_VARS']['FE'] for magic includes.
* See ExtensionManagementUtility::addTypoScript() for more details on this.
*/
private function addStaticMagicFromGlobals(IncludeInterface $parentNode, string $identifier): void
{
$this->addScopedStaticsFromGlobals($parentNode, $identifier);
// If this is a template of type "default content rendering", see if other extensions have added their TypoScript that should be included.
if (in_array($identifier, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
$this->addContentRenderingFromGlobals($parentNode, 'TYPO3_CONF_VARS defaultContentRendering ' . $this->type . ' for ' . $identifier);
}
}
/**
* Get 'basedOn' sys_template sub-rows of sys_templates that use this.
* Note the 'IN()' query implementation below delivers rows in *any* order. To preserve
* basedOn list order, we re-index result rows by uid and then iterate on the original
* order of $basedOnTemplateUids in handleIncludeBasedOnTemplates().
*/
private function getBasedOnSysTemplateRowsFromDatabase(array $basedOnTemplateUids): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->setRestrictions($this->getSysTemplateQueryRestrictionContainer());
$basedOnTemplateRows = $queryBuilder
->select('*')
->from('sys_template')
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter($basedOnTemplateUids, Connection::PARAM_INT_ARRAY)
)
)
->executeQuery()
->fetchAllAssociative();
return array_combine(array_column($basedOnTemplateRows, 'uid'), $basedOnTemplateRows);
}
/**
* Calculate a cache identifier for a sys_template row.
* This is a bit nifty: There are instances in the wild that add the same TypoScript
* sys_template over and over again in a page tree to for instance toggle a single value.
* Those content-identical template rows create only one cache entry: We create a hash
* from the relevant row fields like 'constants' and 'config', but we do NOT include
* the sys_template row 'uid' and 'pid'. So different sys_template rows with the same content
* lead to the same identifier, and we cache that just once.
*
* One additional dependency influences the identifier as well: If the 'clear constants'
* flag is set, this row will later trigger loading of constants from given site settings.
* When two "first" template rows have the exact same field content in different sites, the
* site identifier needs to be added to the hash to still create two different cache entries.
*/
private function getSysTemplateRowIdentifier(array $sysTemplateRow, ?SiteInterface $site): string
{
$siteIdentifier = 'dummy';
if ($this->type === 'constants' && ((int)$sysTemplateRow['clear'] & 1) && $site !== null) {
$siteIdentifier = $site->getIdentifier();
}
$cacheRelevantSysTemplateRowValues = [
'root' => (int)$sysTemplateRow['root'],
'clear' => (int)$sysTemplateRow['clear'],
'include_static_file' => (string)$sysTemplateRow['include_static_file'],
'constants' => (string)$sysTemplateRow['constants'],
'config' => (string)$sysTemplateRow['config'],
'basedOn' => (string)$sysTemplateRow['basedOn'],
'includeStaticAfterBasedOn' => (int)$sysTemplateRow['includeStaticAfterBasedOn'],
'static_file_mode' => (int)$sysTemplateRow['static_file_mode'],
'siteIdentifier' => $siteIdentifier,
];
return hash('xxh3', json_encode($cacheRelevantSysTemplateRowValues, JSON_THROW_ON_ERROR));
}
private function prepareNodeForCache(IncludeInterface $node): string
{
return 'return unserialize(\'' . addcslashes(serialize($node), '\'\\') . '\');';
}
/**
* Get sys_template record query builder restrictions.
* Allows hidden records if enabled in context.
*/
private function getSysTemplateQueryRestrictionContainer(): DefaultRestrictionContainer
{
$restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
if ($this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)) {
$restrictionContainer->removeByType(HiddenRestriction::class);
}
return $restrictionContainer;
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Traverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeVisitorInterface;
/**
* An optimized traverser that does not traverse children when a node is
* a condition node that evaluate false.
*
* This is pretty clever: When adding the ConditionMatcherVisitor as first visitor, it
* sets the condition verdict of a ConditionInterface node in visitBeforeChildren().
* Adding the AstBuilderVisitor as second visitor, the AstBuilderVisitor will not be
* called for ConditionInterface children that did not evaluate to true.
* This way, we can both evaluate conditions and build the AST in only one traversing round.
*
* @internal: Internal tree structure.
*/
final class ConditionVerdictAwareIncludeTreeTraverser implements IncludeTreeTraverserInterface
{
public function traverse(RootInclude $rootInclude, array $visitors): void
{
foreach ($visitors as $visitor) {
if (!$visitor instanceof IncludeTreeVisitorInterface) {
throw new \RuntimeException(
'Visitors must implement IncludeTreeVisitorInterface',
1689244840
);
}
}
$this->traverseRecursive($rootInclude, $visitors, 0);
}
private function traverseRecursive(IncludeInterface $include, array $visitors, int $currentDepth): void
{
foreach ($visitors as $visitor) {
$visitor->visitBeforeChildren($include, $currentDepth);
}
if ($include instanceof IncludeConditionInterface && !$include->getConditionVerdict()) {
// Don't traverse children if condition did not match.
return;
}
foreach ($include->getNextChild() as $child) {
$this->traverseRecursive($child, $visitors, $currentDepth + 1);
foreach ($visitors as $visitor) {
$visitor->visit($child, $currentDepth);
}
}
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Traverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeVisitorInterface;
/**
* Traverse all nodes of a RootInclude. Used mostly in backend "Template" module.
*
* @internal: Internal tree structure.
*/
final class IncludeTreeTraverser implements IncludeTreeTraverserInterface
{
public function traverse(RootInclude $rootInclude, array $visitors): void
{
foreach ($visitors as $visitor) {
if (!$visitor instanceof IncludeTreeVisitorInterface) {
throw new \RuntimeException(
'Visitors must implement IncludeTreeVisitorInterface',
1689244841
);
}
}
$this->traverseRecursive($rootInclude, $visitors, 0);
}
private function traverseRecursive(IncludeInterface $include, array $visitors, int $currentDepth): void
{
foreach ($visitors as $visitor) {
$visitor->visitBeforeChildren($include, $currentDepth);
}
foreach ($include->getNextChild() as $child) {
$this->traverseRecursive($child, $visitors, $currentDepth + 1);
foreach ($visitors as $visitor) {
$visitor->visit($child, $currentDepth);
}
}
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Traverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeVisitorInterface;
/**
* Interface implemented by include tree traversers.
*
* Visitors can be attached and are called for each traversed node.
*
* @internal: Internal tree structure.
*/
interface IncludeTreeTraverserInterface
{
/**
* @param IncludeTreeVisitorInterface[] $visitors
*/
public function traverse(RootInclude $rootInclude, array $visitors): void;
}
@@ -0,0 +1,421 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\AtImportInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionElseInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionStopInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\DefaultTypoScriptMagicKeyInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SegmentInclude;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ConditionElseLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ConditionLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ConditionStopLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineInterface;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Helper class of TreeBuilder classes: This class gets a node with a LineStream - a node
* created from a sys_template 'constants' or 'setup' field, or created from a
* file import or a string. It then looks for conditions and imports in the attached LineStream
* and splits the node into child nodes if needed.
*
* So while SysTemplateTreeBuilder is all about creating includes from sys_template records
* in correct order, this class takes care of conditions and @import within single
* source streams.
*
* This class has no cache-implementation itself: The higher level class caches
* include trees of token streams.
*
* @internal: Internal tree structure.
*/
final class TreeFromLineStreamBuilder
{
/** @var 'constants'|'setup'|'other' */
private string $type;
private TokenizerInterface $tokenizer;
private bool $enableMagicIncludes = false;
/**
* Using "@import" with wildcards, the file ending depends on the given type:
* With Frontend TypoScript, .typoscript is allowed, with TsConfig, .tsconfig
* and .typoscript is allowed. This property maps types to their file suffixes.
*
* @var array<string, array<int, string>>
*/
private array $atImportTypeToSuffixMap = [
'constants' => ['typoscript'],
'setup' => ['typoscript'],
'other' => ['typoscript'],
'tsconfig' => ['typoscript', 'tsconfig'],
];
public function __construct(
private readonly FileNameValidator $fileNameValidator,
) {}
public function buildTree(IncludeInterface $node, string $type, TokenizerInterface $tokenizer, bool $enableMagicIncludes = true): void
{
if (!in_array($type, ['constants', 'setup', 'tsconfig', 'other'], true)) {
// Type "constants" and "setup" trigger the weird addStaticMagicFromGlobals() resolving, while "other" ignores it.
throw new \RuntimeException('type must be either "constants", "setup", "tsconfig" or "other"', 1652741356);
}
$this->type = $type;
$this->tokenizer = $tokenizer;
$this->enableMagicIncludes = $enableMagicIncludes;
$this->buildTreeInternal($node);
}
/**
* This method is a bit tricky and not too easy to follow: It loops over
* a given source stream of lines exactly once, but creates a two-level
* include node structure from it:
*
* For instance, when a condition is encountered, it creates a node for the
* condition, and the "body" lines of the condition are child nodes of the
* condition node. The $previousNode <-> $node juggling handles this: When
* the condition body ends (new condition, or [end] or similar), the
* next include needs to be attached to the former parent node again.
*
* Essentially, a single source stream is split into multiple child nodes
* when there are conditions or imports. A node that is "split" into
* child nodes gets the "split" toggle set, indicating that the entire
* source stream is represented by its child nodes.
*
* A condition body may have more than one child: When there are multiple
* file includes, each one creates an own node, which may have children
* again. This also means the method is called recursive, since the source
* stream of an included file may need to be split into segments again, so
* it calls this method again with itself as entry node.
*/
private function buildTreeInternal(IncludeInterface $node): void
{
$parentNode = $node;
$givenTokenLineStream = $node->getLineStream();
$lineStream = new LineStream();
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
foreach ($givenTokenLineStream->getNextLine() as $line) {
if ($line instanceof ConditionLine && $node instanceof ConditionInclude) {
// Finish current condition when this line is another condition
$node->setSplit();
if (!$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$lineStream = new LineStream();
}
$node = $parentNode;
}
if ($line instanceof ConditionLine) {
// A new condition not yet in condition context
$node->setSplit();
$conditionValueToken = $line->getTokenValue();
if (!$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$lineStream = new LineStream();
}
$childNode = new ConditionInclude();
$childNode->setSplit();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$childNode->setConditionToken($conditionValueToken);
$lineStream->append($line);
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$parentNode = $node;
$node = $childNode;
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$lineStream = new LineStream();
continue;
}
if (($node instanceof ConditionInclude || $node instanceof ConditionElseInclude)
&& $line instanceof ConditionStopLine
) {
// Finish condition segment due to [end] or [global] line
$node->setSplit();
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$node = $parentNode;
$childNode = new ConditionStopInclude();
$childNode->setName($node->getName());
$childNode->setLineStream((new LineStream())->append($line));
$node->addChild($childNode);
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$lineStream = new LineStream();
continue;
}
if ($line instanceof ConditionStopLine) {
// [end] or [global] not within open condition context. Fishy. Still finish current
// segment, mark node split, add new ConditionStopInclude(), open a new segment.
$node->setSplit();
if (!$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
}
$childNode = new ConditionStopInclude();
$childNode->setName($node->getName());
$childNode->setLineStream((new LineStream())->append($line));
$node->addChild($childNode);
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$lineStream = new LineStream();
continue;
}
if ($node instanceof ConditionInclude && $line instanceof ConditionElseLine) {
// Active condition into [else] condition
$node->setSplit();
if (!$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
}
$conditionToken = $node->getConditionToken();
$node = $parentNode;
$childNode = new ConditionElseInclude();
$childNode->setSplit();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$childNode->setConditionToken($conditionToken);
$lineStream = new LineStream();
$lineStream->append($line);
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$parentNode = $node;
$node = $childNode;
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$lineStream = new LineStream();
continue;
}
if ($line instanceof ImportLine) {
$node->setSplit();
$atImportValueToken = $line->getValueToken();
if (!$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
$lineStream = new LineStream();
}
$childNode = new SegmentInclude();
$childNode->setName($node->getName());
$childNode->setPath($node->getPath());
$allowedSuffixes = $this->atImportTypeToSuffixMap[$this->type];
foreach ($allowedSuffixes as $allowedSuffix) {
$this->processAtImport($allowedSuffix, $node, $atImportValueToken, $line);
}
continue;
}
$lineStream->append($line);
}
if ($node->isSplit() && !$lineStream->isEmpty()) {
$childNode->setLineStream($lineStream);
$node->addChild($childNode);
}
}
/**
* Process a single '@import'. May add multiple children when '*' wildcards are involved.
* Warning: Calls buildTree() recursive for each included file.
* Warning: Calls itself recursive for 'relative' lookups.
*/
private function processAtImport(string $fileSuffix, IncludeInterface $node, Token $atImportValueToken, LineInterface $atImportLine, bool $tryRelative = false): void
{
$atImportValue = $atImportValueToken->getValue();
$atImportName = $atImportValue;
if ($tryRelative) {
if (empty($node->getPath())) {
return;
}
$parentPath = rtrim(dirname($node->getPath()), '/') . '/';
$atImportValue = ltrim($atImportValue, './');
$atImportName = preg_replace('#([:/])[^:/]+$#', '$1', $node->getName()) . $atImportValue;
$atImportValue = $parentPath . $atImportValue;
}
$absoluteFileName = rtrim(GeneralUtility::getFileAbsFileName($atImportValue), '/');
if ($absoluteFileName === '') {
return;
}
if (str_ends_with($absoluteFileName, '.' . $fileSuffix) && is_file($absoluteFileName)) {
// Simple file with allowed file suffix
if ($this->fileNameValidator->isValid($absoluteFileName)) {
$this->addSingleAtImportFile($node, $absoluteFileName, $atImportValue, $atImportName, $atImportLine);
$this->addStaticMagicFromGlobals($node, $atImportValue);
}
} elseif (is_dir($absoluteFileName)) {
// Directories with and without ending /
$filesAndDirs = scandir($absoluteFileName);
foreach ($filesAndDirs as $potentialInclude) {
if (!str_ends_with($potentialInclude, '.' . $fileSuffix)
|| is_dir($absoluteFileName . '/' . $potentialInclude)
|| !$this->fileNameValidator->isValid($absoluteFileName . '/' . $potentialInclude)
) {
continue;
}
$singleAbsoluteFileName = $absoluteFileName . '/' . $potentialInclude;
$identifier = rtrim($atImportValue, '/') . '/' . $potentialInclude;
$this->addSingleAtImportFile($node, $singleAbsoluteFileName, $identifier, $identifier, $atImportLine);
$this->addStaticMagicFromGlobals($node, $identifier);
}
} elseif (is_file($absoluteFileName . '.' . $fileSuffix)) {
// File without .typoscript / .tsconfig suffix, but exists when suffix is added
if ($this->fileNameValidator->isValid($absoluteFileName . '.' . $fileSuffix)) {
$singleAbsoluteFileName = $absoluteFileName . '.' . $fileSuffix;
$identifier = $atImportValue . '.' . $fileSuffix;
$this->addSingleAtImportFile($node, $singleAbsoluteFileName, $identifier, $identifier, $atImportLine);
$this->addStaticMagicFromGlobals($node, $identifier);
}
} elseif (str_contains($absoluteFileName, '*')) {
// Something with *
$directory = rtrim(dirname($absoluteFileName) . '/');
$directoryExists = is_dir($directory);
if (!$directoryExists && str_starts_with($atImportValue, './') && !$tryRelative) {
// See if we can import some relative wildcard like "./Setup/*" or "./Setup/*.typoscript"
$this->processAtImport($fileSuffix, $node, $atImportValueToken, $atImportLine, true);
return;
}
if (!$directoryExists) {
// Absolute directory. There is nothing to import if the directory does not exist.
return;
}
$filePattern = basename($absoluteFileName);
if (!str_contains($filePattern, '*')) {
// The * wildcard must occur in the filename, wildcards in directories are not handled.
return;
}
if (mb_substr_count($filePattern, '*') > 1) {
// Only one wildcard character is allowed, foo*.bar*.typoscript is considered an invalid pattern.
return;
}
// Normalize right side, making sure it always ends with $fileSuffix ".typoscript" / ".tsconfig"
if (str_ends_with($filePattern, $fileSuffix)) {
$filePattern = mb_substr($filePattern, 0, -1 * strlen($fileSuffix));
$filePattern = rtrim($filePattern, '.');
}
$filePattern = $filePattern . '.' . $fileSuffix;
$wildcardPosition = mb_strpos($filePattern, '*');
$leftPrefix = mb_substr($filePattern, 0, $wildcardPosition);
$rightPrefix = mb_substr($filePattern, $wildcardPosition + 1);
$filesAndDirs = scandir($directory);
foreach ($filesAndDirs as $potentialInclude) {
if ($potentialInclude === '.'
|| $potentialInclude === '..'
|| !str_starts_with($potentialInclude, $leftPrefix)
|| !str_ends_with($potentialInclude, $rightPrefix)
|| is_dir($directory . $potentialInclude)
|| !$this->fileNameValidator->isValid($directory . $potentialInclude)
) {
continue;
}
$singleAbsoluteFileName = $directory . $potentialInclude;
$identifier = rtrim(dirname($atImportValue), '/') . '/' . $potentialInclude;
$this->addSingleAtImportFile($node, $singleAbsoluteFileName, $identifier, $identifier, $atImportLine);
$this->addStaticMagicFromGlobals($node, $identifier);
}
} elseif (!$tryRelative) {
// See if we can import relative "./foo.typoscript" or "foo.typoscript"
$this->processAtImport($fileSuffix, $node, $atImportValueToken, $atImportLine, true);
}
}
/**
* Get content of a single @import file and add to current node as child.
*
* Warning: Recursively calls buildTree() to process includes of included content.
*/
private function addSingleAtImportFile(
IncludeInterface $parentNode,
string $absoluteFileName,
string $path,
string $name,
LineInterface $atImportLine
): void {
$content = file_get_contents($absoluteFileName);
$newNode = new AtImportInclude();
$newNode->setName($name);
$newNode->setPath($path);
$newNode->setLineStream($this->tokenizer->tokenize($content));
$newNode->setOriginalLine($atImportLine);
$this->buildTreeInternal($newNode);
$parentNode->addChild($newNode);
}
/**
* A rather weird lookup in $GLOBALS['TYPO3_CONF_VARS']['FE'] for magic includes.
* See ExtensionManagementUtility::addTypoScript() for more details on this.
* Warning: Yes, this is recursive again.
*/
private function addStaticMagicFromGlobals(IncludeInterface $parentNode, string $path): void
{
if (!in_array($this->type, ['constants', 'setup'], true) || !str_starts_with($path, 'EXT:')) {
// This magic method is relevant for Frontend TypoScript only, indicated by
// $this->type being either "constants" or "setup".
return;
}
$includeStaticFileWithoutExt = substr($path, 4);
$includeStaticFileExtKeyAndPath = GeneralUtility::trimExplode('/', $includeStaticFileWithoutExt, true, 2);
$extensionKey = $includeStaticFileExtKeyAndPath[0];
$extensionKeyWithoutUnderscores = str_replace('_', '', $extensionKey);
if (!$extensionKeyWithoutUnderscores || !ExtensionManagementUtility::isLoaded($extensionKey)) {
return;
}
// example: 'Configuration/TypoScript/MyStaticInclude/'
$pathSegmentWithAppendedSlash = rtrim(dirname($includeStaticFileExtKeyAndPath[1])) . '/';
$file = basename($path);
$type = GeneralUtility::trimExplode('.', $file, false, 2)[0] ?? '';
if ($type !== $this->type) {
return;
}
$globalsLookup = $extensionKeyWithoutUnderscores . '/' . $pathSegmentWithAppendedSlash;
if (!$this->enableMagicIncludes) {
return;
}
// If this is a template of type "default content rendering", see if other extensions have added their TypoScript that should be included.
if (in_array($globalsLookup, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
$source = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $type . '.']['defaultContentRendering'] ?? null;
if (!empty($source)) {
$node = new DefaultTypoScriptMagicKeyInclude();
$node->setName('TYPO3_CONF_VARS defaultContentRendering for ' . $path);
$node->setLineStream($this->tokenizer->tokenize($source));
$this->buildTreeInternal($node);
$parentNode->addChild($node);
}
}
}
}
@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedPageTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedUserTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\TsConfigInclude;
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Build include tree for user TSconfig and page TSconfig. This is typically used only by
* UserTsConfigFactory and PageTsConfigFactory.
*
* @internal
*/
#[Autoconfigure(public: true)]
final readonly class TsConfigTreeBuilder
{
public function __construct(
private TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
private PackageManager $packageManager,
private EventDispatcher $eventDispatcher,
private SiteFinder $siteFinder,
private SetRegistry $setRegistry,
) {}
public function getUserTsConfigTree(
BackendUserAuthentication $backendUser,
TokenizerInterface $tokenizer,
?PhpFrontend $cache = null
): RootInclude {
$includeTree = new RootInclude();
$collectedUserTsConfigArray = [];
$gotPackagesUserTsConfigFromCache = false;
$cacheIdentifier = (new PackageDependentCacheIdentifier($this->packageManager))
->withPrefix('usertsconfig-packages-strings')
->toString();
if ($cache) {
$collectedUserTsConfigArrayFromCache = $cache->require($cacheIdentifier);
if ($collectedUserTsConfigArrayFromCache) {
$gotPackagesUserTsConfigFromCache = true;
$collectedUserTsConfigArray = $collectedUserTsConfigArrayFromCache;
}
}
if (!$gotPackagesUserTsConfigFromCache) {
$event = $this->eventDispatcher->dispatch(new BeforeLoadedUserTsConfigEvent());
$collectedUserTsConfigArray = $event->getTsConfig();
foreach ($this->packageManager->getActivePackages() as $package) {
$packagePath = $package->getPackagePath();
$tsConfigFile = null;
if (file_exists($packagePath . 'Configuration/user.tsconfig')) {
$tsConfigFile = $packagePath . 'Configuration/user.tsconfig';
} elseif (file_exists($packagePath . 'Configuration/User.tsconfig')) {
$tsConfigFile = $packagePath . 'Configuration/User.tsconfig';
}
if ($tsConfigFile) {
$typoScriptString = @file_get_contents($tsConfigFile);
if (!empty($typoScriptString)) {
$collectedUserTsConfigArray['userTsConfig-package-' . $package->getPackageKey()] = $typoScriptString;
}
}
}
$cache?->set($cacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($collectedUserTsConfigArray), '\'\\') . '\');');
}
foreach ($collectedUserTsConfigArray as $key => $typoScriptString) {
$includeTree->addChild($this->getTreeFromString((string)$key, $typoScriptString, $tokenizer, $cache));
}
foreach ($backendUser->userGroupsUID as $groupId) {
// Loop through all groups and add their 'TSconfig' fields
if (!empty($backendUser->userGroups[$groupId]['TSconfig'] ?? '')) {
$includeTree->addChild($this->getTreeFromString('userTsConfig-group-' . $groupId, $backendUser->userGroups[$groupId]['TSconfig'], $tokenizer, $cache));
}
if (trim($backendUser->userGroups[$groupId]['tsconfig_includes'] ?? '')) {
$includeTsConfigFileList = GeneralUtility::trimExplode(',', $backendUser->userGroups[$groupId]['tsconfig_includes'], true);
foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
$content = $this->getContentOfTsconfigFile($includeTsConfigFile);
if (!empty($content)) {
$includeTree->addChild($this->getTreeFromString('userTsConfig-include-group' . $key, $content, $tokenizer, $cache));
}
}
}
}
if (!empty($backendUser->user['TSconfig'] ?? '')) {
$includeTree->addChild($this->getTreeFromString('userTsConfig-user', $backendUser->user['TSconfig'], $tokenizer, $cache));
}
if (trim($backendUser->user['tsconfig_includes'] ?? '')) {
$includeTsConfigFileList = GeneralUtility::trimExplode(',', $backendUser->user['tsconfig_includes'], true);
foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
$content = $this->getContentOfTsconfigFile($includeTsConfigFile);
if (!empty($content)) {
$includeTree->addChild($this->getTreeFromString('userTsConfig-include-user' . $key, $content, $tokenizer, $cache));
}
}
}
return $includeTree;
}
public function getPagesTsConfigTree(
array $rootLine,
TokenizerInterface $tokenizer,
?PhpFrontend $cache = null
): RootInclude {
$collectedPagesTsConfigArray = [];
$collectedPagesTsConfigArray += $this->getPackagePageTsConfigTree($cache);
// HEADS up: rootLine may be modified by getSitePagesTsConfigTree
$collectedPagesTsConfigArray += $this->getSitePageTsConfigTree($rootLine, $cache);
$collectedPagesTsConfigArray += $this->getRootlinePageTsConfigTree($rootLine, $cache);
$event = $this->eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent(
array_map(static fn(array $descriptor): string => $descriptor['content'], $collectedPagesTsConfigArray),
$rootLine
));
$collectedPagesTsConfigContentArray = $event->getTsConfig();
foreach ($collectedPagesTsConfigContentArray as $key => $content) {
$collectedPagesTsConfigArray[$key]['content'] = $content;
}
$includeTree = new RootInclude();
foreach ($collectedPagesTsConfigArray as $key => $descriptor) {
$typoScriptString = $descriptor['content'];
$filename = $descriptor['filename'] ?? null;
$includeTree->addChild($this->getTreeFromString((string)$key, $typoScriptString, $tokenizer, $cache, $filename));
}
return $includeTree;
}
private function getPackagePageTsConfigTree(
?PhpFrontend $cache = null
): array {
$collectedPagesTsConfigArray = [];
$gotPackagesPagesTsConfigFromCache = false;
$cacheIdentifier = (new PackageDependentCacheIdentifier($this->packageManager))
->withPrefix('pagestsconfig-packages-strings')
->toString();
if ($cache) {
$collectedPagesTsConfigArrayFromCache = $cache->require($cacheIdentifier);
if ($collectedPagesTsConfigArrayFromCache) {
$gotPackagesPagesTsConfigFromCache = true;
$collectedPagesTsConfigArray = $collectedPagesTsConfigArrayFromCache;
}
}
if (!$gotPackagesPagesTsConfigFromCache) {
$event = $this->eventDispatcher->dispatch(new BeforeLoadedPageTsConfigEvent());
$collectedPagesTsConfigArray = array_map(static fn(string $config): array => ['content' => $config, 'filename' => null], $event->getTsConfig());
foreach ($this->packageManager->getActivePackages() as $package) {
$packagePath = $package->getPackagePath();
$tsConfigFile = null;
if (file_exists($packagePath . 'Configuration/page.tsconfig')) {
$tsConfigFile = $packagePath . 'Configuration/page.tsconfig';
} elseif (file_exists($packagePath . 'Configuration/Page.tsconfig')) {
$tsConfigFile = $packagePath . 'Configuration/Page.tsconfig';
}
if ($tsConfigFile) {
$typoScriptString = @file_get_contents($tsConfigFile);
if (!empty($typoScriptString)) {
$collectedPagesTsConfigArray['pagesTsConfig-package-' . $package->getPackageKey()] = [
'filename' => $tsConfigFile,
'content' => $typoScriptString,
];
}
}
}
$cache?->set($cacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($collectedPagesTsConfigArray), '\'\\') . '\');');
}
return $collectedPagesTsConfigArray;
}
private function getSitePageTsConfigTree(
array &$rootLine,
?PhpFrontend $cache = null
): array {
$reverseRootLine = array_reverse($rootLine);
$rootlineUntilSite = [];
$rootSite = null;
foreach ($reverseRootLine as $rootLineEntry) {
array_unshift($rootlineUntilSite, $rootLineEntry);
$uid = (int)($rootLineEntry['uid'] ?? 0);
if ($uid === 0) {
continue;
}
try {
$site = $this->siteFinder->getSiteByRootPageId($uid);
} catch (SiteNotFoundException) {
continue;
}
if ($site->isTypoScriptRoot()) {
$rootSite = $site;
$rootLine = $rootlineUntilSite;
break;
}
}
if ($rootSite === null) {
return [];
}
$cacheIdentifier = (new PackageDependentCacheIdentifier($this->packageManager))
->withPrefix('pagestsconfig-site')
->withAdditionalHashedIdentifier($rootSite->getIdentifier())
->toString();
$pageTsConfig = $cache?->require($cacheIdentifier) ?: null;
if ($pageTsConfig === null) {
$pageTsConfig = [];
$sets = $this->setRegistry->getSets(...$rootSite->getSets());
foreach ($sets as $set) {
if ($set->pagets === null) {
continue;
}
$filename = GeneralUtility::getFileAbsFileName($set->pagets);
if (!file_exists($filename)) {
continue;
}
$content = @file_get_contents($filename);
if (!empty($content)) {
$pageTsConfig['pageTsConfig-set-' . str_replace('/', '-', $set->name)] = [
'filename' => $filename,
'content' => $content,
];
}
}
$pageTsConfig['pageTsConfig-site-' . $rootSite->getIdentifier()] = [
'filename' => GeneralUtility::getFileAbsFileName(Environment::getConfigPath() . '/sites/' . $rootSite->getIdentifier() . '/page.tsconfig'),
'content' => $rootSite->getTSconfig()->pageTSconfig ?? '',
];
$cache?->set($cacheIdentifier, 'return ' . var_export($pageTsConfig, true) . ';');
}
return $pageTsConfig;
}
private function getRootlinePageTsConfigTree(
array $rootLine,
?PhpFrontend $cache = null
): array {
$collectedPagesTsConfigArray = [];
foreach ($rootLine as $page) {
if (empty($page['uid'])) {
// Page 0 can happen when the rootline is given from BE context. It has not TSconfig. Skip this.
continue;
}
if (trim($page['tsconfig_includes'] ?? '')) {
$includeTsConfigFileList = GeneralUtility::trimExplode(',', $page['tsconfig_includes'], true);
foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
$content = $this->getContentOfTsconfigFile($includeTsConfigFile);
if (!empty($content)) {
$collectedPagesTsConfigArray['pagesTsConfig-page-' . $page['uid'] . '-includes-' . $key] = [
'content' => $content,
];
}
}
}
if (!empty($page['TSconfig'])) {
$collectedPagesTsConfigArray['pagesTsConfig-page-' . $page['uid'] . '-tsConfig'] = ['content' => $page['TSconfig']];
}
}
return $collectedPagesTsConfigArray;
}
private function getContentOfTsconfigFile(string $path): string
{
if (PathUtility::isExtensionPath($path)) {
[$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode('/', substr($path, 4), 2);
if ($includeTsConfigFilename !== ''
&& $includeTsConfigFileExtensionKey !== ''
&& ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
) {
$extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey);
$includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename);
if (str_starts_with($includeTsConfigFileAndPath, $extensionPath) && file_exists($includeTsConfigFileAndPath)) {
return (string)file_get_contents($includeTsConfigFileAndPath);
}
}
}
return '';
}
private function getTreeFromString(
string $name,
string $typoScriptString,
TokenizerInterface $tokenizer,
?PhpFrontend $cache = null,
?string $filename = null,
): TsConfigInclude {
$lowercaseName = mb_strtolower($name);
$identifier = (new PackageDependentCacheIdentifier($this->packageManager))
->withPrefix($lowercaseName)
->withAdditionalHashedIdentifier($typoScriptString)
->toString();
if ($cache) {
$includeNode = $cache->require($identifier);
if ($includeNode instanceof TsConfigInclude) {
return $includeNode;
}
}
$includeNode = new TsConfigInclude();
$includeNode->setName($name);
if ($filename !== null) {
$includeNode->setPath($filename);
}
$includeNode->setLineStream($tokenizer->tokenize($typoScriptString));
$this->treeFromTokenStreamBuilder->buildTree($includeNode, 'tsconfig', $tokenizer);
$cache?->set($identifier, 'return unserialize(\'' . addcslashes(serialize($includeNode), '\'\\') . '\');');
return $includeNode;
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface;
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SysTemplateInclude;
/**
* Main visitor that creates the TypoScript AST: When adding this visitor
* and traversing the IncludeTree, the final AST can be fetched using getAst().
*
* This visitor is usually only used together with ConditionVerdictAwareIncludeTreeTraverser,
* and the IncludeTreeConditionMatcherVisitor is added *before* this visitor to determine
* condition verdicts, so AST is only extended for conditions with "true" verdict.
*
* When parsing "setup", "flattened" constants should be assigned to this visitor, so
* the AstBuilder can resolve constants.
*
* @internal: Internal tree structure.
*/
// Ast builder visitor creates state and should not be re-used
#[Autoconfigure(public: true, shared: false)]
final class IncludeTreeAstBuilderVisitor implements IncludeTreeVisitorInterface
{
private RootNode $ast;
/**
* @var array<string, string>
*/
private array $flatConstants = [];
public function __construct(private readonly AstBuilderInterface $astBuilder)
{
$this->ast = new RootNode();
}
/**
* When 'setup' is parsed, setting resolved flat constants here will make
* the AST builder substitute these constants.
*
* @param array<string, string> $flatConstants
*/
public function setFlatConstants(array $flatConstants): void
{
$this->flatConstants = $flatConstants;
}
public function getAst(): RootNode
{
return $this->ast;
}
/**
* Reset AST if "clear" flag is set. That's a sys_template record specific thing
* to restart with a new RootNode and drop any AST calculated already.
*/
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if ($include instanceof SysTemplateInclude && $include->isClear()) {
// Reset any given AST if this sys_template row has clear flag (constants or setup clear) set.
$this->ast = new RootNode();
}
}
/**
* Extend current AST with given LineStream of include node.
*/
public function visit(IncludeInterface $include, int $currentDepth): void
{
$lineStream = $include->getLineStream();
if ($lineStream && !$include->isSplit()) {
// A "split" include means that the entire TypoScript is split into child includes. The
// TokenStream of the split include itself must not be parsed, so it's excluded here.
$this->ast = $this->astBuilder->build($lineStream, $this->ast, $this->flatConstants);
}
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\TypoScript\AST\CommentAwareAstBuilder;
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SysTemplateInclude;
/**
* Secondary visitor that creates the TypoScript AST: When adding this visitor
* and traversing the IncludeTree, the final AST can be fetched using getAst().
* This is an "extended" version of IncludeTreeAstBuilderVisitor that uses
* the CommentAwareAstBuilder instead of the AstBuilder to build the AST: This special
* AST builder is comment aware and adds TypoScript comments to nodes.
*
* This visitor is used in ext:tstemplate TypoScript modules and ext:backend page TSconfig
* to allow implementation of the "comment" related functionality.
*
* When parsing "setup", "flattened" constants should be assigned to this visitor, so
* the AstBuilder can resolve constants.
*
* @internal: Internal tree structure.
*/
// This Ast builder visitor creates state and should not be re-used
#[Autoconfigure(public: true, shared: false)]
final class IncludeTreeCommentAwareAstBuilderVisitor implements IncludeTreeVisitorInterface
{
private RootNode $ast;
/**
* @var array<string, string>
*/
private array $flatConstants = [];
public function __construct(private readonly CommentAwareAstBuilder $astBuilder)
{
$this->ast = new RootNode();
}
/**
* When 'setup' is parsed, setting resolved flat constants here will make
* the AST builder substitute these constants.
*
* @param array<string, string> $flatConstants
*/
public function setFlatConstants(array $flatConstants): void
{
$this->flatConstants = $flatConstants;
}
public function getAst(): RootNode
{
return $this->ast;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if ($include instanceof SysTemplateInclude && $include->isClear()) {
// Reset any given AST if this sys_template row has clear flag (constants or setup clear) set.
$this->ast = new RootNode();
}
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
$tokenStream = $include->getLineStream();
if ($tokenStream && !$include->isSplit()) {
$this->ast = $this->astBuilder->build($tokenStream, $this->ast, $this->flatConstants);
}
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* Gather conditions in an IncludeTree.
*
* This visitor is used in ext:tstemplate TypoScript modules and ext:backend page TSconfig
* backend modules to find available conditions and make them toggleable.
*
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
final class IncludeTreeConditionAggregatorVisitor implements IncludeTreeVisitorInterface
{
/**
* @var array<int, array<string, string>>
*/
private array $conditions = [];
/**
* Get accumulated conditions gathered by visit().
*/
public function getConditions(): array
{
return $this->conditions;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
// No-op. Magic happens in visit()
}
/**
* If the given include is an IncludeConditionInterface, grab it's original (unchanged by constants)
* condition token.
*/
public function visit(IncludeInterface $include, int $currentDepth): void
{
if (!$include instanceof IncludeConditionInterface) {
return;
}
$condition = $include->getConditionToken()->getValue();
if (!in_array($condition, array_column($this->conditions, 'value'))) {
$this->conditions[] = [
'value' => $condition,
'originalValue' => $include->getOriginalConditionToken()?->getValue(),
];
}
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* Force condition verdicts.
*
* This visitor is used in ext:tstemplate TypoScript modules and ext:backend page TSconfig
* backend modules to toggle on/off selected conditions.
*
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
final class IncludeTreeConditionEnforcerVisitor implements IncludeTreeVisitorInterface
{
/**
* @var array<int, string>
*/
private array $enabledConditions;
public function setEnabledConditions(array $enabledConditions): void
{
$this->enabledConditions = $enabledConditions;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if (!$include instanceof IncludeConditionInterface) {
return;
}
$conditionValue = $include->getConditionToken()->getValue();
if (in_array($conditionValue, $this->enabledConditions) && !$include->isConditionNegated()
|| !in_array($conditionValue, $this->enabledConditions) && $include->isConditionNegated()
) {
$include->setConditionVerdict(true);
} else {
$include->setConditionVerdict(false);
}
}
public function visit(IncludeInterface $include, int $currentDepth): void {}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
/**
* This is used in FE to "gather" condition nodes as a flat tree (root + condition nodes).
* The FE uses this optimized tree to quickly determine condition verdicts without loading
* the full tree.
*
* @internal: Internal tree structure.
*/
// This visitor creates state and should not be re-used
#[Autoconfigure(public: true, shared: false)]
final class IncludeTreeConditionIncludeListAccumulatorVisitor implements IncludeTreeVisitorInterface
{
private RootInclude $rootInclude;
public function __construct()
{
$this->rootInclude = new RootInclude();
}
public function getConditionIncludes(): RootInclude
{
return $this->rootInclude;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if (!$include instanceof IncludeConditionInterface) {
return;
}
/** @var IncludeConditionInterface&IncludeInterface $newConditionInclude */
$newConditionInclude = (new ($include::class));
$newConditionInclude->setConditionToken($include->getConditionToken());
$this->rootInclude->addChild($newConditionInclude);
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
// Noop, just implement interface.
}
}
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\ExpressionLanguage\RequestWrapper;
use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
use TYPO3\CMS\Core\Page\PageLayoutResolver;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* A visitor that looks at IncludeConditionInterface nodes and
* evaluates their conditions.
*
* Condition matching is done in visitBeforeChildren() to be used in combination with
* ConditionVerdictAwareIncludeTreeTraverser, so children are only traversed for
* conditions that evaluated true.
*
* @internal: Internal tree structure.
*/
// This visitor creates state and should not be re-used
#[Autoconfigure(public: true, shared: false)]
final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInterface
{
private Resolver $resolver;
private array $conditionList = [];
public function __construct(
private readonly Context $context,
private readonly PageLayoutResolver $pageLayoutResolver,
private readonly LoggerInterface $logger,
) {}
/**
* Prepare the core expression language Resolver class - our API to symfony
* expression language - for typoscript context usage.
*
* The method gets a series of variables hand over coming from caller scope
* like rootline, page array and eventually a request object. These vars are
* munged around a bit and enriched with a series of semi-static state variables:
* Things that can be injected like derived from context, for example
* frontend / backend user, workspace and similar.
* This ensures all typoscript 'conditions' receive similar structured data.
*/
public function initializeExpressionMatcherWithVariables(array $variables): void
{
$context = $this->context;
$enrichedVariables = [
'context' => $context,
];
// Variables derived directly from context are set if context provides according aspects.
$frontendUserAspect = $this->context->getAspect('frontend.user');
if ($frontendUserAspect instanceof UserAspect) {
$frontend = new \stdClass();
$frontend->user = new \stdClass();
$frontend->user->isLoggedIn = $frontendUserAspect->get('isLoggedIn');
$frontend->user->userId = $frontendUserAspect->get('id');
$frontend->user->userGroupList = implode(',', $frontendUserAspect->get('groupIds'));
$frontend->user->userGroupIds = $frontendUserAspect->get('groupIds');
$enrichedVariables['frontend'] = $frontend;
}
$backendUserAspect = $this->context->getAspect('backend.user');
if ($backendUserAspect instanceof UserAspect) {
$backend = new \stdClass();
$backend->user = new \stdClass();
$backend->user->isAdmin = $backendUserAspect->get('isAdmin');
$backend->user->isLoggedIn = $backendUserAspect->get('isLoggedIn');
$backend->user->userId = $backendUserAspect->get('id');
$backend->user->userGroupList = implode(',', $backendUserAspect->get('groupIds'));
$backend->user->userGroupIds = $backendUserAspect->get('groupIds');
$enrichedVariables['backend'] = $backend;
}
$workspaceAspect = $this->context->getAspect('workspace');
if ($workspaceAspect instanceof WorkspaceAspect) {
$workspace = new \stdClass();
$workspace->workspaceId = $workspaceAspect->get('id');
$workspace->isLive = $workspaceAspect->get('isLive');
$workspace->isOffline = $workspaceAspect->get('isOffline');
$enrichedVariables['workspace'] = $workspace;
}
$pageId = $variables['pageId'] ?? 0;
// If rootLine is given, create an object that contains some prepared values.
$fullRootLine = $variables['fullRootLine'] ?? null;
if ($fullRootLine === null && $pageId > 0) {
$fullRootLine = BackendUtility::BEgetRootLine($pageId, '', true);
ksort($fullRootLine);
}
// 'tree' is always exposed to the expression language, even when no rootline could be
// determined (e.g. DataHandler CLI operations on orphaned records with a pid pointing to
// a non-existing page). Conditions like '[123 in tree.rootLineIds]' must then evaluate
// to false instead of raising a SyntaxError for an unknown 'tree' variable.
$localRootLine = $variables['localRootLine'] ?? $fullRootLine ?? [];
$tree = new \stdClass();
$tree->level = count($localRootLine) - 1;
$tree->rootLine = $localRootLine;
$tree->fullRootLine = $fullRootLine ?? [];
$tree->rootLineIds = array_column($localRootLine, 'uid');
$tree->rootLineParentIds = array_slice(array_column($localRootLine, 'pid'), 1);
$tree->pagelayout = null;
if ($localRootLine !== []) {
// We're feeding the "full" RootLine here, not the "local" one that stops at sys_template record having 'root' set.
// This is to be in-line with backend here: A 'backend_layout_next_level' on a page above sys_template 'root' page should
// still be considered. Normally, $fullRootLine is "deepest page first, then up". This is needed for getLayoutForPage() to find
// the 'nearest' parent. However, here it is always passed sorted, so it is a top-down rootLine. Hence, this needs to be once
// again reversed at this point.
$bottomUpFullRootLine = array_reverse($fullRootLine);
$tree->pagelayout = $this->pageLayoutResolver->getLayoutIdentifierForPage($variables['page'], $bottomUpFullRootLine);
}
$enrichedVariables['tree'] = $tree;
// If a request is given, make sure it is an instance of RequestWrapper,
// if not, create an instance from ServerRequestInterface and set it.
if (isset($variables['request']) && !($variables['request'] instanceof RequestWrapper)) {
$variables['request'] = new RequestWrapper($variables['request']);
} elseif (!isset($variables['request'])) {
$variables['request'] = new RequestWrapper(null);
}
// We do not expose pageId, rootLine and fullRootLine to conditions directly.
unset($variables['pageId'], $variables['localRootLine'], $variables['fullRootLine']);
$enrichedVariables = array_replace($enrichedVariables, $variables);
$this->resolver = new Resolver('typoscript', $enrichedVariables);
}
/**
* A list of all handled conditions with their verdicts.
* This is used in FE since condition verdicts influence page caches.
*/
public function getConditionListWithVerdicts(): array
{
return $this->conditionList;
}
/**
* Let symfony expression language handle the expression, gather expressions
* that have been handled since they influence page caching, negate expression
* verdicts if they're a [else] expression.
*/
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if (!$include instanceof IncludeConditionInterface) {
return;
}
$conditionExpression = $include->getConditionToken()->getValue();
try {
$verdict = (bool)$this->resolver->evaluate($conditionExpression);
} catch (SyntaxError $e) {
$this->logger->error('TypoScript condition [{expression}] could not be parsed: {error}', [
'expression' => $conditionExpression,
'error' => $e->getMessage(),
'exception' => $e,
]);
$verdict = false;
} catch (\RuntimeException $e) {
throw new \RuntimeException(
sprintf('TypoScript condition [%s] could not be evaluated: %s', $conditionExpression, $e->getMessage()),
1731486757,
$e
);
}
if ($include->isConditionNegated()) {
// Honor ConditionElseInclude "[ELSE]" which negates the verdict of the main condition.
$verdict = !$verdict;
}
$this->conditionList[$conditionExpression] = $verdict;
$include->setConditionVerdict($verdict);
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
// Noop, just implement interface
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* Find a single node in tree identified by node identifier.
*
* This visitor is used in ext:tstemplate TypoScript modules and ext:backend page TSconfig
* backend modules to find single nodes, for instance when their source should be rendered.
*
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
final class IncludeTreeNodeFinderVisitor implements IncludeTreeVisitorInterface
{
private ?IncludeInterface $foundNode = null;
private string $nodeIdentifier;
public function setNodeIdentifier(string $nodeIdentifier)
{
$this->nodeIdentifier = $nodeIdentifier;
}
public function getFoundNode(): ?IncludeInterface
{
return $this->foundNode;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if ($include->getIdentifier() === $this->nodeIdentifier) {
$this->foundNode = $include;
}
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
// Implement interface
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
/**
* Handle constants within (TS setup) conditions:
* When a conditional include is like this: '["{$foo.bar}" == "4711"]', this visitor looks
* up 'foo.bar in given (flattened) constants and substitutes it with the constant value.
* The 'include' object then contains the substituted condition token for 'getConditionToken()',
* while the original token without the substitution is parked in 'getOriginalConditionToken()'.
* The latter is done to have the original token available in the backend to show, it is irrelevant in frontend.
*
* @internal: Internal tree structure.
*/
// This visitor creates state and should not be re-used
#[Autoconfigure(public: true, shared: false)]
final class IncludeTreeSetupConditionConstantSubstitutionVisitor implements IncludeTreeVisitorInterface
{
/**
* @var array<string, string>
*/
private array $flattenedConstants;
/**
* Must be set when adding this visitor, to an empty array at least.
* Will fatal otherwise, and that's fine, since if not setting this,
* this visitor is useless and shouldn't be added at all.
*
* @param array<string, string> $flattenedConstants
*/
public function setFlattenedConstants(array $flattenedConstants): void
{
$this->flattenedConstants = $flattenedConstants;
}
/**
* Do the magic, see tests for details.
* Implementation within 'visitBeforeChildren()' since this allows running *both* this
* visitor first, and then IncludeTreeConditionMatcherVisitor directly afterward in the same
* traverser cycle!
*/
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if (!$include instanceof IncludeConditionInterface) {
return;
}
$conditionToken = $include->getConditionToken();
$conditionValue = $conditionToken->getValue();
$flattenedConstants = $this->flattenedConstants;
$hadSubstitution = false;
$newConditionValue = preg_replace_callback(
'/{\$(.[^}]*)}/',
static function ($match) use ($flattenedConstants, &$hadSubstitution) {
// Replace {$someConstant} if found, else leave unchanged
if (array_key_exists($match[1], $flattenedConstants)) {
$hadSubstitution = true;
return $flattenedConstants[$match[1]];
}
return $match[0];
},
$conditionValue
);
if ($hadSubstitution) {
$include->setOriginalConditionToken($conditionToken);
$include->setConditionToken(new Token(TokenType::T_VALUE, $newConditionValue, $conditionToken->getLine(), $conditionToken->getColumn()));
}
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
// Noop, just implement interface
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\AtImportInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionElseInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* Create a TypoScript source back from an IncludeTree. Inline source from
* "@import" and friends.
*
* This visitor is used in ext:tstemplate TypoScript modules and ext:backend page TSconfig
* backend modules to show code of single includes with their resolved imports.
*
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
final class IncludeTreeSourceAggregatorVisitor implements IncludeTreeVisitorInterface
{
/**
* The accumulated source.
*/
private string $source = '';
/**
* Restrict source rendering to specific includes. Used in BE template analyzer
* to output source of a single include and its sub includes. Since a single include
* could be included multiple times, we track if source for it has been build to
* suppress outputting it multiple times.
*/
private string $startNodeIdentifier = '';
private bool $startNodeHandled = false;
private int $startNodeDepth = 0;
private bool $isWithinStartNode = false;
public function setStartNodeIdentifier(string $startNodeIdentifier)
{
$this->startNodeIdentifier = $startNodeIdentifier;
}
public function getSource(): string
{
return $this->source;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
{
if ($this->startNodeHandled && $currentDepth <= $this->startNodeDepth) {
$this->isWithinStartNode = false;
}
if ($this->startNodeIdentifier === $include->getIdentifier() && !$this->startNodeHandled) {
$this->startNodeDepth = $currentDepth;
$this->isWithinStartNode = true;
$this->startNodeHandled = true;
}
if (empty($this->startNodeIdentifier) || $this->isWithinStartNode) {
$lineStream = $include->getLineStream();
if ($lineStream !== null
&& !$lineStream->isEmpty()
&& ($include instanceof ConditionInclude || $include instanceof ConditionElseInclude)
) {
$this->source .= "\n#\n# Condition from '" . $include->getName() . '\' Line ' . $include->getConditionToken()->getLine() . "\n#\n";
$this->source .= $lineStream;
}
if ($include instanceof AtImportInclude) {
$this->source .= "\n#\n# Include from definition '" . trim((string)($include->getOriginalLine()->getTokenStream())) . "'\n#\n";
}
}
}
public function visit(IncludeInterface $include, int $currentDepth): void
{
if (empty($this->startNodeIdentifier) || $this->isWithinStartNode) {
$lineStream = $include->getLineStream();
if ($lineStream === null
|| $lineStream->isEmpty()
|| ($include->isSplit())
) {
return;
}
$this->source .= "\n#\n# Content from '" . $include->getName() . "'\n#\n";
$this->source .= $lineStream;
}
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\AtImportInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionElseInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\ConditionInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\InvalidLine;
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineInterface;
/**
* This implements a simple TypoScript syntax scanner. It is used in page TSconfig
* and TypoScript "include" submodules to find and show broken syntax.
*
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
*/
final class IncludeTreeSyntaxScannerVisitor implements IncludeTreeVisitorInterface
{
/**
* @var list<array{type: string, include: IncludeInterface, line: LineInterface, lineNumber: int}>
*/
private array $errors = [];
/**
* @return list<array{type: string, include: IncludeInterface, line: LineInterface, lineNumber: int}>
*/
public function getErrors(): array
{
return $this->errors;
}
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void {}
public function visit(IncludeInterface $include, int $currentDepth): void
{
$this->brokenLinesAndBraces($include);
$this->emptyImports($include);
// Add the line number of the first token of the line object to the error array.
// Not strictly needed, but more convenient in Fluid template to render.
foreach ($this->errors as &$error) {
/** @var LineInterface $line */
$line = $error['line'];
$error['lineNumber'] = $line->getTokenStream()->reset()->peekNext()->getLine();
}
// Sort array by line number to list them top->bottom in view.
usort($this->errors, fn($a, $b) => $a['lineNumber'] <=> $b['lineNumber']);
}
/**
* Scan for invalid lines ("foo.bar <" is invalid since there must be something after "<"),
* and scan for "too many" and "not enough" "}" braces.
*/
private function brokenLinesAndBraces(IncludeInterface $include): void
{
if ($include->isSplit()) {
// If this node is split, don't check for syntax errors, this is
// done for child nodes.
return;
}
$lineStream = $include->getLineStream();
if (!$lineStream) {
return;
}
$braceCount = 0;
$lastLine = null;
foreach ($lineStream->getNextLine() as $line) {
$lastLine = $line;
if ($line instanceof InvalidLine) {
$this->errors[] = [
'type' => 'line.invalid',
'include' => $include,
'line' => $line,
];
}
if ($line instanceof IdentifierBlockOpenLine) {
$braceCount++;
}
if ($line instanceof BlockCloseLine) {
$braceCount--;
if ($braceCount < 0) {
$braceCount = 0;
$this->errors[] = [
'type' => 'brace.excess',
'include' => $include,
'line' => $line,
];
}
}
}
if ($braceCount !== 0) {
$this->errors[] = [
'type' => 'brace.missing',
'include' => $include,
'line' => $lastLine,
];
}
}
/**
* Look for @import that don't find to-include file(s).
*
* @todo: This code is far more complex than it could be. See #102102 and #102103 for
* changes we should apply to the include tree structure to simplify this.
*/
private function emptyImports(IncludeInterface $include): void
{
if (!$include->isSplit()) {
// Nodes containing @import are always split
return;
}
$lineStream = $include->getLineStream();
if (!$lineStream) {
// A node that is split should never have an empty line stream,
// this may be obsolete, but does not hurt much.
return;
}
// Find @import lines in this include, index by
// combination of line number and column position.
$allImportLines = [];
foreach ($lineStream->getNextLine() as $line) {
if ($line instanceof ImportLine) {
$valueToken = $line->getValueToken();
$allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()] = $line;
}
}
// Now iterate children to exclude valid allImportLines, those that included something.
foreach ($include->getNextChild() as $child) {
if ($child instanceof AtImportInclude) {
/** @var ImportLine $originalLine */
$originalLine = $child->getOriginalLine();
$valueToken = $originalLine->getValueToken();
unset($allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()]);
}
// Condition includes don't have the "body" lines itself (or a "body" sub node). This may change,
// but until then we'll have to scan the parent node and loop condition includes here to find out
// which of them resolved to child nodes.
if ($child instanceof ConditionInclude || $child instanceof ConditionElseInclude) {
foreach ($child->getNextChild() as $conditionChild) {
if ($conditionChild instanceof AtImportInclude) {
/** @var ImportLine $originalLine */
$originalLine = $conditionChild->getOriginalLine();
$valueToken = $originalLine->getValueToken();
unset($allImportLines[$valueToken->getLine() . '-' . $valueToken->getColumn()]);
}
}
}
}
// Everything left are invalid includes
foreach ($allImportLines as $importLine) {
$this->errors[] = [
'type' => 'import.empty',
'include' => $include,
'line' => $importLine,
];
}
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* 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\TypoScript\IncludeTree\Visitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
/**
* A visitor that can be attached to IncludeTreeTraverser's.
*
* @internal: Internal tree structure.
*/
interface IncludeTreeVisitorInterface
{
/**
* Gets called by the traversers *before* children are traversed. Useful for
* instance for the IncludeTreeConditionMatcherVisitor to evaluate a condition
* verdict *before* children are traversed (or not).
*/
public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void;
/**
* Main visit method called for each node.
*/
public function visit(IncludeInterface $include, int $currentDepth): void;
}