TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
<?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\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ReferenceChildNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Common methods of both AST builders.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractAstBuilder
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $flatConstants = [];
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected function handleIdentifierUnsetLine(IdentifierUnsetLine $line, CurrentObjectPath $currentObjectPath): void
|
||||
{
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $line->getIdentifierTokenStream()->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
if (!$foundNode = $node->getChildByName($identifierToken->getValue())) {
|
||||
break;
|
||||
}
|
||||
$nextIdentifierToken = $identifierStream->peekNext();
|
||||
if ($nextIdentifierToken) {
|
||||
$node = $foundNode;
|
||||
continue;
|
||||
}
|
||||
$node->removeChildByName($identifierToken->getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleIdentifierCopyLine(IdentifierCopyLine $line, RootNode $rootNode, CurrentObjectPath $currentObjectPath): ?NodeInterface
|
||||
{
|
||||
$sourceIdentifierStream = $line->getValueTokenStream()->reset();
|
||||
$sourceNode = $rootNode;
|
||||
if ($sourceIdentifierStream->isRelative()) {
|
||||
// Entry node is current node from current object path if relative, otherwise RootNode.
|
||||
$sourceNode = $currentObjectPath->getLast();
|
||||
}
|
||||
while ($identifierToken = $sourceIdentifierStream->getNext()) {
|
||||
// Go through source token stream and locate the sourceNode to copy from.
|
||||
if (!$sourceNode = $sourceNode->getChildByName($identifierToken->getValue())) {
|
||||
// Source node not found - nothing to do for this line
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$isSourceNodeValueNull = true;
|
||||
if ($sourceNode->getValue() !== null) {
|
||||
// When the source node value is not null, it will override the target node value if that exists.
|
||||
$isSourceNodeValueNull = false;
|
||||
}
|
||||
|
||||
// Locate/create the targets parent node the copied source should be added as child to,
|
||||
// and get the name of the node we're dealing with.
|
||||
$targetIdentifierTokenStream = $line->getIdentifierTokenStream()->reset();
|
||||
$targetParentNode = $currentObjectPath->getFirst();
|
||||
$targetTokenName = null;
|
||||
while ($targetToken = $targetIdentifierTokenStream->getNext()) {
|
||||
$targetTokenName = $targetToken->getValue();
|
||||
if (!($targetIdentifierTokenStream->peekNext() ?? false)) {
|
||||
break;
|
||||
}
|
||||
if (!$foundNode = $targetParentNode->getChildByName($targetTokenName)) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($targetTokenName);
|
||||
$targetParentNode->addChild($foundNode);
|
||||
}
|
||||
$targetParentNode = $foundNode;
|
||||
}
|
||||
|
||||
$existingTarget = null;
|
||||
if ($isSourceNodeValueNull) {
|
||||
// When the node to copy has no value, but the existing target has,
|
||||
// the value from the existing target is kept. Also, if the existing
|
||||
// node is a ReferenceChildNode and the source does not override this,
|
||||
// source children are added to the existing reference instead of
|
||||
// dropping the existing target.
|
||||
$existingTarget = $targetParentNode->getChildByName($targetTokenName);
|
||||
$existingTargetNodeValue = $existingTarget?->getValue();
|
||||
} else {
|
||||
// Blindly remove existing target node if exists and the value is not overridden by source.
|
||||
$targetParentNode->removeChildByName($targetTokenName);
|
||||
}
|
||||
if ($existingTarget instanceof ReferenceChildNode) {
|
||||
// When existing target is a ReferenceChildNode, keep it and
|
||||
// copy children from source into existing target.
|
||||
$targetNode = $existingTarget;
|
||||
foreach ($sourceNode->getNextChild() as $sourceChild) {
|
||||
$targetNode->addChild(clone $sourceChild);
|
||||
}
|
||||
} else {
|
||||
// Clone full source node tree, update name and add as child to parent node.
|
||||
/** @var ChildNodeInterface $targetNode */
|
||||
$targetNode = clone $sourceNode;
|
||||
$targetNode->updateName($targetTokenName);
|
||||
$targetParentNode->addChild($targetNode);
|
||||
}
|
||||
if ($isSourceNodeValueNull && $existingTargetNodeValue) {
|
||||
// If value of old existing target should be kept, set in now.
|
||||
$targetNode->setValue($existingTargetNodeValue);
|
||||
}
|
||||
|
||||
return $targetNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* "foo =< bar": Prepare a reference resolving.
|
||||
* Note this does *not* resolve "=<" itself at this point since this operator can only be
|
||||
* evaluated after the full AST has been established. Also, having a full AST-traverser run
|
||||
* that does this is *very* expensive and "=<" is only done for "tt_content.myElement" and
|
||||
* "lib.parseFunc" anyways. As such, "=<" is NOT a language construct itself and the AST-parser
|
||||
* only marks nodes that use it by using the special node "ObjectReference".
|
||||
* Resolving then happens "lazy" and "on demand" in ContentObjectRenderer cObjGetSingle()
|
||||
* and mergeTSRef() for frontend "setup" TypoScript.
|
||||
*/
|
||||
protected function handleIdentifierReferenceLine(IdentifierReferenceLine $line, CurrentObjectPath $currentObjectPath): NodeInterface
|
||||
{
|
||||
$tokenStream = $line->getIdentifierTokenStream();
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $tokenStream->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
$nextIdentifier = $identifierStream->peekNext();
|
||||
$identifierTokenValue = $identifierToken->getValue();
|
||||
if (!($node->getChildByName($identifierTokenValue)) && $nextIdentifier) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($identifierTokenValue);
|
||||
$node->addChild($foundNode);
|
||||
} elseif (!$node->getChildByName($identifierTokenValue) && $nextIdentifier === null) {
|
||||
// Parent of target node exists, but target node does not. Add new reference child.
|
||||
$foundNode = new ReferenceChildNode($identifierTokenValue);
|
||||
$foundNode->setReferenceSourceStream($line->getValueTokenStream());
|
||||
$node->addChild($foundNode);
|
||||
} elseif (($foundNode = $node->getChildByName($identifierTokenValue)) && $nextIdentifier === null) {
|
||||
// Target node exists already. We create a new one, remove old, but transfer existing children from old to new.
|
||||
$newNode = new ReferenceChildNode($identifierTokenValue);
|
||||
$newNode->setReferenceSourceStream($line->getValueTokenStream());
|
||||
foreach ($foundNode->getNextChild() as $existingNodeChild) {
|
||||
$newNode->addChild($existingNodeChild);
|
||||
}
|
||||
$node->removeChildByName($identifierTokenValue);
|
||||
$node->addChild($newNode);
|
||||
$foundNode = $newNode;
|
||||
}
|
||||
$node = $foundNode;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getOrAddNodeFromIdentifierStream(CurrentObjectPath $currentObjectPath, IdentifierTokenStream $tokenStream): NodeInterface
|
||||
{
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $tokenStream->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
$identifierTokenValue = $identifierToken->getValue();
|
||||
if (!$foundNode = $node->getChildByName($identifierTokenValue)) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($identifierTokenValue);
|
||||
$node->addChild($foundNode);
|
||||
}
|
||||
$node = $foundNode;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate operator functions, example TypoScript:
|
||||
* "page.10.value := appendString(foo)"
|
||||
*/
|
||||
protected function evaluateValueModifier(Token $functionNameToken, TokenStreamInterface $functionArgumentTokenStream, ?string $originalValue): ?string
|
||||
{
|
||||
$functionName = $functionNameToken->getValue();
|
||||
// Constants are evaluated via __toString() of ConstantAwareTokenStream and thus need current constants.
|
||||
// This implements constants in function arguments: "foo := addToList({$my.constant})"
|
||||
if ($functionArgumentTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$functionArgumentTokenStream->setFlatConstants($this->flatConstants);
|
||||
}
|
||||
$functionArgument = (string)$functionArgumentTokenStream;
|
||||
switch ($functionName) {
|
||||
case 'prependString':
|
||||
return $functionArgument . $originalValue;
|
||||
case 'appendString':
|
||||
return $originalValue . $functionArgument;
|
||||
case 'removeString':
|
||||
return str_replace($functionArgument, '', $originalValue);
|
||||
case 'replaceString':
|
||||
$functionValueArray = explode('|', $functionArgument, 2);
|
||||
$fromStr = $functionValueArray[0];
|
||||
$toStr = $functionValueArray[1] ?? '';
|
||||
return str_replace($fromStr, $toStr, $originalValue);
|
||||
case 'addToList':
|
||||
return ($originalValue !== null ? $originalValue . ',' : '') . $functionArgument;
|
||||
case 'removeFromList':
|
||||
$existingElements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
$removeElements = GeneralUtility::trimExplode(',', $functionArgument);
|
||||
if (!empty($removeElements)) {
|
||||
return implode(',', array_diff($existingElements, $removeElements));
|
||||
}
|
||||
return $originalValue;
|
||||
case 'uniqueList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
return implode(',', array_unique($elements));
|
||||
case 'reverseList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
return implode(',', array_reverse($elements));
|
||||
case 'sortList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
$arguments = GeneralUtility::trimExplode(',', $functionArgument);
|
||||
$arguments = array_map('strtolower', $arguments);
|
||||
$sortFlags = SORT_REGULAR;
|
||||
if (in_array('numeric', $arguments)) {
|
||||
$sortFlags = SORT_NUMERIC;
|
||||
// If the sorting modifier "numeric" is given, all values
|
||||
// are checked and an exception is thrown if a non-numeric value is given
|
||||
// otherwise there is a different behaviour between PHP 7 and PHP 5.x
|
||||
// See also the warning on http://us.php.net/manual/en/function.sort.php
|
||||
foreach ($elements as $element) {
|
||||
if (!is_numeric($element)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The list "' . $originalValue . '" should be sorted numerically but contains a non-numeric value',
|
||||
1650893781
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
sort($elements, $sortFlags);
|
||||
if (in_array('descending', $arguments)) {
|
||||
$elements = array_reverse($elements);
|
||||
}
|
||||
return implode(',', $elements);
|
||||
case 'getEnv':
|
||||
$environmentValue = getenv(trim($functionArgument));
|
||||
if ($environmentValue !== false) {
|
||||
return $environmentValue;
|
||||
}
|
||||
return $originalValue;
|
||||
default:
|
||||
return $this->eventDispatcher->dispatch(new EvaluateModifierFunctionEvent($functionName, $functionArgument, $originalValue))->getValue() ?? $originalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPathStack;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
|
||||
/**
|
||||
* The main TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
#[Autoconfigure(public: true), AsAlias(AstBuilderInterface::class)]
|
||||
final class AstBuilder extends AbstractAstBuilder implements AstBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode
|
||||
{
|
||||
$this->flatConstants = $flatConstants;
|
||||
|
||||
$currentObjectPath = new CurrentObjectPath($ast);
|
||||
$currentObjectPathStack = new CurrentObjectPathStack();
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
|
||||
foreach ($lineStream->getNextLine() as $line) {
|
||||
if ($line instanceof IdentifierAssignmentLine) {
|
||||
// "foo = bar" and "foo ( bar )": Single and multi line assignments
|
||||
$this->handleIdentifierAssignmentLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierBlockOpenLine) {
|
||||
// "foo {": Opening a block - push to object path stack
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$currentObjectPath = (new CurrentObjectPath($node));
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
} elseif ($line instanceof BlockCloseLine) {
|
||||
// "}": Closing a block - pop from object path stack
|
||||
$currentObjectPath = $currentObjectPathStack->pop();
|
||||
} elseif ($line instanceof IdentifierUnsetLine) {
|
||||
// "foo >": Remove a path
|
||||
$this->handleIdentifierUnsetLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierCopyLine) {
|
||||
// "foo < bar": Copy a node source path to a target path
|
||||
$this->handleIdentifierCopyLine($line, $ast, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierFunctionLine) {
|
||||
// "foo := addToList(42)": Evaluate functions
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$node->setValue($this->evaluateValueModifier($line->getFunctionNameToken(), $line->getFunctionValueTokenStream(), $node->getValue()));
|
||||
} elseif ($line instanceof IdentifierReferenceLine) {
|
||||
// "foo =< bar": Prepare a reference resolving
|
||||
$this->handleIdentifierReferenceLine($line, $currentObjectPath);
|
||||
}
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
private function handleIdentifierAssignmentLine(IdentifierAssignmentLine $line, CurrentObjectPath $currentObjectPath): void
|
||||
{
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$valueTokenStream = $line->getValueTokenStream();
|
||||
if ($valueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$valueTokenStream = clone $valueTokenStream;
|
||||
$valueTokenStream->setFlatConstants($this->flatConstants);
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return;
|
||||
}
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
}
|
||||
}
|
||||
@@ -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\AST;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
|
||||
/**
|
||||
* The main TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface AstBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPathStack;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\CommentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\EmptyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
|
||||
/**
|
||||
* Secondary TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* This AST builder is comment aware: Comments are assigned to nodes. This is used
|
||||
* in ext:tstemplate and page TSconfig backend modules to add the comment related
|
||||
* TypoScript functionality.
|
||||
*
|
||||
* This AST builder variant adds runtime overhead and is slower than the main
|
||||
* AstBuilder class.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CommentAwareAstBuilder extends AbstractAstBuilder implements AstBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode
|
||||
{
|
||||
$this->flatConstants = $flatConstants;
|
||||
|
||||
$currentObjectPath = new CurrentObjectPath($ast);
|
||||
$currentObjectPathStack = new CurrentObjectPathStack();
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
|
||||
$previousLineComments = [];
|
||||
while ($line = $lineStream->getNext()) {
|
||||
$node = null;
|
||||
if ($line instanceof IdentifierAssignmentLine) {
|
||||
// "foo = bar" and "foo ( bar )": Single and multi line assignments
|
||||
$node = $this->handleIdentifierAssignmentLine($line, $currentObjectPath);
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierBlockOpenLine) {
|
||||
// "foo {": Opening a block - push to object path stack
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
$currentObjectPath = (new CurrentObjectPath($node));
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
} elseif ($line instanceof BlockCloseLine) {
|
||||
// "}": Closing a block - pop from object path stack
|
||||
$currentObjectPath = $currentObjectPathStack->pop();
|
||||
} elseif ($line instanceof IdentifierUnsetLine) {
|
||||
// "foo >": Remove a path
|
||||
$this->handleIdentifierUnsetLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierCopyLine) {
|
||||
// "foo < bar": Copy a node source path to a target path
|
||||
$node = $this->handleIdentifierCopyLine($line, $ast, $currentObjectPath);
|
||||
if ($node && $previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierFunctionLine) {
|
||||
// "foo := addToList(42)": Evaluate functions
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$functionValueTokenStream = $line->getFunctionValueTokenStream();
|
||||
$node->setValue($this->evaluateValueModifier($line->getFunctionNameToken(), clone $functionValueTokenStream, $node->getValue()));
|
||||
if ($functionValueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
// @todo: This is a bit unfortunate. When multiple functions manipulate a value after each other,
|
||||
// only the stream of the last one is preserved, previous ones are lost. This way, the BE modules
|
||||
// can not reflect when previous functions used constants. One idea to solve this is to turn existing
|
||||
// nodes into special "function" nodes that park single operations, which are then executed lazy when
|
||||
// a node value is string'ified. The BE modules could then render full lists of value manipulations and
|
||||
// show how values evolve. Another idea is to change setOriginalValueTokenStream() to gather multiple
|
||||
// streams - probably together with the current value at this point in time, which would also allow
|
||||
// rendering how a value evolves over time as well.
|
||||
$node->setOriginalValueTokenStream($functionValueTokenStream);
|
||||
}
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierReferenceLine) {
|
||||
// "foo =< bar": Prepare a reference resolving
|
||||
$node = $this->handleIdentifierReferenceLine($line, $currentObjectPath);
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof CommentLine) {
|
||||
$nextLine = $lineStream->peekNext();
|
||||
if ($currentObjectPath->getLast() instanceof RootNode && ($nextLine === null || $nextLine instanceof EmptyLine)) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
foreach ($previousLineComments as $commentLineTokenStream) {
|
||||
$ast->addComment($commentLineTokenStream);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
if ($nextLine instanceof CommentLine) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
}
|
||||
if ($nextLine instanceof IdentifierAssignmentLine
|
||||
|| $nextLine instanceof IdentifierBlockOpenLine
|
||||
|| $nextLine instanceof IdentifierCopyLine
|
||||
|| $nextLine instanceof IdentifierFunctionLine
|
||||
|| $nextLine instanceof IdentifierReferenceLine
|
||||
) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly different from AstBuilder since it sets 'previousValue'
|
||||
*/
|
||||
private function handleIdentifierAssignmentLine(IdentifierAssignmentLine $line, CurrentObjectPath $currentObjectPath): NodeInterface
|
||||
{
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$valueTokenStream = $line->getValueTokenStream();
|
||||
if ($valueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$node->setOriginalValueTokenStream($valueTokenStream);
|
||||
$valueTokenStream = clone $valueTokenStream;
|
||||
$valueTokenStream->setFlatConstants($this->flatConstants);
|
||||
$node->setPreviousValue($node->getValue());
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return $node;
|
||||
}
|
||||
$node->setPreviousValue($node->getValue());
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\AST\CurrentObjectPath;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Internal state class to track the current hierarchy in tree.
|
||||
* This is important in combination with block open "{" and block
|
||||
* close "}" brackets.
|
||||
* Also used in BE Template Object Browser tree rendering.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CurrentObjectPath
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface[]
|
||||
*/
|
||||
private array $path;
|
||||
|
||||
public function __construct(NodeInterface ...$path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
public function append(NodeInterface $node): void
|
||||
{
|
||||
$this->path[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn current object path into a string. Quote dots in keys.
|
||||
* Used in BE Template Object Browser tree, expand and search handling.
|
||||
* Not implementing __toString() here since Fluid can't call this.
|
||||
*
|
||||
* Example:
|
||||
* page.10.foo\.bar.baz
|
||||
*/
|
||||
public function getPathAsString(): string
|
||||
{
|
||||
$flatArray = [];
|
||||
foreach ($this->getAll() as $pathNode) {
|
||||
if ($pathNode instanceof RootNode) {
|
||||
continue;
|
||||
}
|
||||
$name = $pathNode->getName();
|
||||
if ($name === '') {
|
||||
throw new \RuntimeException('Node names must not be empty string', 1658578645);
|
||||
}
|
||||
$flatArray[] = addcslashes($name, '.');
|
||||
}
|
||||
return implode('.', $flatArray);
|
||||
}
|
||||
|
||||
public function getFirst(): NodeInterface
|
||||
{
|
||||
return reset($this->path);
|
||||
}
|
||||
|
||||
public function getLast(): NodeInterface
|
||||
{
|
||||
return array_last($this->path);
|
||||
}
|
||||
|
||||
public function getSecondLast(): NodeInterface
|
||||
{
|
||||
return array_slice($this->path, -2, 1)[0];
|
||||
}
|
||||
|
||||
public function removeLast(): void
|
||||
{
|
||||
array_pop($this->path);
|
||||
}
|
||||
}
|
||||
@@ -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\AST\CurrentObjectPath;
|
||||
|
||||
/**
|
||||
* A stack for CurrentObjectPath: When opening a block "{",
|
||||
* CurrentObjectPath is pushed, when closing a block "}", it
|
||||
* is popped from this stack.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CurrentObjectPathStack
|
||||
{
|
||||
/**
|
||||
* @var CurrentObjectPath[]
|
||||
*/
|
||||
private array $stack = [];
|
||||
private int $stackSize = 0;
|
||||
|
||||
public function push(CurrentObjectPath $path): void
|
||||
{
|
||||
$this->stack[] = $path;
|
||||
$this->stackSize++;
|
||||
}
|
||||
|
||||
public function pop(): CurrentObjectPath
|
||||
{
|
||||
if ($this->stackSize === 1) {
|
||||
// Never pop the very last element off from the stack. This is the
|
||||
// RootNode. This prevents errors when TypoScript has a closing
|
||||
// curly bracket '}' too much.
|
||||
return $this->getCurrent();
|
||||
}
|
||||
array_pop($this->stack);
|
||||
$this->stackSize--;
|
||||
return $this->getCurrent();
|
||||
}
|
||||
|
||||
public function getCurrent(): CurrentObjectPath
|
||||
{
|
||||
return array_last($this->stack);
|
||||
}
|
||||
|
||||
public function getFirst(): CurrentObjectPath
|
||||
{
|
||||
return reset($this->stack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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\AST\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to implement own ":=" TypoScript modifier functions, example:
|
||||
*
|
||||
* foo = myOriginalValue
|
||||
* foo := myNewFunction(myFunctionArgument)
|
||||
*
|
||||
* Listeners should take care function names can not overlap with function names
|
||||
* from other extensions and should thus namespace, example naming: "extNewsSortFunction()"
|
||||
*/
|
||||
final class EvaluateModifierFunctionEvent
|
||||
{
|
||||
private ?string $value = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $functionName,
|
||||
private readonly string $functionArgument,
|
||||
private readonly ?string $originalValue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The function name, for example "extNewsSortFunction" when using "foo := extNewsSortFunction()"
|
||||
*/
|
||||
public function getFunctionName(): string
|
||||
{
|
||||
return $this->functionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional function argument, for example "myArgument" when using "foo := extNewsSortFunction(myArgument)"
|
||||
* If the argument contained constants, those have been resolved at this point.
|
||||
*/
|
||||
public function getFunctionArgument(): string
|
||||
{
|
||||
return $this->functionArgument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original / current value, for example "fooValue" when using:
|
||||
* foo = fooValue
|
||||
* foo := extNewsSortFunction(myArgument)
|
||||
*/
|
||||
public function getOriginalValue(): ?string
|
||||
{
|
||||
return $this->originalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the updated value calculated by a listener.
|
||||
* Note you can not set to null to "unset", since getValue() falls back to
|
||||
* originalValue in this case. Set to empty string instead for this edge case.
|
||||
*/
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by AstBuilder to fetch the updated value, falls back to given original value.
|
||||
* Can be used by Listeners to see if a previous listener changed the value already
|
||||
* by comparing with getOriginalValue().
|
||||
*/
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\AST\Merger;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Frontend TypoScript 'setup' has the main 'config' section for global config,
|
||||
* plus a per type / typeNum specific PAGE 'config' (often page.config) that can
|
||||
* override global 'config' per type / typeNum.
|
||||
*
|
||||
* This class merges both into the final 'config', later available in Request
|
||||
* attribute 'frontend.typoscript' getConfigTree() and getConfigArray().
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final readonly class SetupConfigMerger
|
||||
{
|
||||
public function merge(?ChildNodeInterface $config, ?ChildNodeInterface $pageConfig): RootNode
|
||||
{
|
||||
$configResult = new RootNode();
|
||||
if ($config) {
|
||||
foreach ($config->getNextChild() as $child) {
|
||||
$configResult->addChild($child);
|
||||
}
|
||||
}
|
||||
if (!$pageConfig) {
|
||||
return $configResult;
|
||||
}
|
||||
$this->mergeRecursive($pageConfig, $configResult);
|
||||
return $configResult;
|
||||
}
|
||||
|
||||
private function mergeRecursive(ChildNodeInterface $mergeFrom, NodeInterface $mergeTo): void
|
||||
{
|
||||
foreach ($mergeFrom->getNextChild() as $mergeFromChild) {
|
||||
$mergeToChild = $mergeTo->getChildByName($mergeFromChild->getName());
|
||||
if (!$mergeToChild) {
|
||||
$mergeTo->addChild($mergeFromChild);
|
||||
continue;
|
||||
}
|
||||
$mergeFromChildValue = $mergeFromChild->getValue();
|
||||
if ($mergeFromChildValue !== null && $mergeFromChildValue !== $mergeToChild->getValue()) {
|
||||
$mergeToChild->setValue($mergeFromChildValue);
|
||||
}
|
||||
$this->mergeRecursive($mergeFromChild, $mergeToChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
/**
|
||||
* Generic child node. Implements common methods of NodeInterface used
|
||||
* in all Node classes.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractChildNode extends AbstractNode implements ChildNodeInterface
|
||||
{
|
||||
public function __construct(protected string $name) {}
|
||||
|
||||
/**
|
||||
* Dereference children on clone().
|
||||
* Used with '<' operator to create a deep-copy of the tree to copy.
|
||||
*/
|
||||
public function __clone(): void
|
||||
{
|
||||
foreach ($this->children as $childName => $child) {
|
||||
$this->children[$childName] = clone $child;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function toArray(): ?array
|
||||
{
|
||||
if (!$this->hasChildren()) {
|
||||
return null;
|
||||
}
|
||||
$result = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$childName = $child->getName();
|
||||
$childValue = $child->getValue();
|
||||
if ($child instanceof ReferenceChildNode) {
|
||||
// Hack for b/w compat parsing of `=<` operator. See ContentObjectRenderer cObjGetSingle() and mergeTSRef()
|
||||
// @todo: adding the whitespace after '<' is another bit of a hack here ... maybe solve in tokenizer?
|
||||
// compare this for what happens when doing 'foo = bar' in old parser: Is the whitespace kept for
|
||||
// value to not trigger the ref lookup to often if doing 'foo = <div...' ?
|
||||
// @todo: same situation in RootNode!
|
||||
$childValue = '< ' . $child->getReferenceSourceStream();
|
||||
}
|
||||
if ($childValue !== null) {
|
||||
$result[$child->getName()] = $childValue;
|
||||
}
|
||||
$grandChildren = $child->toArray();
|
||||
if ($grandChildren !== null) {
|
||||
$result[$childName . '.'] = $grandChildren;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function flatten(string $prefix = ''): array
|
||||
{
|
||||
$flatArray = [];
|
||||
$prefixedQuotedNodeName = $prefix . addcslashes($this->getName(), '.');
|
||||
if (!$this->isValueNull()) {
|
||||
$flatArray[$prefixedQuotedNodeName] = $this->getValue();
|
||||
}
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$flatArray = array_merge($flatArray, $child->flatten($prefixedQuotedNodeName . '.'));
|
||||
}
|
||||
return $flatArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Generic node. Implements common methods of NodeInterface used
|
||||
* in all Node classes.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractNode implements NodeInterface
|
||||
{
|
||||
private ?string $identifier = null;
|
||||
protected string $name;
|
||||
private ?string $value = null;
|
||||
private ?string $previousValue = null;
|
||||
|
||||
/**
|
||||
* @var array<string, ChildNodeInterface>
|
||||
*/
|
||||
protected array $children = [];
|
||||
private ?TokenStreamInterface $originalValueTokenStream = null;
|
||||
private array $comments = [];
|
||||
|
||||
/**
|
||||
* When storing to cache, we only store FE relevant properties and skip
|
||||
* various BE related properties which then default to class defaults when
|
||||
* unserialized. This is done to create smaller php cache files.
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return $this->serialize();
|
||||
}
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result = [
|
||||
'name' => $this->name,
|
||||
'children' => $this->children,
|
||||
];
|
||||
if ($this->value !== null) {
|
||||
$result['value'] = $this->value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setIdentifier(string $identifier): void
|
||||
{
|
||||
$this->identifier = hash('xxh3', $identifier);
|
||||
$childCounter = 0;
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$child->setIdentifier($this->identifier . $childCounter);
|
||||
$childCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This forces $this->name NOT to be readonly.
|
||||
* Used with '<' operator on tree root to copy:
|
||||
* foo = value
|
||||
* bar < foo
|
||||
* The 'foo' object node is copied, but added to AST as name 'bar'
|
||||
*/
|
||||
public function updateName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
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.',
|
||||
1674620169
|
||||
);
|
||||
}
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function addChild(ChildNodeInterface $node): void
|
||||
{
|
||||
$this->children[$node->getName()] = $node;
|
||||
}
|
||||
|
||||
public function getChildByName(string $name): ?ChildNodeInterface
|
||||
{
|
||||
return $this->children[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this does *not* choke if that child does not exist, so we can "blindly" remove without error.
|
||||
*/
|
||||
public function removeChildByName(string $name): void
|
||||
{
|
||||
unset($this->children[$name]);
|
||||
}
|
||||
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return !empty($this->children);
|
||||
}
|
||||
|
||||
public function getNextChild(): iterable
|
||||
{
|
||||
foreach ($this->children as $child) {
|
||||
yield $child;
|
||||
}
|
||||
}
|
||||
|
||||
public function sortChildren(): void
|
||||
{
|
||||
ksort($this->children, SORT_FLAG_CASE | SORT_STRING);
|
||||
}
|
||||
|
||||
public function setValue(?string $value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function appendValue(string $value): void
|
||||
{
|
||||
$this->value .= $value;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isValueNull(): bool
|
||||
{
|
||||
return $this->value === null;
|
||||
}
|
||||
|
||||
public function setPreviousValue(?string $value): void
|
||||
{
|
||||
$this->previousValue = $value;
|
||||
}
|
||||
|
||||
public function getPreviousValue(): ?string
|
||||
{
|
||||
return $this->previousValue;
|
||||
}
|
||||
|
||||
public function setOriginalValueTokenStream(?TokenStreamInterface $tokenStream): void
|
||||
{
|
||||
$this->originalValueTokenStream = $tokenStream;
|
||||
}
|
||||
|
||||
public function getOriginalValueTokenStream(): ?TokenStreamInterface
|
||||
{
|
||||
return $this->originalValueTokenStream;
|
||||
}
|
||||
|
||||
public function addComment(TokenStreamInterface $tokenStream): void
|
||||
{
|
||||
$this->comments[] = $tokenStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TokenStreamInterface[]
|
||||
*/
|
||||
public function getComments(): array
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
/**
|
||||
* A generic child node that is not the node root.
|
||||
*/
|
||||
final class ChildNode extends AbstractChildNode {}
|
||||
@@ -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\AST\Node;
|
||||
|
||||
/**
|
||||
* The created AST consists of a NodeRoot object with nested NodeObject children.
|
||||
* This is the interface implemented by all children nodes (not RootNode).
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface ChildNodeInterface extends NodeInterface {}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* The created AST consists of a NodeRoot object with nested NodeObject children.
|
||||
* This is the main interface to any node type.
|
||||
*
|
||||
* Example TypoScript:
|
||||
* "foo = fooValue"
|
||||
* "foo.bar = barValue"
|
||||
* This creates a RootNode with one ChildNode name "foo" and value "fooValue",
|
||||
* that has a child ChildNode name "bar" and value "barValue".
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface NodeInterface
|
||||
{
|
||||
/**
|
||||
* An identifier for this node. Typically, a hash of some kind. This identifier
|
||||
* is unique within the tree, by being created from the parent identifier plus
|
||||
* the name. This identifier is used in the backend, when referencing single nodes.
|
||||
* Calculating identifiers is initiated by calling setIdentifier() on RootNode, which
|
||||
* will recurse the tree. Call this on the final tree, after AST calculation finished,
|
||||
* so AST building itself does not need to fiddle with identifier updates when for
|
||||
* instance tree parts are cloned using '<' operator.
|
||||
* Note this value is skipped when persisting to caches since it's a Backend related
|
||||
* thing that does not use cached context: When retrieving nodes 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;
|
||||
|
||||
/**
|
||||
* Helper methods for node name.
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
public function updateName(string $name): void;
|
||||
|
||||
/**
|
||||
* Helper methods to operate on children.
|
||||
*/
|
||||
public function addChild(ChildNodeInterface $node): void;
|
||||
public function getChildByName(string $name): ?ChildNodeInterface;
|
||||
public function removeChildByName(string $name): void;
|
||||
public function hasChildren(): bool;
|
||||
/**
|
||||
* @return iterable<ChildNodeInterface>
|
||||
*/
|
||||
public function getNextChild(): iterable;
|
||||
public function sortChildren(): void;
|
||||
|
||||
/**
|
||||
* Helper methods for value.
|
||||
*/
|
||||
public function setValue(?string $value): void;
|
||||
public function appendValue(string $value): void;
|
||||
public function getValue(): ?string;
|
||||
public function isValueNull(): bool;
|
||||
|
||||
/**
|
||||
* Previous value is only set by comment aware ast builder. It is used in
|
||||
* constant editor to see if a value has been changed.
|
||||
*/
|
||||
public function setPreviousValue(?string $value): void;
|
||||
public function getPreviousValue(): ?string;
|
||||
|
||||
/**
|
||||
* Helper method for backend object browser to retrieve the original
|
||||
* stream when a constant substitution happened, only set by CommentAwareAstBuilder.
|
||||
*/
|
||||
public function setOriginalValueTokenStream(?TokenStreamInterface $tokenStream): void;
|
||||
public function getOriginalValueTokenStream(): ?TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Helper methods to attach TypoScript tokens to a node.
|
||||
* This is used in ext:tstemplate "Constant Editor" and "Object Browser" and handled
|
||||
* by CommentAwareAstBuilder.
|
||||
*/
|
||||
public function addComment(TokenStreamInterface $tokenStream): void;
|
||||
/**
|
||||
* @return TokenStreamInterface[]
|
||||
*/
|
||||
public function getComments(): array;
|
||||
|
||||
/**
|
||||
* b/w compat method to turn AST into an array.
|
||||
* Note we're NOT using magic __toArray() here to avoid calling array-cast of AST by
|
||||
* accident: toArray() should be called explicitly if needed, which makes it much easier
|
||||
* to drop this b/w compat method when we later want to drop that layer.
|
||||
*
|
||||
* Note RootNode *always* returns an array, while ObjectNode's may return null.
|
||||
*/
|
||||
public function toArray(): ?array;
|
||||
|
||||
/**
|
||||
* Flatten the tree. A RootNode with a ChildNode "foo" and value "fooValue", with this
|
||||
* ChildNode again having a ChildNode "bar" and value "barValue" becomes:
|
||||
* [
|
||||
* 'foo' => 'fooValue',
|
||||
* 'foo.bar' => 'barValue',
|
||||
* ]
|
||||
*
|
||||
* Flattening a TypoScript tree is especially used for constants to quickly look
|
||||
* up constants when parsing setup node value streams that use T_CONSTANT tokens.
|
||||
*/
|
||||
public function flatten(string $prefix = ''): array;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A node object created for LineIdentifierReference lines which use the T_OPERATOR_REFERENCE
|
||||
* operator and have a TokenStreamIdentifier stream for "the right side" of the expression.
|
||||
*
|
||||
* The reference operator is nasty, since it's no "true" reference / pointer:
|
||||
* foo.bar = barValue1
|
||||
* baz =< foo
|
||||
* baz.bar = barValue2
|
||||
* This ends up with "barValue1" for "foo.bar", and "barValue2" for "baz.bar". "barValue1"
|
||||
* for "foo.bar" is kept!
|
||||
*
|
||||
* Note the reference operator *only* works for TS "setup" code, not for "constants", and it
|
||||
* is only resolved in these cases. See ContentObjectRenderer->cObjGetSingle() for details.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class ReferenceChildNode extends AbstractChildNode
|
||||
{
|
||||
private ?IdentifierTokenStream $referenceSourceStream;
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result = parent::serialize();
|
||||
if ($this->referenceSourceStream !== null) {
|
||||
$result['referenceSourceStream'] = $this->referenceSourceStream;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setReferenceSourceStream(?IdentifierTokenStream $referenceSourceStream): void
|
||||
{
|
||||
$this->referenceSourceStream = $referenceSourceStream;
|
||||
}
|
||||
|
||||
public function getReferenceSourceStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->referenceSourceStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\AST\Node;
|
||||
|
||||
/**
|
||||
* AST entry node.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class RootNode extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Attempting to clone the RootNode indicates a bug in AstBuilder.
|
||||
* It should never happen.
|
||||
*/
|
||||
public function __clone(): void
|
||||
{
|
||||
throw new \LogicException('Can not clone RootNode', 1655988945);
|
||||
}
|
||||
|
||||
/**
|
||||
* RootNode has no properties to cache, just children.
|
||||
*/
|
||||
protected function serialize(): array
|
||||
{
|
||||
return [
|
||||
'children' => $this->children,
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function updateName(string $name): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no name. Don\'t call updateName().', 1653743453);
|
||||
}
|
||||
|
||||
public function setValue(?string $value): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no value. Don\'t call setValue().', 1653743454);
|
||||
}
|
||||
|
||||
public function appendValue(string $value): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no value. Don\'t call appendValue().', 1653743455);
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isValueNull(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$childName = $child->getName();
|
||||
if ($child instanceof ReferenceChildNode) {
|
||||
// Hack for b/w compat parsing of `=<` operator. See ContentObjectRenderer cObjGetSingle() and mergeTSRef()
|
||||
$childValue = '< ' . $child->getReferenceSourceStream();
|
||||
} else {
|
||||
$childValue = $child->getValue();
|
||||
}
|
||||
if ($childValue !== null) {
|
||||
$result[$childName] = $childValue;
|
||||
}
|
||||
$grandChildren = $child->toArray();
|
||||
if ($grandChildren !== null) {
|
||||
$result[$childName . '.'] = $grandChildren;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function flatten(string $prefix = ''): array
|
||||
{
|
||||
$flatArray = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$flatArray = array_merge($flatArray, $child->flatten(''));
|
||||
}
|
||||
return $flatArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\AST\Traverser;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstVisitorInterface;
|
||||
|
||||
/**
|
||||
* Traverse the entire AST.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class AstTraverser
|
||||
{
|
||||
/**
|
||||
* @param AstVisitorInterface[] $visitors
|
||||
*/
|
||||
public function traverse(RootNode $rootNode, array $visitors): void
|
||||
{
|
||||
foreach ($visitors as $visitor) {
|
||||
if (!$visitor instanceof AstVisitorInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Visitors must implement AstTreeVisitorInterface',
|
||||
1689244842
|
||||
);
|
||||
}
|
||||
}
|
||||
$currentObjectPath = new CurrentObjectPath();
|
||||
$this->traverseRecursive($visitors, $rootNode, $rootNode, $currentObjectPath, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AstVisitorInterface[] $visitors
|
||||
*/
|
||||
private function traverseRecursive(array $visitors, RootNode $nodeRoot, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
$currentObjectPath->append($node);
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitBeforeChildren($nodeRoot, $node, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
foreach ($node->getNextChild() as $child) {
|
||||
$this->traverseRecursive($visitors, $nodeRoot, $child, $currentObjectPath, $currentDepth + 1);
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visit($nodeRoot, $child, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
}
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitAfterChildren($nodeRoot, $node, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
$currentObjectPath->removeLast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
<?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\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Gather comments suitable for constant editor.
|
||||
*
|
||||
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class AstConstantCommentVisitor implements AstVisitorInterface
|
||||
{
|
||||
private array $categories = [
|
||||
'basic' => [
|
||||
'label' => 'Basic',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'menu' => [
|
||||
'label' => 'Menu',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'content' => [
|
||||
'label' => 'Content',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'page' => [
|
||||
'label' => 'Page',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'advanced' => [
|
||||
'label' => 'Advanced',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'all' => [
|
||||
'label' => 'All',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
private array $subCategories = [
|
||||
'enable' => [
|
||||
'label' => 'Enable features',
|
||||
'sorting' => 'a',
|
||||
],
|
||||
'dims' => [
|
||||
'label' => 'Dimensions, widths, heights, pixels',
|
||||
'sorting' => 'b',
|
||||
],
|
||||
'file' => [
|
||||
'label' => 'Files',
|
||||
'sorting' => 'c',
|
||||
],
|
||||
'typo' => [
|
||||
'label' => 'Typography',
|
||||
'sorting' => 'd',
|
||||
],
|
||||
'color' => [
|
||||
'label' => 'Colors',
|
||||
'sorting' => 'e',
|
||||
],
|
||||
'links' => [
|
||||
'label' => 'Links and targets',
|
||||
'sorting' => 'f',
|
||||
],
|
||||
'language' => [
|
||||
'label' => 'Language specific constants',
|
||||
'sorting' => 'g',
|
||||
],
|
||||
'cheader' => [
|
||||
'label' => 'Content: \'Header\'',
|
||||
'sorting' => 'ma',
|
||||
],
|
||||
'cheader_g' => [
|
||||
'label' => 'Content: \'Header\', Graphical',
|
||||
'sorting' => 'ma',
|
||||
],
|
||||
'ctext' => [
|
||||
'label' => 'Content: \'Text\'',
|
||||
'sorting' => 'mb',
|
||||
],
|
||||
'cimage' => [
|
||||
'label' => 'Content: \'Image\'',
|
||||
'sorting' => 'md',
|
||||
],
|
||||
'ctextmedia' => [
|
||||
'label' => 'Content: \'Textmedia\'',
|
||||
'sorting' => 'ml',
|
||||
],
|
||||
'cbullets' => [
|
||||
'label' => 'Content: \'Bullet list\'',
|
||||
'sorting' => 'me',
|
||||
],
|
||||
'ctable' => [
|
||||
'label' => 'Content: \'Table\'',
|
||||
'sorting' => 'mf',
|
||||
],
|
||||
'cuploads' => [
|
||||
'label' => 'Content: \'Filelinks\'',
|
||||
'sorting' => 'mg',
|
||||
],
|
||||
'cmultimedia' => [
|
||||
'label' => 'Content: \'Multimedia\'',
|
||||
'sorting' => 'mh',
|
||||
],
|
||||
'cmedia' => [
|
||||
'label' => 'Content: \'Media\'',
|
||||
'sorting' => 'mr',
|
||||
],
|
||||
'cmailform' => [
|
||||
'label' => 'Content: \'Form\'',
|
||||
'sorting' => 'mi',
|
||||
],
|
||||
'csearch' => [
|
||||
'label' => 'Content: \'Search\'',
|
||||
'sorting' => 'mj',
|
||||
],
|
||||
'clogin' => [
|
||||
'label' => 'Content: \'Login\'',
|
||||
'sorting' => 'mk',
|
||||
],
|
||||
'cmenu' => [
|
||||
'label' => 'Content: \'Menu/Sitemap\'',
|
||||
'sorting' => 'mm',
|
||||
],
|
||||
'cshortcut' => [
|
||||
'label' => 'Content: \'Insert records\'',
|
||||
'sorting' => 'mn',
|
||||
],
|
||||
'clist' => [
|
||||
'label' => 'Content: \'List of records\'',
|
||||
'sorting' => 'mo',
|
||||
],
|
||||
'chtml' => [
|
||||
'label' => 'Content: \'HTML\'',
|
||||
'sorting' => 'mq',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper hack variable to have a unique sub category order if no sub category is given.
|
||||
*/
|
||||
private int $subCategoryCounter = 0;
|
||||
|
||||
private array $currentTemplateFlatConstants = [];
|
||||
|
||||
private array $constants = [];
|
||||
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
if ($node instanceof RootNode) {
|
||||
$rootNodeComments = $rootNode->getComments();
|
||||
foreach ($rootNodeComments as $comment) {
|
||||
$this->subCategoryCounter++;
|
||||
// Additional custom categories are attached as comments to root node
|
||||
$this->parseCustomCategoryAndSubCategories($comment);
|
||||
}
|
||||
} else {
|
||||
$nodeComments = $node->getComments();
|
||||
foreach ($nodeComments as $comment) {
|
||||
$this->subCategoryCounter++;
|
||||
$parsedCommentArray = $this->parseNodeComment($comment, $node->getName(), $node->getValue());
|
||||
if (empty($parsedCommentArray)) {
|
||||
continue;
|
||||
}
|
||||
$currentDottedPath = $currentObjectPath->getPathAsString();
|
||||
if (array_key_exists($currentDottedPath, $this->constants)) {
|
||||
// A constant definition can be defined only once. Stop when trying to override.
|
||||
continue;
|
||||
}
|
||||
$parsedCommentArray['name'] = $currentDottedPath;
|
||||
$parsedCommentArray['idName'] = str_replace('.', '-', $currentDottedPath);
|
||||
$parsedCommentArray['value'] = $node->getValue();
|
||||
$parsedCommentArray['default_value'] = $node->getPreviousValue() ?? $node->getValue() ?? '[Empty]';
|
||||
$parsedCommentArray['isInCurrentTemplate'] = false;
|
||||
if (array_key_exists($currentDottedPath, $this->currentTemplateFlatConstants)) {
|
||||
$parsedCommentArray['isInCurrentTemplate'] = true;
|
||||
}
|
||||
$this->constants[$currentDottedPath] = $parsedCommentArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setCurrentTemplateFlatConstants(array $currentTemplateFlatConstants)
|
||||
{
|
||||
$this->currentTemplateFlatConstants = $currentTemplateFlatConstants;
|
||||
}
|
||||
|
||||
public function getConstants(): array
|
||||
{
|
||||
return $this->constants;
|
||||
}
|
||||
|
||||
public function getCategories(): array
|
||||
{
|
||||
return $this->categories;
|
||||
}
|
||||
|
||||
private function parseNodeComment(TokenStreamInterface $commentTokenStream, string $nodeName, ?string $currentValue = null): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$parsedCommentArray = [];
|
||||
$commentTokenStream->reset();
|
||||
$trimmedTokenStream = new TokenStream();
|
||||
while ($token = $commentTokenStream->getNext()) {
|
||||
if ($token->getType() !== TokenType::T_BLANK) {
|
||||
$trimmedTokenStream->append($token);
|
||||
}
|
||||
}
|
||||
$firstTokenType = $trimmedTokenStream->peekNext()->getType();
|
||||
if ($firstTokenType !== TokenType::T_COMMENT_ONELINE_HASH && $firstTokenType !== TokenType::T_COMMENT_ONELINE_DOUBLESLASH) {
|
||||
// Ignore multiline comments, only '#' and '//' allowed here
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
$commentString = trim((string)$trimmedTokenStream);
|
||||
// Get rid of '#' and '//'
|
||||
$commentString = trim(preg_replace('/^[#\\/]*/', '', $commentString));
|
||||
if (empty($commentString)) {
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
// "# cat=my custom: custom1/customsub1; type=string; label=custom1 customsub1 test1"
|
||||
$commentParts = explode(';', $commentString);
|
||||
foreach ($commentParts as $commentPart) {
|
||||
if (!str_contains($commentPart, '=')) {
|
||||
// Whatever it is, we ignore it.
|
||||
continue;
|
||||
}
|
||||
$partArray = explode('=', $commentPart, 2);
|
||||
$partKey = strtolower(trim($partArray[0]));
|
||||
$partValue = trim($partArray[1] ?? '');
|
||||
if (empty($partKey) || empty($partValue)) {
|
||||
continue;
|
||||
}
|
||||
if ($partKey === 'type') {
|
||||
if (str_starts_with($partValue, 'int+')) {
|
||||
$parsedCommentArray['type'] = 'int+';
|
||||
$parsedCommentArray['typeIntPlusMin'] = 0;
|
||||
preg_match('/int\+\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1]) && str_contains($typeMatches[1], '-')) {
|
||||
$intPlusExplodedRange = GeneralUtility::intExplode('-', $typeMatches[1]);
|
||||
$parsedCommentArray['typeIntPlusMin'] = $intPlusExplodedRange[0];
|
||||
$parsedCommentArray['typeHint'] = 'Greater than ' . $intPlusExplodedRange[0];
|
||||
if ($intPlusExplodedRange[1] > 0) {
|
||||
$parsedCommentArray['typeIntPlusMax'] = $intPlusExplodedRange[1];
|
||||
$parsedCommentArray['typeHint'] = 'Range ' . $intPlusExplodedRange[0] . ' - ' . $intPlusExplodedRange[1];
|
||||
}
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'int')) {
|
||||
preg_match('/int\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$parsedCommentArray['type'] = 'int';
|
||||
if (!empty($typeMatches[1]) && str_contains($typeMatches[1], '-')) {
|
||||
$rangeArray = mb_str_split($typeMatches[1]);
|
||||
$negativeStart = false;
|
||||
$negativeStop = false;
|
||||
$gotSeparatorDash = false;
|
||||
$start = null;
|
||||
$stop = null;
|
||||
foreach ($rangeArray as $index => $char) {
|
||||
if ($index === 0 && $char === '-') {
|
||||
$negativeStart = true;
|
||||
} elseif ($char === '-' && !$gotSeparatorDash) {
|
||||
$gotSeparatorDash = true;
|
||||
} elseif (!$gotSeparatorDash) {
|
||||
$start .= $char;
|
||||
} elseif ($stop === null && $char === '-') {
|
||||
$negativeStop = true;
|
||||
} else {
|
||||
$stop .= $char;
|
||||
}
|
||||
}
|
||||
if ($start !== null) {
|
||||
if ($negativeStart) {
|
||||
$start = (int)$start * -1;
|
||||
}
|
||||
$parsedCommentArray['typeIntMin'] = (string)$start;
|
||||
$parsedCommentArray['typeHint'] = 'Greater than ' . $start;
|
||||
}
|
||||
if ($stop !== null) {
|
||||
if ($negativeStop) {
|
||||
$stop = (int)$stop * -1;
|
||||
}
|
||||
$parsedCommentArray['typeIntMax'] = (string)$stop;
|
||||
$parsedCommentArray['typeHint'] = 'Range ' . $start . ' - ' . $stop;
|
||||
}
|
||||
}
|
||||
} elseif ($partValue === 'wrap') {
|
||||
$parsedCommentArray['type'] = 'wrap';
|
||||
$splitValue = explode('|', $currentValue ?? '');
|
||||
$parsedCommentArray['wrapStart'] = $splitValue[0];
|
||||
$parsedCommentArray['wrapEnd'] = $splitValue[1] ?? '';
|
||||
} elseif (str_starts_with($partValue, 'offset')) {
|
||||
$parsedCommentArray['type'] = 'offset';
|
||||
preg_match('/offset\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$labelArray = explode(',', $typeMatches[1] ?? '');
|
||||
$valueArray = explode(',', $currentValue ?? '');
|
||||
$parsedCommentArray['labelValueArray'] = [
|
||||
[
|
||||
'label' => (!empty($labelArray[0])) ? $labelArray[0] : 'x',
|
||||
'value' => (!empty($valueArray[0])) ? $valueArray[0] : '',
|
||||
],
|
||||
[
|
||||
'label' => $labelArray[1] ?? 'y',
|
||||
'value' => $valueArray[1] ?? '',
|
||||
],
|
||||
];
|
||||
for ($i = 2; $i <= 5; $i++) {
|
||||
if (!($labelArray[$i] ?? false)) {
|
||||
break;
|
||||
}
|
||||
$parsedCommentArray['labelValueArray'][] = [
|
||||
'label' => $labelArray[$i],
|
||||
'value' => $valueArray[$i] ?? '',
|
||||
];
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'options')) {
|
||||
preg_match('/options\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['type'] = 'options';
|
||||
$labelValueStringArray = GeneralUtility::trimExplode(',', $typeMatches[1], true);
|
||||
foreach ($labelValueStringArray as $labelValueString) {
|
||||
$labelValueArray = explode('=', $labelValueString, 2);
|
||||
$label = $labelValueArray[0];
|
||||
$value = $labelValueArray[1] ?? $labelValueArray[0];
|
||||
$selected = false;
|
||||
if ($value === $currentValue) {
|
||||
$selected = true;
|
||||
}
|
||||
$parsedCommentArray['labelValueArray'][] = [
|
||||
'label' => $languageService->sL($label),
|
||||
'value' => $value,
|
||||
'selected' => $selected,
|
||||
];
|
||||
}
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'boolean')) {
|
||||
$parsedCommentArray['type'] = 'boolean';
|
||||
preg_match('/boolean\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$parsedCommentArray['trueValue'] = '1';
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['trueValue'] = $typeMatches[1];
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'user')) {
|
||||
preg_match('/user\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['type'] = 'user';
|
||||
$userFunction = $typeMatches[1];
|
||||
$userFunctionParams = [
|
||||
'fieldName' => $nodeName,
|
||||
'fieldValue' => $currentValue,
|
||||
];
|
||||
$parsedCommentArray['html'] = (string)GeneralUtility::callUserFunction(
|
||||
$userFunction,
|
||||
$userFunctionParams
|
||||
);
|
||||
}
|
||||
} elseif ($partValue === 'comment') {
|
||||
$parsedCommentArray['type'] = 'comment';
|
||||
} elseif ($partValue === 'color') {
|
||||
$parsedCommentArray['type'] = 'color';
|
||||
} else {
|
||||
$parsedCommentArray['type'] = 'string';
|
||||
}
|
||||
} elseif ($partKey === 'cat') {
|
||||
$categorySplitArray = explode('/', strtolower($partValue));
|
||||
$mainCategory = strtolower(trim($categorySplitArray[0]));
|
||||
if (empty($mainCategory)) {
|
||||
return [];
|
||||
}
|
||||
if (isset($this->categories[$mainCategory])) {
|
||||
$this->categories[$mainCategory]['usageCount']++;
|
||||
} else {
|
||||
$this->categories[$mainCategory] = [
|
||||
'usageCount' => 1,
|
||||
'label' => $mainCategory,
|
||||
];
|
||||
}
|
||||
$parsedCommentArray['cat'] = $mainCategory;
|
||||
$subCategory = trim($categorySplitArray[1] ?? '');
|
||||
$subCategoryOrder = trim($categorySplitArray[2] ?? '');
|
||||
if ($subCategory && array_key_exists($subCategory, $this->subCategories)) {
|
||||
$parsedCommentArray['subcat_name'] = $subCategory;
|
||||
$parsedCommentArray['subcat_label'] = $languageService->sL($this->subCategories[$subCategory]['label']);
|
||||
$sortIdentifier = empty($subCategoryOrder) ? $this->subCategoryCounter : $subCategoryOrder;
|
||||
$parsedCommentArray['subcat_sorting_first'] = $this->subCategories[$subCategory]['sorting'];
|
||||
$parsedCommentArray['subcat_sorting_second'] = $sortIdentifier . 'z';
|
||||
} elseif ($subCategoryOrder !== '') {
|
||||
// "0" is a valid key for an assignment like "# cat=foo//0; type=boolean; label=some config"
|
||||
$parsedCommentArray['subcat_name'] = 'other';
|
||||
$parsedCommentArray['subcat_label'] = 'Other';
|
||||
$parsedCommentArray['subcat_sorting_first'] = 'o';
|
||||
$parsedCommentArray['subcat_sorting_second'] = $subCategoryOrder . 'z';
|
||||
} else {
|
||||
$parsedCommentArray['subcat_name'] = 'other';
|
||||
$parsedCommentArray['subcat_label'] = 'Other';
|
||||
$parsedCommentArray['subcat_sorting_first'] = 'o';
|
||||
$parsedCommentArray['subcat_sorting_second'] = $this->subCategoryCounter . 'z';
|
||||
}
|
||||
} elseif ($partKey === 'label') {
|
||||
$fullLabel = $languageService->sL($partValue);
|
||||
$splitLabelArray = explode(':', $fullLabel, 2);
|
||||
$parsedCommentArray['label'] = $splitLabelArray[0];
|
||||
$parsedCommentArray['description'] = $splitLabelArray[1] ?? '';
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('cat', $parsedCommentArray)) {
|
||||
// At least 'category' must be there, everything else is optional.
|
||||
return [];
|
||||
}
|
||||
$parsedCommentArray['type'] ??= 'string';
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RootNode comments for additional custom categories and sub categories
|
||||
* and register them in $this properties.
|
||||
*/
|
||||
private function parseCustomCategoryAndSubCategories(TokenStreamInterface $commentTokenStream): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$firstTokenType = $commentTokenStream->peekNext()->getType();
|
||||
if ($firstTokenType !== TokenType::T_COMMENT_ONELINE_HASH && $firstTokenType !== TokenType::T_COMMENT_ONELINE_DOUBLESLASH) {
|
||||
// Ignore multiline comments, only '#' and '//' allowed here
|
||||
return;
|
||||
}
|
||||
$commentString = trim((string)$commentTokenStream);
|
||||
// Get rid of '#' and '//'
|
||||
$commentString = trim(preg_replace('/^[#\\/]*/', '', $commentString));
|
||||
if (empty($commentString)) {
|
||||
return;
|
||||
}
|
||||
// "# customcategory=myCustomCategoryKey=My custom category label"
|
||||
if (str_contains($commentString, '=') && str_starts_with(strtolower($commentString), 'customcategory')) {
|
||||
$customCategoryArray = explode('=', $commentString, 3);
|
||||
if (strtolower(trim($customCategoryArray[0])) !== 'customcategory'
|
||||
|| empty(trim($customCategoryArray[1]))
|
||||
|| empty(trim($customCategoryArray[2]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$categoryKey = strtolower($customCategoryArray[1]);
|
||||
$categoryLabel = $customCategoryArray[2];
|
||||
if (!isset($this->categories[$categoryKey])) {
|
||||
$this->categories[$categoryKey] = [
|
||||
'usageCount' => 0,
|
||||
'label' => $languageService->sL($categoryLabel),
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
// "customsubcategory=120=My custom sub category label"
|
||||
if (str_contains($commentString, '=') && str_starts_with(strtolower($commentString), 'customsubcategory')) {
|
||||
$customSubCategoryArray = explode('=', $commentString, 3);
|
||||
if (strtolower(trim($customSubCategoryArray[0])) !== 'customsubcategory'
|
||||
|| empty(trim($customSubCategoryArray[1]))
|
||||
|| empty(trim($customSubCategoryArray[2]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$subCategoryKey = strtolower($customSubCategoryArray[1]);
|
||||
$subCategoryLabel = $customSubCategoryArray[2];
|
||||
if (!isset($this->subCategories[$subCategoryKey])) {
|
||||
$this->subCategories[$subCategoryKey] = [
|
||||
'label' => $languageService->sL($subCategoryLabel),
|
||||
'sorting' => $this->subCategoryCounter,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Find a single node in tree identified by node identifier.
|
||||
*
|
||||
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class AstNodeFinderVisitor implements AstVisitorInterface
|
||||
{
|
||||
private string $nodeIdentifier;
|
||||
private ?NodeInterface $foundNode = null;
|
||||
private ?CurrentObjectPath $foundNodeCurrentObjectPath = null;
|
||||
|
||||
public function setNodeIdentifier(string $nodeIdentifier)
|
||||
{
|
||||
$this->nodeIdentifier = $nodeIdentifier;
|
||||
}
|
||||
|
||||
public function getFoundNode(): ?NodeInterface
|
||||
{
|
||||
return $this->foundNode;
|
||||
}
|
||||
|
||||
public function getFoundNodeCurrentObjectPath(): ?CurrentObjectPath
|
||||
{
|
||||
return $this->foundNodeCurrentObjectPath;
|
||||
}
|
||||
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
if ($node->getIdentifier() === $this->nodeIdentifier) {
|
||||
$this->foundNode = $node;
|
||||
$this->foundNodeCurrentObjectPath = clone $currentObjectPath;
|
||||
}
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
}
|
||||
@@ -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\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Sort all children alphabetically. Used in backend Object Browser.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class AstSortChildrenVisitor implements AstVisitorInterface
|
||||
{
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
$node->sortChildren();
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
}
|
||||
@@ -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\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* An interface implemented by all visitors of AstTraverser.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface AstVisitorInterface
|
||||
{
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
|
||||
|
||||
/**
|
||||
* This class contains the TypoScript set up by the PrepareTypoScriptFrontendRendering
|
||||
* Frontend middleware. It can be accessed in content objects:
|
||||
*
|
||||
* $frontendTypoScript = $request->getAttribute('frontend.typoscript');
|
||||
*/
|
||||
final class FrontendTypoScript
|
||||
{
|
||||
private ?RootInclude $setupIncludeTree = null;
|
||||
private ?RootNode $setupTree = null;
|
||||
private ?array $setupArray = null;
|
||||
private ?RootNode $configTree = null;
|
||||
private ?array $configArray = null;
|
||||
private ?RootNode $pageTree = null;
|
||||
private ?array $pageArray = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly RootNode $settingsTree,
|
||||
private readonly array $settingsConditionList,
|
||||
private readonly array $flatSettings,
|
||||
private readonly array $setupConditionList,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The settings ("constants") AST.
|
||||
*
|
||||
* @internal Internal for now until the AST API stabilized.
|
||||
*/
|
||||
public function getSettingsTree(): RootNode
|
||||
{
|
||||
return $this->settingsTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of settings conditions with verdicts. Used internally for
|
||||
* page cache identifier calculation.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSettingsConditionList(): array
|
||||
{
|
||||
return $this->settingsConditionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is *always* set up by the middleware / factory: Current settings ("constants")
|
||||
* are needed for page cache identifier calculation.
|
||||
* This is a "flattened" array of all settings, as example, consider these settings TypoScript:
|
||||
*
|
||||
* ```
|
||||
* mySettings {
|
||||
* foo = fooValue
|
||||
* bar = barValue
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This will result in this array:
|
||||
*
|
||||
* ```
|
||||
* $flatSettings = [
|
||||
* 'mySettings.foo' => 'fooValue',
|
||||
* 'mySettings.bar' => 'barValue',
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
public function getFlatSettings(): array
|
||||
{
|
||||
return $this->flatSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of setup conditions with verdicts. Used internally for
|
||||
* page cache identifier calculation.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSetupConditionList(): array
|
||||
{
|
||||
return $this->setupConditionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setSetupIncludeTree(RootInclude $setupIncludeTree): void
|
||||
{
|
||||
$this->setupIncludeTree = $setupIncludeTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tree of all TypoScript setup includes. Used internally within
|
||||
* FrontendTypoScriptFactory to suppress calculating the include tree
|
||||
* twice.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getSetupIncludeTree(): ?RootInclude
|
||||
{
|
||||
return $this->setupIncludeTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setSetupTree(RootNode $setupTree): void
|
||||
{
|
||||
$this->setupTree = $setupTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a page is retrieved from cache and does not contain COA_INT or USER_INT objects,
|
||||
* Frontend TypoScript setup is not calculated, AST and the array representation aren't set.
|
||||
* Calling getSetupTree() or getSetupArray() will then throw an exception.
|
||||
*
|
||||
* To avoid the exception, consumers can call hasSetup() beforehand.
|
||||
*
|
||||
* Note casual content objects do not need to do this, since setup TypoScript is always
|
||||
* set up when content objects need to be calculated.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function hasSetup(): bool
|
||||
{
|
||||
return $this->setupTree !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Internal for now until the AST API stabilized.
|
||||
*/
|
||||
public function getSetupTree(): RootNode
|
||||
{
|
||||
if ($this->setupTree === null) {
|
||||
throw new \RuntimeException(
|
||||
'Setup tree has not been initialized. This happens in cached Frontend scope where full TypoScript'
|
||||
. ' is not needed by the system.',
|
||||
1666513644
|
||||
);
|
||||
}
|
||||
return $this->setupTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setSetupArray(array $setupArray): void
|
||||
{
|
||||
$this->setupArray = $setupArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* The full Frontend TypoScript array.
|
||||
*
|
||||
* This is always set up as soon as the Frontend rendering needs to actually render something and
|
||||
* can not get the *full* content from page cache. This is the case when a page cache entry does
|
||||
* not exist, or when the page contains COA_INT or USER_INT objects.
|
||||
*/
|
||||
public function getSetupArray(): array
|
||||
{
|
||||
if ($this->setupArray === null) {
|
||||
throw new \RuntimeException(
|
||||
'Setup array has not been initialized. This happens in cached Frontend scope where full TypoScript'
|
||||
. ' is not needed by the system.',
|
||||
1666513645
|
||||
);
|
||||
}
|
||||
return $this->setupArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setConfigTree(RootNode $setupConfig): void
|
||||
{
|
||||
$this->configTree = $setupConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged TypoScript 'config.'.
|
||||
*
|
||||
* This is the result of the "global" TypoScript 'config' section, merged with
|
||||
* the 'config' section of the determined PAGE object which can override
|
||||
* "global" 'config' per type / typeNum.
|
||||
*
|
||||
* This is *always* needed within casual Frontend rendering by FrontendTypoScriptFactory and
|
||||
* has a dedicated cache layer to be quick to retrieve. It is needed even in fully cached pages
|
||||
* context to for instance know if debug headers should be added ("config.debug=1") to a response.
|
||||
*
|
||||
* @internal Internal for now until the AST API stabilized.
|
||||
*/
|
||||
public function getConfigTree(): RootNode
|
||||
{
|
||||
if ($this->configTree === null) {
|
||||
throw new \RuntimeException(
|
||||
'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?',
|
||||
1710666154
|
||||
);
|
||||
}
|
||||
return $this->configTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setConfigArray(array $configArray): void
|
||||
{
|
||||
$this->configArray = $configArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array representation of getConfigTree().
|
||||
*/
|
||||
public function getConfigArray(): array
|
||||
{
|
||||
if ($this->configArray === null) {
|
||||
throw new \RuntimeException(
|
||||
'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?',
|
||||
1710666123
|
||||
);
|
||||
}
|
||||
return $this->configArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setPageTree(RootNode $pageTree): void
|
||||
{
|
||||
$this->pageTree = $pageTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* The determined PAGE object from main TypoScript 'setup' that depends
|
||||
* on type / typeNum.
|
||||
*
|
||||
* This is used internally by RequestHandler for page generation.
|
||||
* It is *not* set in full cached page scenarios without _INT object.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getPageTree(): RootNode
|
||||
{
|
||||
if ($this->pageTree === null) {
|
||||
throw new \RuntimeException(
|
||||
'PAGE node has not been initialized. This happens in cached Frontend scope where full TypoScript'
|
||||
. ' is not needed by the system, and if a PAGE object for given type could not be determined.'
|
||||
. ' Test with hasPage().',
|
||||
1710399966
|
||||
);
|
||||
}
|
||||
return $this->pageTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function hasPage(): bool
|
||||
{
|
||||
return $this->pageTree !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setPageArray(array $pageArray): void
|
||||
{
|
||||
$this->pageArray = $pageArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array representation of getPageTree().
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getPageArray(): array
|
||||
{
|
||||
if ($this->pageArray === null) {
|
||||
throw new \RuntimeException(
|
||||
'PAGE array has not been initialized. This happens in cached Frontend scope where full TypoScript'
|
||||
. ' is not needed by the system, and if a PAGE object for given type could not be determined.'
|
||||
. ' Test with hasPage().',
|
||||
1710399967
|
||||
);
|
||||
}
|
||||
return $this->pageArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
|
||||
use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent;
|
||||
use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent;
|
||||
|
||||
/**
|
||||
* Create FrontendTypoScript with its details. This is typically used by a Frontend middleware
|
||||
* to calculate the TypoScript needed to satisfy rendering details of the specific Request.
|
||||
*
|
||||
* @internal Methods signatures and detail implementations are still subject to change.
|
||||
*/
|
||||
final readonly class FrontendTypoScriptFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ContainerInterface $container,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private SysTemplateTreeBuilder $treeBuilder,
|
||||
private LossyTokenizer $tokenizer,
|
||||
private IncludeTreeTraverser $includeTreeTraverser,
|
||||
private ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* First step of TypoScript calculations.
|
||||
* This is *always* called, even in FE fully cached pages context since the page
|
||||
* cache entry depends on setup condition verdicts, which depends on settings.
|
||||
*
|
||||
* Returns the FrontendTypoScript object with these parameters set:
|
||||
* * settingsTree: The full settings ("constants") AST
|
||||
* * flatSettings: Flattened list of settings, derived from settings tree
|
||||
* * settingsConditionList: Settings conditions with verdicts of this Request
|
||||
* * setupConditionList: Setup conditions with verdicts of this Request
|
||||
* * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated
|
||||
*/
|
||||
public function createSettingsAndSetupConditions(
|
||||
SiteInterface $site,
|
||||
array $sysTemplateRows,
|
||||
array $expressionMatcherVariables,
|
||||
?PhpFrontend $typoScriptCache,
|
||||
): FrontendTypoScript {
|
||||
$settingsDetails = $this->createSettings(
|
||||
$site,
|
||||
$sysTemplateRows,
|
||||
$expressionMatcherVariables,
|
||||
$typoScriptCache
|
||||
);
|
||||
$setupDetails = $this->createSetupConditionList(
|
||||
$site,
|
||||
$sysTemplateRows,
|
||||
$expressionMatcherVariables,
|
||||
$typoScriptCache,
|
||||
$settingsDetails['flatSettings'],
|
||||
$settingsDetails['settingsConditionList'],
|
||||
);
|
||||
$frontendTypoScript = new FrontendTypoScript(
|
||||
$settingsDetails['settingsTree'],
|
||||
$settingsDetails['settingsConditionList'],
|
||||
$settingsDetails['flatSettings'],
|
||||
$setupDetails['setupConditionList'],
|
||||
);
|
||||
if ($setupDetails['setupIncludeTree']) {
|
||||
$frontendTypoScript->setSetupIncludeTree($setupDetails['setupIncludeTree']);
|
||||
}
|
||||
return $frontendTypoScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate settings (formerly "constants").
|
||||
*
|
||||
* The page cache entry identifier depends on setup TypoScript: A single page with two different
|
||||
* setup TypoScript AST will probably render different results, thus two page-cache entries.
|
||||
* Setup TypoScript can be different when setup conditions match differently.
|
||||
* Setup conditions can use settings "[{$foo} = 42]".
|
||||
*
|
||||
* All FE requests thus need the current list of settings, and settings can have conditions, too.
|
||||
* We thus *always* need the current list of settings, even in fully cached pages context.
|
||||
*
|
||||
* The method calculates settings and uses caches as much as possible:
|
||||
* * settingsTree: The full settings AST
|
||||
* * flatSettings: Flattened list of settings, derived from settings AST
|
||||
* * settingsConditionList: Settings conditions with verdicts of this Request
|
||||
*
|
||||
* @return array{settingsTree: RootNode, flatSettings: array, settingsConditionList: array}
|
||||
*/
|
||||
private function createSettings(
|
||||
SiteInterface $site,
|
||||
array $sysTemplateRows,
|
||||
array $expressionMatcherVariables,
|
||||
?PhpFrontend $typoScriptCache,
|
||||
): array {
|
||||
$cacheCriteria = [
|
||||
'sysTemplateRows' => $sysTemplateRows,
|
||||
];
|
||||
if ($site instanceof Site && $site->isTypoScriptRoot()) {
|
||||
$cacheCriteria['siteIdentifier'] = $site->getIdentifier();
|
||||
}
|
||||
$conditionTreeCacheIdentifier = 'settings-condition-tree-' . hash('xxh3', json_encode($cacheCriteria, JSON_THROW_ON_ERROR));
|
||||
|
||||
if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
|
||||
// Got the (flat) include tree of all settings conditions for this TypoScript combination from cache.
|
||||
// Good. Traverse this list to calculate "current" condition verdicts. Hash this list together with a
|
||||
// hash of the TypoScript sys_templates, and try to retrieve the full settings TypoScript AST from cache.
|
||||
// Note: Working with the derived condition tree that *only* contains conditions, but not the full
|
||||
// include tree is a trick: We only need the condition verdicts to know the AST cache identifier,
|
||||
// and traversing the flat condition tree is quicker than traversing the entire settings include tree,
|
||||
// since it only scales with the number of settings conditions and not with the full amount of TypoScript
|
||||
// settings. The same trick is used for the setup AST cache later.
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
// It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
|
||||
// Conditions list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
|
||||
$this->includeTreeTraverser->traverse($conditionTree, [$conditionMatcherVisitor]);
|
||||
$conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
|
||||
$settings = $typoScriptCache->require(
|
||||
'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR))
|
||||
);
|
||||
if (is_array($settings)) {
|
||||
return [
|
||||
'settingsTree' => $settings['ast'],
|
||||
'flatSettings' => $settings['flatSettings'],
|
||||
'settingsConditionList' => $conditionList,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// We did not get settings from cache, or are not allowed to use cache. Build settings from scratch.
|
||||
// We fetch the full settings include tree (from cache if possible), register the condition
|
||||
// matcher and register the AST builder and traverse include tree to retrieve settings AST and derive
|
||||
// 'flat settings' from it. Both are cached if allowed afterward for the above 'if' to kick in next time.
|
||||
$includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
$visitors = [];
|
||||
$visitors[] = $conditionMatcherVisitor;
|
||||
$astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
|
||||
$visitors[] = $astBuilderVisitor;
|
||||
// We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into
|
||||
// children for not matching conditions, which is important to create the correct AST.
|
||||
$this->includeTreeTraverserConditionVerdictAware->traverse($includeTree, $visitors);
|
||||
$tree = $astBuilderVisitor->getAst();
|
||||
// @internal Dispatch an experimental event allowing listeners to still change the settings AST,
|
||||
// to for instance implement nested constants if really needed. Note this event may change
|
||||
// or vanish later without further notice.
|
||||
$tree = $this->eventDispatcher->dispatch(new ModifyTypoScriptConstantsEvent($tree))->getConstantsAst();
|
||||
$flatSettings = $tree->flatten();
|
||||
|
||||
// Prepare the full list of settings conditions in order to cache this list, avoiding the
|
||||
// settings AST building next time. We need all conditions of the entire include tree, but the
|
||||
// above ConditionVerdictAwareIncludeTreeTraverser did not find nested conditions if an upper
|
||||
// condition did not match. We thus have to traverse include tree a second time with the
|
||||
// IncludeTreeTraverser. This one does traverse into not matching conditions.
|
||||
$visitors = [];
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
$visitors[] = $conditionMatcherVisitor;
|
||||
$conditionTreeAccumulatorVisitor = null;
|
||||
if (!$conditionTree && $typoScriptCache) {
|
||||
// If the settingsConditionTree did not come from cache above and if we are allowed to cache,
|
||||
// register the visitor that creates the settings condition include tree, to cache it.
|
||||
$conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
|
||||
$visitors[] = $conditionTreeAccumulatorVisitor;
|
||||
}
|
||||
$this->includeTreeTraverser->traverse($includeTree, $visitors);
|
||||
$conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
|
||||
|
||||
if ($conditionTreeAccumulatorVisitor) {
|
||||
// Cache the flat condition include tree for next run.
|
||||
$conditionTree = $conditionTreeAccumulatorVisitor->getConditionIncludes();
|
||||
$typoScriptCache?->set(
|
||||
$conditionTreeCacheIdentifier,
|
||||
'return unserialize(\'' . addcslashes(serialize($conditionTree), '\'\\') . '\');'
|
||||
);
|
||||
}
|
||||
$typoScriptCache?->set(
|
||||
// Cache full AST and the derived 'flattened' variant for next run, which will kick in if
|
||||
// the sys_templates and condition verdicts are identical with another Request.
|
||||
'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR)),
|
||||
'return unserialize(\'' . addcslashes(serialize(['ast' => $tree, 'flatSettings' => $flatSettings]), '\'\\') . '\');'
|
||||
);
|
||||
|
||||
return [
|
||||
'settingsTree' => $tree,
|
||||
'flatSettings' => $flatSettings,
|
||||
'settingsConditionList' => $conditionList,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate setup condition verdicts.
|
||||
*
|
||||
* With settings being done, the list of matching setup condition verdicts is calculated,
|
||||
* which depend on settings. Setup conditions with their verdicts are part of the page
|
||||
* cache identifier, they are *always* needed in the FE rendering chain.
|
||||
*
|
||||
* The cached variant uses a similar trick as with the settings calculation above: We
|
||||
* calculate a flat tree of all conditions and cache this, so the traverser only needs
|
||||
* to iterate the conditions to calculate their verdicts, but not the entire include
|
||||
* tree next time.
|
||||
*
|
||||
* The method returns:
|
||||
* * 'setupConditionList': Setup conditions with verdicts of this Request
|
||||
* * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated. Used internally
|
||||
* to suppress a second calculation in createSetupConfigOrFullSetup().
|
||||
*
|
||||
* @return array{setupConditionList: array, setupIncludeTree: RootInclude|null}
|
||||
*/
|
||||
private function createSetupConditionList(
|
||||
SiteInterface $site,
|
||||
array $sysTemplateRows,
|
||||
array $expressionMatcherVariables,
|
||||
?PhpFrontend $typoScriptCache,
|
||||
array $flatSettings,
|
||||
array $settingsConditionList,
|
||||
): array {
|
||||
$conditionTreeCacheIdentifier = 'setup-condition-tree-' . hash(
|
||||
'xxh3',
|
||||
json_encode($sysTemplateRows, JSON_THROW_ON_ERROR)
|
||||
. json_encode($site instanceof Site && $site->isTypoScriptRoot() ? $site->getSets() : '', JSON_THROW_ON_ERROR)
|
||||
. json_encode($settingsConditionList, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
|
||||
// We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse
|
||||
// this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier.
|
||||
// We're done and return. Note 'setupIncludeTree' is *not* returned in this case since it is not needed and
|
||||
// may or may not be needed later, depending on if we can get a page cache entry later and if it has _INT objects.
|
||||
$visitors = [];
|
||||
$conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
|
||||
$conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
|
||||
$visitors[] = $conditionConstantSubstitutionVisitor;
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
$visitors[] = $conditionMatcherVisitor;
|
||||
// It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
|
||||
// Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
|
||||
$this->includeTreeTraverser->traverse($conditionTree, $visitors);
|
||||
return [
|
||||
'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
|
||||
'setupIncludeTree' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup
|
||||
// condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible),
|
||||
// register the constant substitution visitor, the condition matcher and the condition accumulator visitor.
|
||||
$includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
|
||||
$visitors = [];
|
||||
$conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
|
||||
$conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
|
||||
$visitors[] = $conditionConstantSubstitutionVisitor;
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
$visitors[] = $conditionMatcherVisitor;
|
||||
$conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
|
||||
$visitors[] = $conditionTreeAccumulatorVisitor;
|
||||
// It is important to use IncludeTreeTraverser here: We need the condition verdicts of *all* conditions, and
|
||||
// we want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested
|
||||
// conditions if an upper one does not match, which defeats cache identifier calculations.
|
||||
$this->includeTreeTraverser->traverse($includeTree, $visitors);
|
||||
|
||||
$typoScriptCache?->set(
|
||||
$conditionTreeCacheIdentifier,
|
||||
'return unserialize(\'' . addcslashes(serialize($conditionTreeAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');'
|
||||
);
|
||||
|
||||
return [
|
||||
'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
|
||||
'setupIncludeTree' => $includeTree,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich the given FrontendTypoScript object with TypoScript 'setup' relevant data.
|
||||
*
|
||||
* The method is called in FE after an attempt to retrieve page content from cache has
|
||||
* been done. There are three possible outcomes:
|
||||
* * The page has been retrieved from cache and the content *does not* contain uncached "_INT" objects
|
||||
* * The page has been retrieved from cache and the content *does* contain uncached "_INT" objects
|
||||
* * The page could not be retrieved from cache
|
||||
*
|
||||
* If the page could not be retrieved from cache, or if the cached page content contains "_INT" objects,
|
||||
* flag $needsFullSetup is given true, and the full TypoScript is calculated since at least parts of
|
||||
* the page content has to be rendered, which then needs full TypoScript.
|
||||
* If the page could be retrieved from cache, and contains no "_INT" objects, $needsFullSetup in false, the
|
||||
* rendering chain only needs the "config." part of TypoScript to satisfy the remaining middlewares.
|
||||
*
|
||||
* The method implements these variants and tries to add as little overhead as possible.
|
||||
*
|
||||
* Returns the FrontendTypoScript object:
|
||||
* * configTree: Always set. Global TypoScript 'config.' merged with overrides from given type/typeNum "page.config.".
|
||||
* * configArray: Always set. Array representation of configTree.
|
||||
* * setupTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache. Full TypoScript setup.
|
||||
* * setupArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache.
|
||||
* Array representation of setupTree.
|
||||
* * pageTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object
|
||||
* could be determined. The 'PAGE' object tree for given type/typeNum.
|
||||
* * pageArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object
|
||||
* could be determined. Array representation of PageTree.
|
||||
*/
|
||||
public function createSetupConfigOrFullSetup(
|
||||
bool $needsFullSetup,
|
||||
FrontendTypoScript $frontendTypoScript,
|
||||
SiteInterface $site,
|
||||
array $sysTemplateRows,
|
||||
array $expressionMatcherVariables,
|
||||
string $type,
|
||||
?PhpFrontend $typoScriptCache,
|
||||
?ServerRequestInterface $request,
|
||||
): FrontendTypoScript {
|
||||
$setupTypoScriptCacheIdentifier = 'setup-' . hash(
|
||||
'xxh3',
|
||||
json_encode($sysTemplateRows, JSON_THROW_ON_ERROR)
|
||||
. ($site instanceof Site && $site->isTypoScriptRoot() ? $site->getIdentifier() : '')
|
||||
. json_encode($frontendTypoScript->getSettingsConditionList(), JSON_THROW_ON_ERROR)
|
||||
. json_encode($frontendTypoScript->getSetupConditionList(), JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$setupConfigTypoScriptCacheIdentifier = 'setup-config-' . hash('xxh3', $setupTypoScriptCacheIdentifier . $type);
|
||||
|
||||
$gotSetupConfigFromCache = false;
|
||||
if ($setupConfigTypoScriptCache = $typoScriptCache?->require($setupConfigTypoScriptCacheIdentifier)) {
|
||||
$frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']);
|
||||
$frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']);
|
||||
if (!$needsFullSetup) {
|
||||
// Fully cached page context without _INT - only 'config' is needed. Return early.
|
||||
return $frontendTypoScript;
|
||||
}
|
||||
$gotSetupConfigFromCache = true;
|
||||
}
|
||||
|
||||
$setupRawConfigAst = null;
|
||||
if (!$typoScriptCache || $needsFullSetup || !$gotSetupConfigFromCache) {
|
||||
// If caching is not allowed, if no page cache entry could be loaded or if the page cache entry has _INT
|
||||
// object, we need the full setup AST. Try to use a cache entry for setup AST, which especially up _INT
|
||||
// parsing. In unavailable, calculate full setup AST and cache it if allowed.
|
||||
$gotSetupFromCache = false;
|
||||
if ($setupTypoScriptCache = $typoScriptCache?->require($setupTypoScriptCacheIdentifier)) {
|
||||
// We need AST, and we got it from cache.
|
||||
$frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']);
|
||||
$frontendTypoScript->setSetupArray($setupTypoScriptCache['array']);
|
||||
$setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config');
|
||||
$gotSetupFromCache = true;
|
||||
}
|
||||
if (!$typoScriptCache || !$gotSetupFromCache) {
|
||||
// We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup
|
||||
// IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not.
|
||||
$setupIncludeTree = $frontendTypoScript->getSetupIncludeTree();
|
||||
if (!$typoScriptCache || $setupIncludeTree === null) {
|
||||
// A previous method *may* have calculated setup include tree already. Calculate now if not.
|
||||
$setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
|
||||
}
|
||||
$visitors = [];
|
||||
$conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
|
||||
$conditionConstantSubstitutionVisitor->setFlattenedConstants($frontendTypoScript->getFlatSettings());
|
||||
$visitors[] = $conditionConstantSubstitutionVisitor;
|
||||
$conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
|
||||
$visitors[] = $conditionMatcherVisitor;
|
||||
$astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
|
||||
$astBuilderVisitor->setFlatConstants($frontendTypoScript->getFlatSettings());
|
||||
$visitors[] = $astBuilderVisitor;
|
||||
$this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $visitors);
|
||||
$setupAst = $astBuilderVisitor->getAst();
|
||||
// @todo: It would be good to actively remove 'config' from AST and array here
|
||||
// to prevent people from using the unmerged variant. The same
|
||||
// is already done for the determined PAGE 'config' below. This works, but
|
||||
// is currently blocked by functional tests that assert details?
|
||||
// Also, we need to still cache with full 'config' to handle multiple types.
|
||||
$setupRawConfigAst = $setupAst->getChildByName('config');
|
||||
$frontendTypoScript->setSetupTree($setupAst);
|
||||
$frontendTypoScript->setSetupArray($setupAst->toArray());
|
||||
|
||||
// Write cache entry for AST and its array representation.
|
||||
$typoScriptCache?->set(
|
||||
$setupTypoScriptCacheIdentifier,
|
||||
'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');'
|
||||
);
|
||||
}
|
||||
|
||||
$setupAst = $frontendTypoScript->getSetupTree();
|
||||
$rawSetupPageNodeFromType = null;
|
||||
$pageNodeFoundByType = false;
|
||||
foreach ($setupAst->getNextChild() as $potentialPageNode) {
|
||||
// Find the PAGE object that matches given type/typeNum
|
||||
if ($potentialPageNode->getValue() === 'PAGE') {
|
||||
// @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people
|
||||
// from accessing other ones than the determined one in $frontendTypoScript->getSetupArray().
|
||||
$typeNumChild = $potentialPageNode->getChildByName('typeNum');
|
||||
if ($typeNumChild && $type === $typeNumChild->getValue()) {
|
||||
$rawSetupPageNodeFromType = $potentialPageNode;
|
||||
$pageNodeFoundByType = true;
|
||||
break;
|
||||
}
|
||||
if (!$typeNumChild && $type === '0') {
|
||||
// The first PAGE node that has no typeNum is considered '0' automatically.
|
||||
$rawSetupPageNodeFromType = $potentialPageNode;
|
||||
$pageNodeFoundByType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$pageNodeFoundByType) {
|
||||
$rawSetupPageNodeFromType = new RootNode();
|
||||
}
|
||||
$setupPageAst = new RootNode();
|
||||
foreach ($rawSetupPageNodeFromType->getNextChild() as $child) {
|
||||
$setupPageAst->addChild($child);
|
||||
}
|
||||
|
||||
if (!$gotSetupConfigFromCache) {
|
||||
// If we did not get merged 'config.' from cache above, create it now and cache it.
|
||||
$mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config'));
|
||||
if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) {
|
||||
// Make sure config.absRefPrefix is set, fallback to 'auto'.
|
||||
$absRefPrefixNode = new ChildNode('absRefPrefix');
|
||||
$absRefPrefixNode->setValue('auto');
|
||||
$mergedSetupConfigAst->addChild($absRefPrefixNode);
|
||||
}
|
||||
if ($mergedSetupConfigAst->getChildByName('doctype') === null) {
|
||||
// Make sure config.doctype is set, fallback to 'html5'.
|
||||
$doctypeNode = new ChildNode('doctype');
|
||||
$doctypeNode->setValue('html5');
|
||||
$mergedSetupConfigAst->addChild($doctypeNode);
|
||||
}
|
||||
if ($request) {
|
||||
// Dispatch ModifyTypoScriptConfigEvent before config is cached and if Request is given.
|
||||
$mergedSetupConfigAst = $this->eventDispatcher
|
||||
->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree();
|
||||
}
|
||||
$frontendTypoScript->setConfigTree($mergedSetupConfigAst);
|
||||
$setupConfigArray = $mergedSetupConfigAst->toArray();
|
||||
$frontendTypoScript->setConfigArray($setupConfigArray);
|
||||
$typoScriptCache?->set(
|
||||
$setupConfigTypoScriptCacheIdentifier,
|
||||
'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');'
|
||||
);
|
||||
}
|
||||
|
||||
if ($pageNodeFoundByType) {
|
||||
// Remove "page.config" to prevent people from working with the not merged variant.
|
||||
// We do *not* set page if it could not be determined (important for hasPage() later
|
||||
// to return an early "no PAGE for type found" Response.
|
||||
$setupPageAst->removeChildByName('config');
|
||||
$frontendTypoScript->setPageTree($setupPageAst);
|
||||
$frontendTypoScript->setPageArray($setupPageAst->toArray());
|
||||
}
|
||||
}
|
||||
return $frontendTypoScript;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+68
@@ -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 {}
|
||||
}
|
||||
+64
@@ -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
|
||||
}
|
||||
}
|
||||
+95
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* A data object that carries the final page TSconfig. This is created by PageTsConfigFactory.
|
||||
*
|
||||
* @internal Internal for now until API stabilized. Use BackendUtility::getPagesTSconfig().
|
||||
*/
|
||||
final readonly class PageTsConfig
|
||||
{
|
||||
private array $pageTsConfigArray;
|
||||
|
||||
public function __construct(
|
||||
private RootNode $pageTsConfigTree,
|
||||
private array $conditionListWithVerdicts,
|
||||
) {
|
||||
$this->pageTsConfigArray = $pageTsConfigTree->toArray();
|
||||
}
|
||||
|
||||
public function getPageTsConfigTree(): RootNode
|
||||
{
|
||||
return $this->pageTsConfigTree;
|
||||
}
|
||||
|
||||
public function getPageTsConfigArray(): array
|
||||
{
|
||||
return $this->pageTsConfigArray;
|
||||
}
|
||||
|
||||
public function getConditionListWithVerdicts(): array
|
||||
{
|
||||
return $this->conditionListWithVerdicts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\SiteInclude;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\TsConfigInclude;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\TsConfigTreeBuilder;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Calculate page TSconfig. This does the heavy lifting additionally supported by
|
||||
* TsConfigTreeBuilder: Load basic page TSconfig tree, overload with user TSconfig, parse
|
||||
* site settings ("constants"), then build the page TSconfig AST and return page TSconfig DTO.
|
||||
*
|
||||
* @internal Internal for now until API stabilized. Use BackendUtility::getPagesTSconfig().
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class PageTsConfigFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ContainerInterface $container,
|
||||
private TokenizerInterface $tokenizer,
|
||||
private TsConfigTreeBuilder $tsConfigTreeBuilder,
|
||||
#[Autowire(service: 'cache.typoscript')]
|
||||
private PhpFrontend $cache,
|
||||
) {}
|
||||
|
||||
public function create(
|
||||
array $fullRootLine,
|
||||
SiteInterface $site,
|
||||
?UserTsConfig $userTsConfig = null
|
||||
): PageTsConfig {
|
||||
$pagesTsConfigTree = $this->tsConfigTreeBuilder->getPagesTsConfigTree($fullRootLine, $this->tokenizer, $this->cache);
|
||||
|
||||
// Overloading with user TSconfig if hand over
|
||||
if ($userTsConfig !== null) {
|
||||
$userTsConfigAst = $userTsConfig->getUserTsConfigTree();
|
||||
$userTsConfigPageOverrides = '';
|
||||
// @todo: This is ugly and expensive. There should be a better way to do this. Similar in BE page TSconfig controllers.
|
||||
$userTsConfigFlat = $userTsConfigAst->flatten();
|
||||
foreach ($userTsConfigFlat as $userTsConfigIdentifier => $userTsConfigValue) {
|
||||
if (str_starts_with($userTsConfigIdentifier, 'page.')) {
|
||||
$userTsConfigPageOverrides .= substr($userTsConfigIdentifier, 5) . ' = ' . $userTsConfigValue . chr(10);
|
||||
}
|
||||
}
|
||||
if (!empty($userTsConfigPageOverrides)) {
|
||||
$includeNode = new TsConfigInclude();
|
||||
$includeNode->setName('pageTsConfig-overrides-by-userTsConfig');
|
||||
$includeNode->setLineStream($this->tokenizer->tokenize($userTsConfigPageOverrides));
|
||||
$pagesTsConfigTree->addChild($includeNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare site constants to be substituted
|
||||
$includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser();
|
||||
$siteSettingsFlat = [];
|
||||
if ($site instanceof Site) {
|
||||
$siteSettings = $site->getSettings();
|
||||
if (!$siteSettings->isEmpty()) {
|
||||
$siteSettingsCacheIdentifier = 'site-settings-flat-' . hash('xxh3', json_encode($siteSettings, JSON_THROW_ON_ERROR));
|
||||
$siteSettingsCacheArray = $this->cache->require($siteSettingsCacheIdentifier);
|
||||
if (isset($siteSettingsCacheArray['flatConstants'])) {
|
||||
$siteSettingsFlat = $siteSettingsCacheArray['flatConstants'];
|
||||
} else {
|
||||
$siteConstants = '';
|
||||
$siteSettings = $siteSettings->getAllFlat();
|
||||
foreach ($siteSettings as $nodeIdentifier => $value) {
|
||||
$siteConstants .= $nodeIdentifier . ' = ' . $value . LF;
|
||||
}
|
||||
$siteSettingsNode = new SiteInclude();
|
||||
$siteSettingsNode->setName('Site constants settings of site "' . $site->getIdentifier() . '"');
|
||||
$siteSettingsNode->setLineStream($this->tokenizer->tokenize($siteConstants));
|
||||
$siteSettingsTreeRoot = new RootInclude();
|
||||
$siteSettingsTreeRoot->addChild($siteSettingsNode);
|
||||
$astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
|
||||
$includeTreeTraverserConditionVerdictAware->traverse($siteSettingsTreeRoot, [$astBuilderVisitor]);
|
||||
$siteSettingsFlat = $astBuilderVisitor->getAst()->flatten();
|
||||
$this->cache->set($siteSettingsCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['flatConstants' => $siteSettingsFlat]), '\'\\') . '\');');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create AST with constants from site and conditions
|
||||
$includeTreeTraverserConditionVerdictAwareVisitors = [];
|
||||
if (!empty($siteSettingsFlat)) {
|
||||
$setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
|
||||
$setupConditionConstantSubstitutionVisitor->setFlattenedConstants($siteSettingsFlat);
|
||||
$includeTreeTraverserConditionVerdictAwareVisitors[] = $setupConditionConstantSubstitutionVisitor;
|
||||
}
|
||||
$lastPageFullRecord = [];
|
||||
$pageId = 0;
|
||||
if (!empty($fullRootLine)) {
|
||||
$lastPage = array_last($fullRootLine);
|
||||
$pageId = $lastPage['uid'];
|
||||
$lastPageFullRecord = BackendUtility::getRecord('pages', $pageId) ?: [];
|
||||
}
|
||||
$conditionMatcherVariables = [
|
||||
'fullRootLine' => $fullRootLine,
|
||||
'site' => $site,
|
||||
// @todo We're using the full page row here to provide all necessary fields (e.g. "backend_layout"),
|
||||
// which are currently not included in the rows, RootlineUtility provides by default. We might
|
||||
// want to switch to array_last($fullRootLine) as soon as it contains all fields.
|
||||
'page' => $lastPageFullRecord,
|
||||
'pageId' => $pageId,
|
||||
];
|
||||
$conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
|
||||
$conditionMatcherVisitor->initializeExpressionMatcherWithVariables($conditionMatcherVariables);
|
||||
$includeTreeTraverserConditionVerdictAwareVisitors[] = $conditionMatcherVisitor;
|
||||
$astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
|
||||
$astBuilderVisitor->setFlatConstants($siteSettingsFlat);
|
||||
$includeTreeTraverserConditionVerdictAwareVisitors[] = $astBuilderVisitor;
|
||||
$includeTreeTraverserConditionVerdictAware->traverse($pagesTsConfigTree, $includeTreeTraverserConditionVerdictAwareVisitors);
|
||||
|
||||
return new PageTsConfig($astBuilderVisitor->getAst(), $conditionMatcherVisitor->getConditionListWithVerdicts());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Implement main LineInterface methods.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
abstract class AbstractLine implements LineInterface
|
||||
{
|
||||
protected TokenStreamInterface $tokenStream;
|
||||
|
||||
public function setTokenStream(TokenStreamInterface $tokenStream): static
|
||||
{
|
||||
$this->tokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTokenStream(): TokenStreamInterface
|
||||
{
|
||||
return $this->tokenStream;
|
||||
}
|
||||
}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* A block close line, essentially "}".
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class BlockCloseLine extends AbstractLine {}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* A commented TypoScript line: Lines that start with "#", "//" and multiline comments "/* ... *\/"
|
||||
*
|
||||
* Note multiline comments often represent multiple source lines: An opening "/*" as
|
||||
* first source line, then the comment body with one or more source lines, then finally
|
||||
* the closing "*\/". These still create only one "CommentLine".
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class CommentLine extends AbstractLine {}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* "[ELSE]" / "[else]": An else block after a starting ConditionLine.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class ConditionElseLine extends AbstractLine {}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
|
||||
/**
|
||||
* A condition line: "[foo == bar]".
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class ConditionLine extends AbstractLine
|
||||
{
|
||||
private Token $valueToken;
|
||||
|
||||
public function setValueToken(Token $token): static
|
||||
{
|
||||
if ($token->getType() !== TokenType::T_VALUE) {
|
||||
throw new \LogicException('Token must be of type T_VALUE', 1655823705);
|
||||
}
|
||||
$this->valueToken = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTokenValue(): Token
|
||||
{
|
||||
return $this->valueToken;
|
||||
}
|
||||
}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* A line stopping current ConditionLine context:
|
||||
* "[END]" / "[end]" / "[GLOBAL]" / "[global]".
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class ConditionStopLine extends AbstractLine {}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* A completely empty line, or a line consisting of tabs or whitespaces only.
|
||||
*
|
||||
* This is not created when the TypoScript source line is within multiline "("
|
||||
* assignments and multiline "/*" comments: The T_BLANK and T_NEWLINE tokens
|
||||
* are part of the value steram in these contexts.
|
||||
*
|
||||
* Note the LossyTokenizers does not create these and just skips them since
|
||||
* they have no semantic meaning for the resulting TypoScript tree.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class EmptyLine extends AbstractLine {}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Simple "=" assignments and multiline "(" assignments: "foo.bar = barValue".
|
||||
*
|
||||
* Each line has two additional token streams: $identifierTokenStream for the
|
||||
* left side ("foo" and "bar" tokens) and $valueTokenStream for the right side
|
||||
* ("barValue" token). Right side is often a single token only, but can be many
|
||||
* tokens when constants and multiline assignments are involved.
|
||||
*
|
||||
* Neither the left, nor the right side streams can be empty: Even with "foo.bar ="
|
||||
* a T_VALUE token with empty value is created for the right side.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierAssignmentLine extends AbstractLine
|
||||
{
|
||||
private IdentifierTokenStream $identifierTokenStream;
|
||||
private TokenStreamInterface $valueTokenStream;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655824257);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
|
||||
public function setValueTokenStream(TokenStreamInterface $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Value token stream must not be empty', 1655824258);
|
||||
}
|
||||
$this->valueTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValueTokenStream(): TokenStreamInterface
|
||||
{
|
||||
return $this->valueTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A block open line: "foo.bar {".
|
||||
*
|
||||
* $identifierTokenStream is a stream of tokens on the left side, "foo"
|
||||
* and "bar" token in the example above. That stream must not be empty.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierBlockOpenLine extends AbstractLine
|
||||
{
|
||||
private IdentifierTokenStream $identifierTokenStream;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655824621);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A line using the copy operator: "foo.bar < lib.myLib".
|
||||
*
|
||||
* Contains a stream of tokens for the left side ("foo" and "bar" tokens) and
|
||||
* a stream of tokens for the right side ("lib" and "myLib"). None of these
|
||||
* token streams can be empty, it's an InvalidLine otherwise.
|
||||
*
|
||||
* Note the right side TokenStreamIdentifier can be relative: "foo.bar < .baz".
|
||||
* Flag $relative in TokenStreamIdentifier represents this start dot on the right side.
|
||||
*
|
||||
* None of the two streams can be empty.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierCopyLine extends AbstractLine
|
||||
{
|
||||
private IdentifierTokenStream $identifierTokenStream;
|
||||
private IdentifierTokenStream $valueTokenStream;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655824946);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
|
||||
public function setValueTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Value token stream must not be empty', 1655824947);
|
||||
}
|
||||
$this->valueTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValueTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->valueTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
|
||||
/**
|
||||
* A line with a function assignment using the ":=" operator: "foo.bar := addToList(42)".
|
||||
*
|
||||
* Contains $identifierTokenStream for the left side ("foo" and "bar" token), a single
|
||||
* token for the function name ("addToList"), and an optional token for the value ("42").
|
||||
* Note the value token is optional since there are functions without values (eg. "uniqueList()").
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierFunctionLine extends AbstractLine
|
||||
{
|
||||
private ?IdentifierTokenStream $identifierTokenStream = null;
|
||||
private ?Token $functionNameToken = null;
|
||||
private ?TokenStreamInterface $functionValueTokenStream = null;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): IdentifierFunctionLine
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655825120);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
if ($this->identifierTokenStream === null) {
|
||||
throw new \RuntimeException('Identifier token stream has not been set', 1717495444);
|
||||
}
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
|
||||
public function setFunctionNameToken(Token $token): IdentifierFunctionLine
|
||||
{
|
||||
if ($token->getType() !== TokenType::T_FUNCTION_NAME) {
|
||||
throw new \LogicException('Function name token must be of type T_FUNCTION_NAME', 1655825121);
|
||||
}
|
||||
$this->functionNameToken = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFunctionNameToken(): Token
|
||||
{
|
||||
if ($this->functionNameToken === null) {
|
||||
throw new \RuntimeException('Function name token has not been set', 1717495576);
|
||||
}
|
||||
return $this->functionNameToken;
|
||||
}
|
||||
|
||||
public function setFunctionValueTokenStream(TokenStreamInterface $tokenStream): IdentifierFunctionLine
|
||||
{
|
||||
$this->functionValueTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFunctionValueTokenStream(): TokenStreamInterface
|
||||
{
|
||||
if ($this->functionValueTokenStream === null) {
|
||||
throw new \RuntimeException('Function value token stream has not been set', 1717495996);
|
||||
}
|
||||
return $this->functionValueTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A line using the reference ("=<") operator: "foo.bar =< lib.myLib".
|
||||
*
|
||||
* Contains two non-empty token streams: One for the left side ("foo" and "bar" tokens),
|
||||
* and one for the right side ("lib" and "myLib"). Both streams must not be empty.
|
||||
*
|
||||
* Note the AstBuilder does not directly resolve "=<" operators. This is
|
||||
* not a language construct itself and is only resolved in some special cases
|
||||
* in frontend. See ContentObjectRenderer->cObjGetSingle() for more details.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierReferenceLine extends AbstractLine
|
||||
{
|
||||
private IdentifierTokenStream $identifierTokenStream;
|
||||
private IdentifierTokenStream $valueTokenStream;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655825891);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
|
||||
public function setValueTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Value token stream must not be empty', 1655825892);
|
||||
}
|
||||
$this->valueTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValueTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->valueTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A line using the unset (">") operator: "foo.bar >".
|
||||
*
|
||||
* Has $identifierTokenStream for the stream of tokens on the left
|
||||
* side ("foo" and "bar" tokens).
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierUnsetLine extends AbstractLine
|
||||
{
|
||||
private IdentifierTokenStream $identifierTokenStream;
|
||||
|
||||
public function setIdentifierTokenStream(IdentifierTokenStream $tokenStream): static
|
||||
{
|
||||
if ($tokenStream->isEmpty()) {
|
||||
throw new \LogicException('Identifier token stream must not be empty', 1655826025);
|
||||
}
|
||||
$this->identifierTokenStream = $tokenStream;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIdentifierTokenStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->identifierTokenStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
|
||||
/**
|
||||
* A line using the "@import" keyword: "@import 'EXT:my_extension/Configuration/TypoScript/randomfile.typoscript'"
|
||||
*
|
||||
* Contains the $valueToken ("EXT:my_extension/Configuration/TypoScript/randomfile.typoscript"), without the
|
||||
* surrounding tick (') or doubletick ("). The value itself is not parsed further at this point, this
|
||||
* is done by the IncludeTree classes.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class ImportLine extends AbstractLine
|
||||
{
|
||||
private Token $valueToken;
|
||||
|
||||
public function setValueToken(Token $token): static
|
||||
{
|
||||
if ($token->getType() !== TokenType::T_VALUE) {
|
||||
throw new \LogicException('Value token must be of type T_VALUE', 1655826193);
|
||||
}
|
||||
$this->valueToken = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValueToken(): Token
|
||||
{
|
||||
return $this->valueToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* A line that is syntactically invalid.
|
||||
*
|
||||
* This is created by LosslessTokenizer whenever a line does not make sense.
|
||||
* Examples:
|
||||
* "foo.bar" - no operator
|
||||
* "foo.bar <" - right side empty
|
||||
* "@import ''" - no import value
|
||||
*
|
||||
* Note only LosslessTokenizer creates these lines, LossyTokenizer just skips them.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class InvalidLine extends AbstractLine {}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* The TypoScript tokenizers deliver streams of lines. This is the main line interface.
|
||||
*
|
||||
* Each line is represented by a specific line type. For instance, "foo.bar {" creates
|
||||
* an IdentifierBlockOpenLine and has the additional method getIdentifierTokenStream()
|
||||
* to retrieve the "foo" and "bar" identifier tokens.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
interface LineInterface
|
||||
{
|
||||
/**
|
||||
* Set and get the token stream that represents the full line. This is mostly used
|
||||
* in backend to for instance create a TypoScript string back from tokenized lines.
|
||||
*
|
||||
* Note: Only the LosslessTokenizer fills this 'full line' stream, LossyTokenizer
|
||||
* does not for performance reasons.
|
||||
*/
|
||||
public function setTokenStream(TokenStreamInterface $tokenStream): static;
|
||||
public function getTokenStream(): TokenStreamInterface;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?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\Tokenizer\Line;
|
||||
|
||||
/**
|
||||
* Each TypoScript snippet is turned by the tokenizers into a
|
||||
* stream of lines. Tokenizers return instances of this class.
|
||||
*
|
||||
* Iterate line streams in a foreach loop using getNextLine().
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class LineStream
|
||||
{
|
||||
/**
|
||||
* @var LineInterface[]
|
||||
*/
|
||||
private array $lines = [];
|
||||
private int $currentIndex = -1;
|
||||
|
||||
/**
|
||||
* Create a source string from given token lines. This is used in backend
|
||||
* to turn the "full" token streams of lines into strings for output.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$source = '';
|
||||
foreach ($this->getNextLine() as $line) {
|
||||
// We do *not* implement __toString() on lines since this is a
|
||||
// backend thing only, and we do not want to accidentally stringify
|
||||
// lines based on the full stream anywhere.
|
||||
$source .= $line->getTokenStream()->reset();
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* When storing to cache, we only store FE relevant properties and skip
|
||||
* irrelevant things. In particular, $currentIndex should always initialize
|
||||
* to -1 and does not need to be stored.
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'lines' => $this->lines,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream creation.
|
||||
*/
|
||||
public function append(LineInterface $line): self
|
||||
{
|
||||
$this->lines[] = $line;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* We sometimes create a line stream but don't add lines.
|
||||
* This method returns true if lines have been added.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<LineInterface>
|
||||
*/
|
||||
public function getNextLine(): iterable
|
||||
{
|
||||
foreach ($this->lines as $child) {
|
||||
yield $child;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset current pointer. Typically, call this before iterating with getNext().
|
||||
*/
|
||||
public function reset(): self
|
||||
{
|
||||
$this->currentIndex = -1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next line and raise pointer.
|
||||
*
|
||||
* Methods getNext(), peekNext() and reset() are an alternative to
|
||||
* getNextLine() which allow peek of the next line, which getNextLine()
|
||||
* does not. The disadvantage is that these methods create internal
|
||||
* state in $this->currentIndex, which getNextLine() does not. Use
|
||||
* getNext() iteration only if peekNext() is needed to avoid creating
|
||||
* useless state.
|
||||
*/
|
||||
public function getNext(): ?LineInterface
|
||||
{
|
||||
$this->currentIndex++;
|
||||
return $this->lines[$this->currentIndex] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next line but do not raise pointer.
|
||||
*/
|
||||
public function peekNext(): ?LineInterface
|
||||
{
|
||||
return $this->lines[$this->currentIndex + 1] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
<?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\Tokenizer;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\CommentLine;
|
||||
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\EmptyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\InvalidLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierToken;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
|
||||
/**
|
||||
* A lossless tokenizer for TypoScript syntax.
|
||||
*
|
||||
* tokenize() creates a flat stream of tokens from a TypoScript string. It is lossless
|
||||
* and never "looses" characters to allow syntax linting and creating linter-fixed source
|
||||
* strings: tokenize() to create a TokenStream and using string cast (__toString()) on
|
||||
* that stream creates *the same* source string again.
|
||||
*
|
||||
* The tokenizer *does not* parse conditions or includes itself (no file / db lookups),
|
||||
* this is part of the IncludeTree parser.
|
||||
*
|
||||
* This class is unit test covered by TokenizerInterfaceTest and paired with LossyTokenizer.
|
||||
* Never change anything in this class without additional test coverage!
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class LosslessTokenizer implements TokenizerInterface
|
||||
{
|
||||
private LineStream $lineStream;
|
||||
|
||||
private TokenStreamInterface $tokenStream;
|
||||
private IdentifierTokenStream $identifierStream;
|
||||
private TokenStreamInterface $valueStream;
|
||||
|
||||
private array $lines;
|
||||
private int $currentLineNumber;
|
||||
private string $currentLineString;
|
||||
private \closure $currentLinebreakCallback;
|
||||
private int $currentColumnInLine = 0;
|
||||
|
||||
public function tokenize(string $source): LineStream
|
||||
{
|
||||
$this->lineStream = new LineStream();
|
||||
$this->currentLineNumber = -1;
|
||||
$this->lines = $this->splitLines($source);
|
||||
|
||||
while (true) {
|
||||
$this->tokenStream = new TokenStream();
|
||||
$this->currentLineNumber++;
|
||||
if (!array_key_exists($this->currentLineNumber, $this->lines)) {
|
||||
break;
|
||||
}
|
||||
$this->currentColumnInLine = 0;
|
||||
$this->currentLineString = $this->lines[$this->currentLineNumber]['line'];
|
||||
$this->currentLinebreakCallback = $this->lines[$this->currentLineNumber]['linebreakCallback'];
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
if ($nextChar === '') {
|
||||
($this->currentLinebreakCallback)();
|
||||
if (!$this->tokenStream->isEmpty()) {
|
||||
$this->createEmptyLine();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$nextTwoChars = substr($this->currentLineString, 0, 2);
|
||||
if ($nextChar === '#') {
|
||||
$this->createHashCommentLine();
|
||||
} elseif ($nextTwoChars === '//') {
|
||||
$this->createDoubleSlashCommentLine();
|
||||
} elseif ($nextTwoChars === '/*') {
|
||||
$this->createMultilineCommentLine();
|
||||
} elseif ($nextChar === '[') {
|
||||
$this->createConditionLine();
|
||||
} elseif ($nextChar === '}') {
|
||||
$this->createBlockStopLine();
|
||||
} elseif (str_starts_with($this->currentLineString, '@import')) {
|
||||
$this->parseImportLine();
|
||||
} elseif (str_starts_with($this->currentLineString, '<INCLUDE_TYPOSCRIPT:')) {
|
||||
// @todo: Could be relocated elsewhere. This is just to make sure this
|
||||
// old language construct is detected as InvalidLine.
|
||||
$this->tokenStream->append(new Token(TokenType::T_VALUE, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
} else {
|
||||
$this->parseIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lineStream;
|
||||
}
|
||||
|
||||
private function splitLines($source): array
|
||||
{
|
||||
$vanillaLines = explode(chr(10), $source);
|
||||
$lines = array_map(
|
||||
fn(int $lineNumber, string $vanillaLine): array => [
|
||||
'line' => rtrim($vanillaLine, "\r"),
|
||||
'linebreakCallback' => str_ends_with($vanillaLine, "\r")
|
||||
? fn() => $this->tokenStream->append(new Token(TokenType::T_NEWLINE, "\r\n", $lineNumber, mb_strlen($vanillaLine) - 1))
|
||||
: fn() => $this->tokenStream->append(new Token(TokenType::T_NEWLINE, "\n", $lineNumber, mb_strlen($vanillaLine))),
|
||||
],
|
||||
array_keys($vanillaLines),
|
||||
$vanillaLines
|
||||
);
|
||||
// Set the linebreak callback of last line to empty to suppress dangling linebreak tokens
|
||||
$lines[count($vanillaLines) - 1]['linebreakCallback'] = function () {};
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function createEmptyLine(): void
|
||||
{
|
||||
$this->lineStream->append((new EmptyLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tabs and whitespaces until some different char appears.
|
||||
*/
|
||||
private function parseTabsAndWhitespaces(): void
|
||||
{
|
||||
$matches = [];
|
||||
if (preg_match('#^(\s+)(.*)$#', $this->currentLineString, $matches)) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_BLANK, $matches[1], $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentLineString = $matches[2];
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + strlen($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private function makeComment(): void
|
||||
{
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
if ($nextChar === '') {
|
||||
($this->currentLinebreakCallback)();
|
||||
return;
|
||||
}
|
||||
$nextTwoChars = substr($this->currentLineString, 0, 2);
|
||||
if ($nextChar === '#') {
|
||||
$this->parseHashComment();
|
||||
} elseif ($nextTwoChars === '//') {
|
||||
$this->parseDoubleSlashComment();
|
||||
} elseif ($nextTwoChars === '/*') {
|
||||
$this->parseMultilineComment();
|
||||
} else {
|
||||
$this->parseHashComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function createHashCommentLine(): void
|
||||
{
|
||||
$this->parseHashComment();
|
||||
$this->lineStream->append((new CommentLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
private function parseHashComment(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_COMMENT_ONELINE_HASH, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
($this->currentLinebreakCallback)();
|
||||
}
|
||||
|
||||
private function createDoubleSlashCommentLine(): void
|
||||
{
|
||||
$this->parseDoubleSlashComment();
|
||||
$this->lineStream->append((new CommentLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
private function parseDoubleSlashComment(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_COMMENT_ONELINE_DOUBLESLASH, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
($this->currentLinebreakCallback)();
|
||||
}
|
||||
|
||||
private function createMultilineCommentLine(): void
|
||||
{
|
||||
$this->parseMultilineComment();
|
||||
$this->lineStream->append((new CommentLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
private function parseMultilineComment(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_COMMENT_MULTILINE_START, '/*', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine += 2;
|
||||
$this->currentLineString = substr($this->currentLineString, 2);
|
||||
while (true) {
|
||||
if (str_ends_with($this->currentLineString, '*/')) {
|
||||
if (strlen($this->currentLineString) > 2) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_VALUE, substr($this->currentLineString, 0, -2), $this->currentLineNumber, $this->currentColumnInLine));
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_COMMENT_MULTILINE_STOP, '*/', $this->currentLineNumber, $this->currentColumnInLine + mb_strlen($this->currentLineString) - 2));
|
||||
($this->currentLinebreakCallback)();
|
||||
return;
|
||||
}
|
||||
if (strlen($this->currentLineString)) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_VALUE, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
if (!array_key_exists($this->currentLineNumber + 1, $this->lines)) {
|
||||
return;
|
||||
}
|
||||
$this->currentLineNumber++;
|
||||
$this->currentColumnInLine = 0;
|
||||
$this->currentLineString = $this->lines[$this->currentLineNumber]['line'];
|
||||
$this->currentLinebreakCallback = $this->lines[$this->currentLineNumber]['linebreakCallback'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a condition line from token stream of this line.
|
||||
*/
|
||||
private function createConditionLine(): void
|
||||
{
|
||||
$upperCaseLine = strtoupper($this->currentLineString);
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_START, '[', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
if (str_starts_with($upperCaseLine, '[ELSE]')) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_ELSE, substr($this->currentLineString, 1, 4), $this->currentLineNumber, $this->currentColumnInLine + 1));
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_STOP, ']', $this->currentLineNumber, $this->currentColumnInLine + 5));
|
||||
$this->currentLineString = substr($this->currentLineString, 6);
|
||||
$this->currentColumnInLine += 6;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new ConditionElseLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($upperCaseLine, '[END]')) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_END, substr($this->currentLineString, 1, 3), $this->currentLineNumber, $this->currentColumnInLine + 1));
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_STOP, ']', $this->currentLineNumber, $this->currentColumnInLine + 4));
|
||||
$this->currentLineString = substr($this->currentLineString, 5);
|
||||
$this->currentColumnInLine += 5;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new ConditionStopLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($upperCaseLine, '[GLOBAL]')) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_GLOBAL, substr($this->currentLineString, 1, 6), $this->currentLineNumber, $this->currentColumnInLine + 1));
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_STOP, ']', $this->currentLineNumber, $this->currentColumnInLine + 7));
|
||||
$this->currentLineString = substr($this->currentLineString, 8);
|
||||
$this->currentColumnInLine += 8;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new ConditionStopLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$conditionBody = '';
|
||||
$conditionBodyStartPosition = $this->currentColumnInLine + 1;
|
||||
$conditionBodyCharCount = 0;
|
||||
$conditionBodyChars = mb_str_split(substr($this->currentLineString, 1), 1, 'UTF-8');
|
||||
$bracketCount = 1;
|
||||
while (true) {
|
||||
$nextChar = $conditionBodyChars[$conditionBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
if ($conditionBodyCharCount) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_VALUE, $conditionBody, $this->currentLineNumber, $conditionBodyStartPosition));
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '[') {
|
||||
$bracketCount++;
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
continue;
|
||||
}
|
||||
if ($nextChar === ']') {
|
||||
$bracketCount--;
|
||||
if ($bracketCount === 0) {
|
||||
if ($conditionBodyCharCount) {
|
||||
$conditionBodyToken = new Token(TokenType::T_VALUE, $conditionBody, $this->currentLineNumber, $conditionBodyStartPosition);
|
||||
$this->tokenStream->append($conditionBodyToken);
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_STOP, ']', $this->currentLineNumber, $this->currentColumnInLine + $conditionBodyCharCount + 1));
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $conditionBodyCharCount + 2);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $conditionBodyCharCount + 2;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new ConditionLine())->setTokenStream($this->tokenStream)->setValueToken($conditionBodyToken));
|
||||
return;
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_CONDITION_STOP, ']', $this->currentLineNumber, $this->currentColumnInLine + $conditionBodyCharCount + 1));
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $conditionBodyCharCount + 2);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $conditionBodyCharCount + 2;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
continue;
|
||||
}
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private function createBlockStopLine(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_BLOCK_STOP, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new BlockCloseLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
private function parseBlockStart(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_BLOCK_START, '{', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
if (str_starts_with($this->currentLineString, '}')) {
|
||||
// Edge case: foo = { } in one line. Note content within {} is not parsed, everything behind { ends up as comment.
|
||||
$this->lineStream->append((new IdentifierBlockOpenLine())->setIdentifierTokenStream($this->identifierStream)->setTokenStream($this->tokenStream));
|
||||
$this->tokenStream = new TokenStream();
|
||||
$this->tokenStream->append(new Token(TokenType::T_BLOCK_STOP, '}', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->currentColumnInLine++;
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new BlockCloseLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new IdentifierBlockOpenLine())->setIdentifierTokenStream($this->identifierStream)->setTokenStream($this->tokenStream));
|
||||
}
|
||||
|
||||
private function parseImportLine(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_IMPORT_KEYWORD, '@import', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine += 7;
|
||||
$this->currentLineString = substr($this->currentLineString, 7);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
|
||||
// Next char should be the opening tick or doubletick, otherwise we create a comment until end of line
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
if ($nextChar !== '\'' && $nextChar !== '"') {
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_IMPORT_START, $nextChar, $this->currentLineNumber, $this->currentColumnInLine));
|
||||
|
||||
$importBody = '';
|
||||
$importBodyStartPosition = $this->currentColumnInLine + 1;
|
||||
$importBodyCharCount = 0;
|
||||
$importBodyChars = mb_str_split(substr($this->currentLineString, 1), 1, 'UTF-8');
|
||||
while (true) {
|
||||
$nextChar = $importBodyChars[$importBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
if ($importBodyCharCount) {
|
||||
$importBodyToken = (new Token(TokenType::T_VALUE, $importBody, $this->currentLineNumber, $importBodyStartPosition));
|
||||
$this->tokenStream->append($importBodyToken);
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new ImportLine())->setTokenStream($this->tokenStream)->setValueToken($importBodyToken));
|
||||
return;
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '\'' || $nextChar === '"') {
|
||||
if ($importBodyCharCount) {
|
||||
$importBodyToken = new Token(TokenType::T_VALUE, $importBody, $this->currentLineNumber, $importBodyStartPosition);
|
||||
$this->tokenStream->append($importBodyToken);
|
||||
$this->tokenStream->append(new Token(TokenType::T_IMPORT_STOP, $nextChar, $this->currentLineNumber, $this->currentColumnInLine + $importBodyCharCount + 1));
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $importBodyCharCount + 2);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $importBodyCharCount + 2;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new ImportLine())->setTokenStream($this->tokenStream)->setValueToken($importBodyToken));
|
||||
return;
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_IMPORT_STOP, $nextChar, $this->currentLineNumber, $this->currentColumnInLine + $importBodyCharCount + 1));
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $importBodyCharCount + 2);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $importBodyCharCount + 2;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$importBody .= $nextChar;
|
||||
$importBodyCharCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseIdentifier(): void
|
||||
{
|
||||
$splitLine = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
$currentPosition = $this->parseIdentifierUntilStopChar($splitLine);
|
||||
if (!$currentPosition) {
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $currentPosition);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $currentPosition;
|
||||
$currentColumnInLineBefore = $this->currentColumnInLine;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$currentPosition = $currentPosition + $this->currentColumnInLine - $currentColumnInLineBefore;
|
||||
$nextChar = $splitLine[$currentPosition] ?? null;
|
||||
$nextTwoChars = $nextChar . ($splitLine[$currentPosition + 1] ?? '');
|
||||
if ($nextTwoChars === '=<') {
|
||||
$this->parseOperatorReference();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '=') {
|
||||
$this->parseOperatorAssignment();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '{') {
|
||||
$this->parseBlockStart();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '>') {
|
||||
$this->parseOperatorUnset();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '<') {
|
||||
$this->parseOperatorCopy();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
$this->parseOperatorMultilineAssignment();
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === ':=') {
|
||||
$this->parseOperatorFunction();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '#') {
|
||||
$this->parseHashComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === '//') {
|
||||
$this->parseDoubleSlashComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === '/*') {
|
||||
$this->parseMultilineComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextChar === null) {
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorAssignment(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_ASSIGNMENT, '=', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->valueStream = new TokenStream();
|
||||
[$this->valueStream, $this->tokenStream] = $this->parseValueForConstants($this->valueStream, $this->tokenStream, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine);
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setTokenStream($this->tokenStream)->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream));
|
||||
}
|
||||
|
||||
private function parseOperatorMultilineAssignment(): void
|
||||
{
|
||||
$this->valueStream = new TokenStream();
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_ASSIGNMENT_MULTILINE_START, '(', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
// True if we're currently in the line with the opening '('
|
||||
$isFirstLine = true;
|
||||
// True if the first line has a first value token: "foo ( thisIsTheFirstValueToken"
|
||||
$valueOnFirstLine = false;
|
||||
// True if the line after '(' is parsed
|
||||
$isSecondLine = false;
|
||||
$previousLineCallback = function () {};
|
||||
while (true) {
|
||||
if (str_starts_with(ltrim($this->currentLineString), ')')) {
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_ASSIGNMENT_MULTILINE_STOP, ')', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->currentColumnInLine++;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
if ($this->valueStream->isEmpty()) {
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
} else {
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream)->setTokenStream($this->tokenStream));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine && str_ends_with($this->currentLineString, ')')) {
|
||||
// Special case if the ')' is on same line as the opening '('
|
||||
$this->currentLineString = substr($this->currentLineString, 0, -1);
|
||||
if (strlen($this->currentLineString) > 1) {
|
||||
[$this->valueStream, $this->tokenStream] = $this->parseValueForConstants($this->valueStream, $this->tokenStream, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine);
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_ASSIGNMENT_MULTILINE_STOP, ')', $this->currentLineNumber, $this->currentColumnInLine + mb_strlen($this->currentLineString)));
|
||||
// Tricky to swap the streams here, but that's the most effective solution I could come up with for the line endings here.
|
||||
($this->currentLinebreakCallback)();
|
||||
$tempStream = $this->tokenStream;
|
||||
$this->tokenStream = $this->valueStream;
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->tokenStream = $tempStream;
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream)->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_ASSIGNMENT_MULTILINE_STOP, ')', $this->currentLineNumber, $this->currentColumnInLine + strlen($this->currentLineString)));
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine && strlen($this->currentLineString)) {
|
||||
[$this->valueStream, $this->tokenStream] = $this->parseValueForConstants($this->valueStream, $this->tokenStream, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine);
|
||||
$valueOnFirstLine = true;
|
||||
$previousLineCallback = $this->currentLinebreakCallback;
|
||||
}
|
||||
if (($isFirstLine && $valueOnFirstLine)
|
||||
|| (!$isFirstLine && !$isSecondLine)
|
||||
) {
|
||||
$tempStream = $this->tokenStream;
|
||||
$this->tokenStream = $this->valueStream;
|
||||
$previousLineCallback();
|
||||
$this->tokenStream = $tempStream;
|
||||
}
|
||||
if (!$isFirstLine && strlen($this->currentLineString)) {
|
||||
[$this->valueStream, $this->tokenStream] = $this->parseValueForConstants($this->valueStream, $this->tokenStream, $this->currentLineString, $this->currentLineNumber, $this->currentColumnInLine);
|
||||
}
|
||||
$previousLineCallback = $this->currentLinebreakCallback;
|
||||
($this->currentLinebreakCallback)();
|
||||
if (!array_key_exists($this->currentLineNumber + 1, $this->lines)) {
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine) {
|
||||
$isSecondLine = true;
|
||||
} else {
|
||||
$isSecondLine = false;
|
||||
}
|
||||
$isFirstLine = false;
|
||||
$valueOnFirstLine = false;
|
||||
$this->currentLineNumber++;
|
||||
$this->currentColumnInLine = 0;
|
||||
$this->currentLineString = $this->lines[$this->currentLineNumber]['line'];
|
||||
$this->currentLinebreakCallback = $this->lines[$this->currentLineNumber]['linebreakCallback'];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorUnset(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_UNSET, '>', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new IdentifierUnsetLine())->setTokenStream($this->tokenStream)->setIdentifierTokenStream($this->identifierStream));
|
||||
}
|
||||
|
||||
private function parseOperatorCopy(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_COPY, '<', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$identifierStream = $this->identifierStream;
|
||||
$this->parseIdentifierAtEndOfLine();
|
||||
$referenceStream = $this->identifierStream;
|
||||
if ($referenceStream->isEmpty()) {
|
||||
// @todo: ($this->currentLinebreakCallback)(); is missing here?!
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->lineStream->append(
|
||||
(new IdentifierCopyLine())
|
||||
->setIdentifierTokenStream($identifierStream)
|
||||
->setValueTokenStream($referenceStream)
|
||||
->setTokenStream($this->tokenStream)
|
||||
);
|
||||
}
|
||||
|
||||
private function parseOperatorReference(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_REFERENCE, '=<', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine += 2;
|
||||
$this->currentLineString = substr($this->currentLineString, 2);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$identifierStream = $this->identifierStream;
|
||||
$this->parseIdentifierAtEndOfLine();
|
||||
$referenceStream = $this->identifierStream;
|
||||
if ($referenceStream->isEmpty()) {
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$this->lineStream->append(
|
||||
(new IdentifierReferenceLine())
|
||||
->setIdentifierTokenStream($identifierStream)
|
||||
->setValueTokenStream($referenceStream)
|
||||
->setTokenStream($this->tokenStream)
|
||||
);
|
||||
}
|
||||
|
||||
private function parseIdentifierAtEndOfLine(): void
|
||||
{
|
||||
$this->identifierStream = new IdentifierTokenStream();
|
||||
$isRelative = false;
|
||||
$splitLine = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
$char = $splitLine[0] ?? null;
|
||||
if ($char === null) {
|
||||
return;
|
||||
}
|
||||
$nextTwoChars = $char . ($splitLine[1] ?? '');
|
||||
if ($char === '.') {
|
||||
// A relative right side: foo.bar < .foo (note the dot!). we identifierStream->setRelative() and
|
||||
// get rid of the dot for the rest of the processing.
|
||||
$isRelative = true;
|
||||
$this->tokenStream->append((new Token(TokenType::T_DOT, '.', 0, $this->currentColumnInLine)));
|
||||
array_shift($splitLine);
|
||||
$this->currentColumnInLine++;
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
}
|
||||
if ($char === '#') {
|
||||
$this->parseHashComment();
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === '//') {
|
||||
$this->parseDoubleSlashComment();
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === '/*') {
|
||||
$this->parseMultilineComment();
|
||||
return;
|
||||
}
|
||||
$currentPosition = $this->parseIdentifierUntilStopChar($splitLine, $isRelative);
|
||||
if (!$currentPosition) {
|
||||
return;
|
||||
}
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $currentPosition);
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $currentPosition;
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
}
|
||||
|
||||
private function parseIdentifierUntilStopChar(array $splitLine, bool $isRelative = false): ?int
|
||||
{
|
||||
$this->identifierStream = new IdentifierTokenStream();
|
||||
if ($isRelative) {
|
||||
$this->identifierStream->setRelative();
|
||||
}
|
||||
$currentPosition = 0;
|
||||
$currentIdentifierStartPosition = $this->currentColumnInLine;
|
||||
$currentIdentifierBody = '';
|
||||
$currentIdentifierCharCount = 0;
|
||||
while (true) {
|
||||
$nextChar = $splitLine[$currentPosition] ?? null;
|
||||
if ($nextChar === null) {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody, $this->currentLineNumber, $currentIdentifierStartPosition);
|
||||
$this->tokenStream->append($identifierToken);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
return null;
|
||||
}
|
||||
$nextTwoChars = $nextChar . ($splitLine[$currentPosition + 1] ?? null);
|
||||
if ($currentPosition > 0
|
||||
&& ($nextChar === ' ' || $nextChar === "\t" || $nextChar === '=' || $nextChar === '<' || $nextChar === '>' || $nextChar === '{' || $nextTwoChars === ':=' || $nextChar === '(')
|
||||
) {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody, $this->currentLineNumber, $currentIdentifierStartPosition);
|
||||
$this->tokenStream->append($identifierToken);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($nextTwoChars === '\\.') {
|
||||
// A quoted dot is part of *this* identifier
|
||||
$currentIdentifierBody .= '.';
|
||||
$currentPosition += 2;
|
||||
$currentIdentifierCharCount++;
|
||||
} elseif ($nextChar === '.') {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody, $this->currentLineNumber, $currentIdentifierStartPosition);
|
||||
$this->tokenStream->append($identifierToken);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
$currentIdentifierCharCount = 0;
|
||||
$currentIdentifierBody = '';
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_DOT, '.', $this->currentLineNumber, $this->currentColumnInLine + $currentPosition));
|
||||
$currentPosition++;
|
||||
$currentIdentifierStartPosition = $this->currentColumnInLine + $currentPosition;
|
||||
} else {
|
||||
$currentIdentifierBody .= $nextChar;
|
||||
$currentIdentifierCharCount++;
|
||||
$currentPosition++;
|
||||
}
|
||||
}
|
||||
return $currentPosition;
|
||||
}
|
||||
|
||||
private function parseOperatorFunction(): void
|
||||
{
|
||||
$this->tokenStream->append(new Token(TokenType::T_OPERATOR_FUNCTION, ':=', $this->currentLineNumber, $this->currentColumnInLine));
|
||||
$this->currentColumnInLine += 2;
|
||||
$this->currentLineString = substr($this->currentLineString, 2);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
if ($this->currentLineString === '') {
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$functionName = '';
|
||||
$functionNameStartPosition = $this->currentColumnInLine;
|
||||
$functionNameCharCount = 0;
|
||||
$functionChars = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
while (true) {
|
||||
$nextChar = $functionChars[$functionNameCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
if ($functionNameCharCount) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_FUNCTION_NAME, $functionName, $this->currentLineNumber, $functionNameStartPosition));
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
if ($functionNameCharCount) {
|
||||
$functionNameToken = new Token(TokenType::T_FUNCTION_NAME, $functionName, $this->currentLineNumber, $functionNameStartPosition);
|
||||
$this->tokenStream->append($functionNameToken);
|
||||
$this->tokenStream->append(new Token(TokenType::T_FUNCTION_VALUE_START, '(', $this->currentLineNumber, $this->currentColumnInLine + $functionNameCharCount));
|
||||
$functionNameCharCount++;
|
||||
break;
|
||||
}
|
||||
$this->makeComment();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
$functionName .= $nextChar;
|
||||
$functionNameCharCount++;
|
||||
}
|
||||
$functionBodyStartPosition = $functionNameCharCount;
|
||||
$functionBodyPart = '';
|
||||
$functionBodyCharCount = 0;
|
||||
$functionValueStream = new TokenStream();
|
||||
$parenthesesLevel = 0;
|
||||
while (true) {
|
||||
$nextChar = $functionChars[$functionBodyStartPosition + $functionBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
if ($functionBodyCharCount) {
|
||||
$this->tokenStream->append(new Token(TokenType::T_VALUE, $functionBodyPart, $this->currentLineNumber, $functionBodyStartPosition));
|
||||
}
|
||||
($this->currentLinebreakCallback)();
|
||||
$this->lineStream->append((new InvalidLine())->setTokenStream($this->tokenStream));
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
// In case of a function call like "appendString(something(somethingelse))"
|
||||
// we shall only stop processing when the last bracket was evaluated.
|
||||
$parenthesesLevel++;
|
||||
}
|
||||
if ($nextChar === ')') {
|
||||
if ($parenthesesLevel > 0) {
|
||||
$parenthesesLevel--;
|
||||
// Continue collecting characters from the (...) argument stream.
|
||||
// Also, ")" will be appended, thus intentionally no "break" occurs.
|
||||
} else {
|
||||
if ($functionBodyCharCount) {
|
||||
[$functionValueStream, $this->tokenStream] = $this->parseValueForConstants($functionValueStream, $this->tokenStream, $functionBodyPart, $this->currentLineNumber, $this->currentColumnInLine, $functionBodyStartPosition);
|
||||
}
|
||||
$this->tokenStream->append(new Token(TokenType::T_FUNCTION_VALUE_STOP, ')', $this->currentLineNumber, $this->currentColumnInLine + $functionNameCharCount + $functionBodyCharCount));
|
||||
$functionBodyCharCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$functionBodyPart .= $nextChar;
|
||||
$functionBodyCharCount++;
|
||||
}
|
||||
$this->currentColumnInLine = $this->currentColumnInLine + $functionNameCharCount + $functionBodyCharCount;
|
||||
$this->currentLineString = mb_substr($this->currentLineString, $functionNameCharCount + $functionBodyCharCount);
|
||||
$this->parseTabsAndWhitespaces();
|
||||
$this->makeComment();
|
||||
$this->lineStream->append(
|
||||
(new IdentifierFunctionLine())
|
||||
->setIdentifierTokenStream($this->identifierStream)
|
||||
->setFunctionNameToken($functionNameToken)
|
||||
->setTokenStream($this->tokenStream)
|
||||
->setFunctionValueTokenStream($functionValueStream)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: TokenStreamInterface, 1: TokenStreamInterface}
|
||||
*/
|
||||
private function parseValueForConstants(TokenStreamInterface $valueStream, TokenStreamInterface $tokenStream, string $value, int $line, int $column, int $tokenOffsetPosition = 0): array
|
||||
{
|
||||
if (!str_contains($value, '{$')) {
|
||||
$valueToken = new Token(TokenType::T_VALUE, $value, $line, $column + $tokenOffsetPosition);
|
||||
$valueStream->append($valueToken);
|
||||
$tokenStream->append($valueToken);
|
||||
return [$valueStream, $tokenStream];
|
||||
}
|
||||
$splitLine = mb_str_split($value, 1, 'UTF-8');
|
||||
$isInConstant = false;
|
||||
$currentPosition = 0;
|
||||
$currentString = '';
|
||||
$currentStringLength = 0;
|
||||
$lastTokenEndPosition = 0;
|
||||
while (true) {
|
||||
$char = $splitLine[$currentPosition] ?? null;
|
||||
if ($char === null) {
|
||||
if ($currentStringLength) {
|
||||
$valueToken = new Token(TokenType::T_VALUE, $currentString, $line, $column + $lastTokenEndPosition + $tokenOffsetPosition);
|
||||
$valueStream->append($valueToken);
|
||||
$tokenStream->append($valueToken);
|
||||
}
|
||||
break;
|
||||
}
|
||||
$nextTwoChars = $char . ($splitLine[$currentPosition + 1] ?? '');
|
||||
if ($nextTwoChars === '{$') {
|
||||
$isInConstant = true;
|
||||
if ($currentStringLength) {
|
||||
$valueToken = new Token(TokenType::T_VALUE, $currentString, $line, $column + $lastTokenEndPosition + $tokenOffsetPosition);
|
||||
$valueStream->append($valueToken);
|
||||
$tokenStream->append($valueToken);
|
||||
$lastTokenEndPosition = $currentPosition;
|
||||
}
|
||||
$currentString = '{$';
|
||||
$currentPosition += 2;
|
||||
continue;
|
||||
}
|
||||
if ($isInConstant && $char === '}') {
|
||||
$valueToken = new Token(TokenType::T_CONSTANT, $currentString . '}', $line, $column + $lastTokenEndPosition + $tokenOffsetPosition);
|
||||
if (!$valueStream instanceof ConstantAwareTokenStream) {
|
||||
$valueStream = (new ConstantAwareTokenStream())->setAll($valueStream->getAll());
|
||||
}
|
||||
$valueStream->append($valueToken);
|
||||
$tokenStream->append($valueToken);
|
||||
$currentPosition++;
|
||||
$currentString = '';
|
||||
$currentStringLength = 0;
|
||||
$lastTokenEndPosition = $currentPosition;
|
||||
$isInConstant = false;
|
||||
continue;
|
||||
}
|
||||
$currentPosition++;
|
||||
$currentStringLength++;
|
||||
$currentString .= $char;
|
||||
}
|
||||
return [$valueStream, $tokenStream];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
<?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\Tokenizer;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
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\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\ImportLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierToken;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
|
||||
/**
|
||||
* A lossy tokenizer implementation: Do not create invalid lines, do not create empty lines,
|
||||
* do not create token line and column positions.
|
||||
*
|
||||
* This tokenizer creates a much smaller streams of only relevant lines. All information
|
||||
* not essential for the AstBuilder is skipped. This tokenizer is used in frontend rendering
|
||||
* for quicker AST building.
|
||||
*
|
||||
* An instance of this tokenizer is injected by DI when injecting TokenizerInterface.
|
||||
*
|
||||
* This class is unit test covered by TokenizerInterfaceTest and paired with LossyTokenizer.
|
||||
* Never change anything in this class without additional test coverage!
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
#[AsAlias(TokenizerInterface::class)]
|
||||
final class LossyTokenizer implements TokenizerInterface
|
||||
{
|
||||
private LineStream $lineStream;
|
||||
|
||||
private IdentifierTokenStream $identifierStream;
|
||||
private TokenStreamInterface $valueStream;
|
||||
|
||||
private array $lines;
|
||||
private int $currentLineNumber;
|
||||
private string $currentLineString;
|
||||
|
||||
public function tokenize(string $source): LineStream
|
||||
{
|
||||
$this->lineStream = new LineStream();
|
||||
$this->currentLineNumber = -1;
|
||||
$this->lines = $this->splitLines($source);
|
||||
|
||||
while (true) {
|
||||
$this->currentLineNumber++;
|
||||
if (!array_key_exists($this->currentLineNumber, $this->lines)) {
|
||||
break;
|
||||
}
|
||||
$this->currentLineString = trim($this->lines[$this->currentLineNumber]['line']);
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
if ($nextChar === '') {
|
||||
continue;
|
||||
}
|
||||
$nextTwoChars = substr($this->currentLineString, 0, 2);
|
||||
if ($nextChar === '#' || $nextTwoChars === '//') {
|
||||
continue;
|
||||
}
|
||||
if ($nextTwoChars === '/*') {
|
||||
// @todo: This is one of multiple places where multiline "/*" comments are parsed in this tokenizer. Other
|
||||
// places are cluttered in detail methods. It might be more straight to have an early scanning
|
||||
// phase through all lines to remove comments up front, to not wire especially the multiline comment
|
||||
// parsing to single places, and throw away commented lines early. This isn't trivial though, since
|
||||
// for instance "foo = bar /* not a comment */" then needs to be sorted out, too. Having an early
|
||||
// "kick comments" loop however might be quicker in the end and would make the main parsing
|
||||
// methods more concise and probably more bullet proof.
|
||||
// Also note there are currently not-unit-tested edge cases, that will currently not parse as
|
||||
// (maybe) expected. In the example below, "foo2 = bar2" is ignored. This is an issue with the
|
||||
// LosslessTokenizer as well, probably, and we may rather want to declare this as invalid syntax?!
|
||||
// foo = bar /* comment start
|
||||
// comment end */ foo2 = bar2
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
continue;
|
||||
}
|
||||
if ($nextChar === '[') {
|
||||
$this->createConditionLine();
|
||||
} elseif ($nextChar === '}') {
|
||||
$this->lineStream->append((new BlockCloseLine()));
|
||||
} elseif (str_starts_with($this->currentLineString, '@import')) {
|
||||
$this->parseImportLine();
|
||||
} elseif (str_starts_with($this->currentLineString, '<INCLUDE_TYPOSCRIPT:')) {
|
||||
// @todo: Do nothing. This creates an InvalidLine in LossyTokenizer.
|
||||
} else {
|
||||
$this->parseIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lineStream;
|
||||
}
|
||||
|
||||
private function splitLines($source): array
|
||||
{
|
||||
$vanillaLines = explode(chr(10), $source);
|
||||
return array_map(
|
||||
fn(int $lineNumber, string $vanillaLine): array => [
|
||||
'line' => rtrim($vanillaLine, "\r"),
|
||||
],
|
||||
array_keys($vanillaLines),
|
||||
$vanillaLines
|
||||
);
|
||||
}
|
||||
|
||||
private function ignoreUntilEndOfMultilineComment(): void
|
||||
{
|
||||
while (true) {
|
||||
if (str_contains($this->currentLineString, '*/')) {
|
||||
return;
|
||||
}
|
||||
if (!array_key_exists($this->currentLineNumber + 1, $this->lines)) {
|
||||
return;
|
||||
}
|
||||
$this->currentLineNumber++;
|
||||
$this->currentLineString = trim($this->lines[$this->currentLineNumber]['line']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a condition line from token stream of this line.
|
||||
*/
|
||||
private function createConditionLine(): void
|
||||
{
|
||||
$upperCaseLine = strtoupper($this->currentLineString);
|
||||
if (str_starts_with($upperCaseLine, '[ELSE]')) {
|
||||
$this->lineStream->append((new ConditionElseLine()));
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 6));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($upperCaseLine, '[END]')) {
|
||||
$this->lineStream->append((new ConditionStopLine()));
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 5));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($upperCaseLine, '[GLOBAL]')) {
|
||||
$this->lineStream->append((new ConditionStopLine()));
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 8));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
return;
|
||||
}
|
||||
$conditionBody = '';
|
||||
$conditionBodyCharCount = 0;
|
||||
$conditionBodyChars = mb_str_split(substr($this->currentLineString, 1), 1, 'UTF-8');
|
||||
$bracketCount = 1;
|
||||
while (true) {
|
||||
$nextChar = $conditionBodyChars[$conditionBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '[') {
|
||||
$bracketCount++;
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
continue;
|
||||
}
|
||||
if ($nextChar === ']') {
|
||||
$bracketCount--;
|
||||
if ($bracketCount === 0) {
|
||||
if ($conditionBodyCharCount) {
|
||||
$conditionBodyToken = new Token(TokenType::T_VALUE, $conditionBody);
|
||||
$this->lineStream->append((new ConditionLine())->setValueToken($conditionBodyToken));
|
||||
$conditionBodyCharCount++;
|
||||
break;
|
||||
}
|
||||
$conditionBodyCharCount++;
|
||||
break;
|
||||
}
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
continue;
|
||||
}
|
||||
$conditionBody .= $nextChar;
|
||||
$conditionBodyCharCount++;
|
||||
}
|
||||
$this->currentLineString = trim(mb_substr($this->currentLineString, $conditionBodyCharCount + 1));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseBlockStart(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 1));
|
||||
if (str_starts_with($this->currentLineString, '}')) {
|
||||
// Edge case: foo = { } in one line. Note content within {} is not parsed, everything behind { ends up as comment.
|
||||
$this->lineStream->append((new IdentifierBlockOpenLine())->setIdentifierTokenStream($this->identifierStream));
|
||||
$this->lineStream->append((new BlockCloseLine()));
|
||||
return;
|
||||
}
|
||||
$this->lineStream->append((new IdentifierBlockOpenLine())->setIdentifierTokenStream($this->identifierStream));
|
||||
}
|
||||
|
||||
private function parseImportLine(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 7));
|
||||
|
||||
// Next char should be the opening tick or doubletick, otherwise treat it as ignored comment
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
if ($nextChar !== '\'' && $nextChar !== '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
$importBody = '';
|
||||
$importBodyCharCount = 0;
|
||||
$importBodyChars = mb_str_split(substr($this->currentLineString, 1), 1, 'UTF-8');
|
||||
while (true) {
|
||||
$nextChar = $importBodyChars[$importBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
if ($importBodyCharCount) {
|
||||
$importBodyToken = (new Token(TokenType::T_VALUE, $importBody));
|
||||
$this->lineStream->append((new ImportLine())->setValueToken($importBodyToken));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '\'' || $nextChar === '"') {
|
||||
if ($importBodyCharCount) {
|
||||
$importBodyToken = new Token(TokenType::T_VALUE, $importBody);
|
||||
$this->lineStream->append((new ImportLine())->setValueToken($importBodyToken));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
$importBody .= $nextChar;
|
||||
$importBodyCharCount++;
|
||||
}
|
||||
$this->currentLineString = trim(mb_substr($this->currentLineString, $importBodyCharCount + 2));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseIdentifier(): void
|
||||
{
|
||||
$splitLine = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
$currentPosition = $this->parseIdentifierUntilStopChar($splitLine);
|
||||
if (!$currentPosition) {
|
||||
return;
|
||||
}
|
||||
$this->currentLineString = trim(mb_substr($this->currentLineString, $currentPosition));
|
||||
$nextChar = substr($this->currentLineString, 0, 1);
|
||||
$nextTwoChars = $nextChar . substr($this->currentLineString, 1, 1);
|
||||
if ($nextTwoChars === '=<') {
|
||||
$this->parseOperatorReference();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '=') {
|
||||
$this->parseOperatorAssignment();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '{') {
|
||||
$this->parseBlockStart();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '>') {
|
||||
$this->parseOperatorUnset();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '<') {
|
||||
$this->parseOperatorCopy();
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
$this->parseOperatorMultilineAssignment();
|
||||
return;
|
||||
}
|
||||
if ($nextTwoChars === ':=') {
|
||||
$this->parseOperatorFunction();
|
||||
}
|
||||
if ($nextTwoChars === '/*') {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorUnset(): void
|
||||
{
|
||||
$this->lineStream->append((new IdentifierUnsetLine())->setIdentifierTokenStream($this->identifierStream));
|
||||
$this->currentLineString = trim(trim(trim($this->currentLineString), '>'));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorAssignment(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 1));
|
||||
$this->valueStream = $this->parseValueForConstants(new TokenStream(), $this->currentLineString);
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream));
|
||||
}
|
||||
|
||||
private function parseOperatorMultilineAssignment(): void
|
||||
{
|
||||
$this->valueStream = new TokenStream();
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
// True if we're currently in the line with the opening '('
|
||||
$isFirstLine = true;
|
||||
// True if the first line has a first value token: "foo ( thisIsTheFirstValueToken"
|
||||
$valueOnFirstLine = false;
|
||||
// True if the line after '(' is parsed
|
||||
$isSecondLine = false;
|
||||
while (true) {
|
||||
if (str_starts_with(ltrim($this->currentLineString), ')')) {
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 1));
|
||||
if (!$this->valueStream->isEmpty()) {
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream));
|
||||
}
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine && str_ends_with($this->currentLineString, ')')) {
|
||||
$this->currentLineString = substr($this->currentLineString, 0, -1);
|
||||
if (strlen($this->currentLineString) > 1) {
|
||||
$this->valueStream = $this->parseValueForConstants($this->valueStream, $this->currentLineString);
|
||||
$this->lineStream->append((new IdentifierAssignmentLine())->setIdentifierTokenStream($this->identifierStream)->setValueTokenStream($this->valueStream));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine && strlen($this->currentLineString)) {
|
||||
$this->valueStream = $this->parseValueForConstants($this->valueStream, $this->currentLineString);
|
||||
$valueOnFirstLine = true;
|
||||
}
|
||||
if (($isFirstLine && $valueOnFirstLine)
|
||||
|| (!$isFirstLine && !$isSecondLine)
|
||||
) {
|
||||
$this->valueStream->append(new Token(TokenType::T_NEWLINE, "\n"));
|
||||
}
|
||||
if (!$isFirstLine && strlen($this->currentLineString)) {
|
||||
$this->valueStream = $this->parseValueForConstants($this->valueStream, $this->currentLineString);
|
||||
}
|
||||
if (!array_key_exists($this->currentLineNumber + 1, $this->lines)) {
|
||||
return;
|
||||
}
|
||||
if ($isFirstLine) {
|
||||
$isSecondLine = true;
|
||||
} else {
|
||||
$isSecondLine = false;
|
||||
}
|
||||
$isFirstLine = false;
|
||||
$valueOnFirstLine = false;
|
||||
$this->currentLineNumber++;
|
||||
$this->currentLineString = $this->lines[$this->currentLineNumber]['line'];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorCopy(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 1));
|
||||
$identifierStream = $this->identifierStream;
|
||||
$charsHandled = $this->parseIdentifierAtEndOfLine();
|
||||
$referenceStream = $this->identifierStream;
|
||||
if ($referenceStream->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$this->lineStream->append(
|
||||
(new IdentifierCopyLine())
|
||||
->setIdentifierTokenStream($identifierStream)
|
||||
->setValueTokenStream($referenceStream)
|
||||
);
|
||||
$this->currentLineString = trim(mb_substr($this->currentLineString, $charsHandled));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOperatorReference(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 2));
|
||||
$identifierStream = $this->identifierStream;
|
||||
$charsHandled = $this->parseIdentifierAtEndOfLine();
|
||||
$referenceStream = $this->identifierStream;
|
||||
if ($referenceStream->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$this->lineStream->append(
|
||||
(new IdentifierReferenceLine())
|
||||
->setIdentifierTokenStream($identifierStream)
|
||||
->setValueTokenStream($referenceStream)
|
||||
);
|
||||
$this->currentLineString = trim(mb_substr($this->currentLineString, $charsHandled));
|
||||
if (str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseIdentifierAtEndOfLine(): int
|
||||
{
|
||||
$this->identifierStream = new IdentifierTokenStream();
|
||||
$isRelative = false;
|
||||
$splitLine = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
$char = $splitLine[0] ?? null;
|
||||
if ($char === null) {
|
||||
return 0;
|
||||
}
|
||||
$nextTwoChars = $char . ($splitLine[1] ?? '');
|
||||
if ($char === '.') {
|
||||
// A relative right side: foo.bar < .foo (note the dot!). we identifierStream->setRelative() and
|
||||
// get rid of the dot for the rest of the processing.
|
||||
$isRelative = true;
|
||||
array_shift($splitLine);
|
||||
$this->currentLineString = substr($this->currentLineString, 1);
|
||||
}
|
||||
if ($char === '#') {
|
||||
return 1;
|
||||
}
|
||||
if ($nextTwoChars === '//') {
|
||||
return 2;
|
||||
}
|
||||
if ($nextTwoChars === '/*') {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
return 0;
|
||||
}
|
||||
return $this->parseIdentifierUntilStopChar($splitLine, $isRelative);
|
||||
}
|
||||
|
||||
private function parseIdentifierUntilStopChar(array $splitLine, bool $isRelative = false): int
|
||||
{
|
||||
$this->identifierStream = new IdentifierTokenStream();
|
||||
if ($isRelative) {
|
||||
$this->identifierStream->setRelative();
|
||||
}
|
||||
$currentPosition = 0;
|
||||
$currentIdentifierBody = '';
|
||||
$currentIdentifierCharCount = 0;
|
||||
while (true) {
|
||||
$nextChar = $splitLine[$currentPosition] ?? null;
|
||||
if ($nextChar === null) {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
}
|
||||
return $currentPosition;
|
||||
}
|
||||
$nextTwoChars = $nextChar . ($splitLine[$currentPosition + 1] ?? null);
|
||||
if ($currentPosition > 0
|
||||
&& ($nextChar === ' ' || $nextChar === "\t" || $nextChar === '=' || $nextChar === '<' || $nextChar === '>' || $nextChar === '{' || $nextTwoChars === ':=' || $nextChar === '(')
|
||||
) {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($nextTwoChars === '\\.') {
|
||||
// A quoted dot is part of *this* identifier
|
||||
$currentIdentifierBody .= '.';
|
||||
$currentPosition += 2;
|
||||
$currentIdentifierCharCount++;
|
||||
} elseif ($nextChar === '.') {
|
||||
if ($currentIdentifierCharCount) {
|
||||
$identifierToken = new IdentifierToken(TokenType::T_IDENTIFIER, $currentIdentifierBody);
|
||||
$this->identifierStream->append($identifierToken);
|
||||
$currentIdentifierCharCount = 0;
|
||||
$currentIdentifierBody = '';
|
||||
}
|
||||
$currentPosition++;
|
||||
} else {
|
||||
$currentIdentifierBody .= $nextChar;
|
||||
$currentIdentifierCharCount++;
|
||||
$currentPosition++;
|
||||
}
|
||||
}
|
||||
return $currentPosition;
|
||||
}
|
||||
|
||||
private function parseOperatorFunction(): void
|
||||
{
|
||||
$this->currentLineString = trim(substr($this->currentLineString, 2));
|
||||
if ($this->currentLineString === '') {
|
||||
return;
|
||||
}
|
||||
$functionName = '';
|
||||
$functionNameCharCount = 0;
|
||||
$functionChars = mb_str_split($this->currentLineString, 1, 'UTF-8');
|
||||
while (true) {
|
||||
$nextChar = $functionChars[$functionNameCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
// end of chars
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
if ($functionNameCharCount) {
|
||||
$functionNameToken = new Token(TokenType::T_FUNCTION_NAME, $functionName);
|
||||
$functionNameCharCount++;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
$functionName .= $nextChar;
|
||||
$functionNameCharCount++;
|
||||
}
|
||||
$functionBodyStartPosition = $functionNameCharCount;
|
||||
$functionBodyPart = '';
|
||||
$functionBodyCharCount = 0;
|
||||
$functionValueStream = new TokenStream();
|
||||
$parenthesesLevel = 0;
|
||||
while (true) {
|
||||
$nextChar = $functionChars[$functionBodyStartPosition + $functionBodyCharCount] ?? null;
|
||||
if ($nextChar === null) {
|
||||
return;
|
||||
}
|
||||
if ($nextChar === '(') {
|
||||
// In case of a function call like "appendString(something(somethingelse))"
|
||||
// we shall only stop processing when the last bracket was evaluated.
|
||||
$parenthesesLevel++;
|
||||
}
|
||||
if ($nextChar === ')') {
|
||||
if ($parenthesesLevel > 0) {
|
||||
$parenthesesLevel--;
|
||||
// Continue collecting characters from the (...) argument stream.
|
||||
// Also, ")" will be appended, thus intentionally no "break" occurs.
|
||||
} else {
|
||||
if ($functionBodyCharCount) {
|
||||
$functionValueStream = $this->parseValueForConstants($functionValueStream, $functionBodyPart);
|
||||
$functionBodyCharCount++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$functionBodyPart .= $nextChar;
|
||||
$functionBodyCharCount++;
|
||||
}
|
||||
$this->lineStream->append(
|
||||
(new IdentifierFunctionLine())
|
||||
->setIdentifierTokenStream($this->identifierStream)
|
||||
->setFunctionNameToken($functionNameToken)
|
||||
->setFunctionValueTokenStream($functionValueStream)
|
||||
);
|
||||
// Check for multiline comment
|
||||
$this->currentLineString = implode('', array_slice($functionChars, $functionBodyStartPosition + $functionBodyCharCount + 1));
|
||||
if (mb_strlen($this->currentLineString) >= 1 && str_starts_with($this->currentLineString, '/*')) {
|
||||
$this->ignoreUntilEndOfMultilineComment();
|
||||
}
|
||||
}
|
||||
|
||||
private function parseValueForConstants(TokenStreamInterface $valueStream, string $value): TokenStreamInterface
|
||||
{
|
||||
if (!str_contains($value, '{$')) {
|
||||
$valueStream->append(new Token(TokenType::T_VALUE, $value));
|
||||
return $valueStream;
|
||||
}
|
||||
$splitLine = mb_str_split($value, 1, 'UTF-8');
|
||||
$isInConstant = false;
|
||||
$currentPosition = 0;
|
||||
$currentString = '';
|
||||
$currentStringLength = 0;
|
||||
while (true) {
|
||||
$char = $splitLine[$currentPosition] ?? null;
|
||||
if ($char === null) {
|
||||
if ($currentStringLength) {
|
||||
$valueToken = new Token(TokenType::T_VALUE, $currentString);
|
||||
$valueStream->append($valueToken);
|
||||
}
|
||||
break;
|
||||
}
|
||||
$nextTwoChars = $char . ($splitLine[$currentPosition + 1] ?? '');
|
||||
if ($nextTwoChars === '{$') {
|
||||
$isInConstant = true;
|
||||
if ($currentStringLength) {
|
||||
$valueToken = new Token(TokenType::T_VALUE, $currentString);
|
||||
$valueStream->append($valueToken);
|
||||
}
|
||||
$currentString = '{$';
|
||||
$currentPosition += 2;
|
||||
continue;
|
||||
}
|
||||
if ($isInConstant && $char === '}') {
|
||||
$valueToken = new Token(TokenType::T_CONSTANT, $currentString . '}');
|
||||
if (!$valueStream instanceof ConstantAwareTokenStream) {
|
||||
$valueStream = (new ConstantAwareTokenStream())->setAll($valueStream->getAll());
|
||||
}
|
||||
$valueStream->append($valueToken);
|
||||
$currentPosition++;
|
||||
$currentString = '';
|
||||
$currentStringLength = 0;
|
||||
$isInConstant = false;
|
||||
continue;
|
||||
}
|
||||
$currentPosition++;
|
||||
$currentStringLength++;
|
||||
$currentString .= $char;
|
||||
}
|
||||
return $valueStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* Main implementation of a TokenInterface.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
abstract class AbstractToken implements TokenInterface
|
||||
{
|
||||
protected int $line = 0;
|
||||
protected int $column = 0;
|
||||
|
||||
public function __construct(
|
||||
private readonly TokenType $type,
|
||||
protected readonly string $value,
|
||||
int $line = 0,
|
||||
int $column = 0
|
||||
) {
|
||||
// No constructor property promotion for $line and $column: We don't serialize
|
||||
// these two and want to still default them to 0 (zero) when unserialized.
|
||||
$this->line = $line;
|
||||
$this->column = $column;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not store line and column when structure is serialized to cache.
|
||||
* Not storing $line and $column reduces the cache size by about 1/3 since
|
||||
* we're typically storing *a lot* of tokens.
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
||||
public function getType(): TokenType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getLine(): int
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
public function getColumn(): int
|
||||
{
|
||||
return $this->column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A generic implementation of TokenStreamInterface.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
abstract class AbstractTokenStream implements TokenStreamInterface
|
||||
{
|
||||
/**
|
||||
* @var TokenInterface[]
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
protected int $currentIndex = -1;
|
||||
|
||||
/**
|
||||
* Create a source string from given tokens.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$source = '';
|
||||
$this->reset();
|
||||
while ($token = $this->getNext()) {
|
||||
$source .= $token;
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* When storing to cache, we only store FE relevant properties and skip
|
||||
* irrelevant things. For instance $currentIndex should always initialize
|
||||
* to -1 and does not need to be stored.
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return $this->serialize();
|
||||
}
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result['tokens'] = $this->tokens;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream creation.
|
||||
*/
|
||||
public function append(TokenInterface $token): self
|
||||
{
|
||||
$this->tokens[] = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* We sometimes create a stream but don't add tokens.
|
||||
* This method returns true if tokens have been added.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset current pointer. Typically, call this before iterating with getNext().
|
||||
*/
|
||||
public function reset(): static
|
||||
{
|
||||
$this->currentIndex = -1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next token and raise pointer.
|
||||
*/
|
||||
public function getNext(): ?TokenInterface
|
||||
{
|
||||
$this->currentIndex++;
|
||||
return $this->tokens[$this->currentIndex] ?? null;
|
||||
}
|
||||
|
||||
public function peekNext(): ?TokenInterface
|
||||
{
|
||||
return $this->tokens[$this->currentIndex + 1] ?? null;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
public function setAll(array $tokens): self
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A list of single T_VALUE, T_NEWLINE and T_CONSTANT tokens. This is only created for
|
||||
* LineIdentifierAssignment lines if there is at least one T_CONSTANT token
|
||||
* in the assignment that needs to be evaluated when string'ified by the
|
||||
* AST-builder.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class ConstantAwareTokenStream extends AbstractTokenStream
|
||||
{
|
||||
private ?array $flatConstants = null;
|
||||
|
||||
/**
|
||||
* Set by the AstBuilder to resolve constant values. Never cached.
|
||||
*/
|
||||
public function setFlatConstants(array $flatConstants): void
|
||||
{
|
||||
$this->flatConstants = $flatConstants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a source string from given tokens.
|
||||
* This resolves T_CONSTANT tokens to their value if they exist in $this->flatConstants.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$source = '';
|
||||
$this->reset();
|
||||
while ($token = $this->getNext()) {
|
||||
if ($token->getType() === TokenType::T_CONSTANT) {
|
||||
$token = $this->getConstantValue($this->parseConstantExpression($token->getValue())) ?? $token;
|
||||
}
|
||||
$source .= $token;
|
||||
}
|
||||
$this->reset();
|
||||
return $source;
|
||||
}
|
||||
|
||||
private function getConstantValue(?array $constantNames): ?string
|
||||
{
|
||||
if ($this->flatConstants === null || $constantNames === null) {
|
||||
return null;
|
||||
}
|
||||
foreach ($constantNames as $constantName) {
|
||||
$value = $this->flatConstants[$constantName] ?? null;
|
||||
if ($value !== null) {
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse constant expression, including null coalescing operator into an
|
||||
* array of constant names to look up in order.
|
||||
*
|
||||
* @todo: The tokenization of this constant expression should ideally be moved
|
||||
* into the TypoScript Tokenizer in order to produce a list of multiple tokens
|
||||
* instead of just a T_CONSTANT for the entire body.
|
||||
* This would allow early static syntax analysis of the construct and maybe
|
||||
* detection of invalid and fallback to T_CONSTANT_INVALID that is treated
|
||||
* like T_VALUE and can be detected. Maybe something like this:
|
||||
* TokenType::T_CONSTANT_START "{"
|
||||
* TokenType::T_CONSTANT_END "}"
|
||||
* TokenType::T_CONSTANT_NAME "$foo.bar"
|
||||
* TokenType::T_CONSTANT_OPERATOR_NULL_COALESCE " ?? "
|
||||
* TokenType::T_CONSTANT_INVALID "{$foo ?? bar}" (missing $ before bar)
|
||||
*/
|
||||
private function parseConstantExpression(string $constantExpression): ?array
|
||||
{
|
||||
$innerExpression = ltrim(rtrim($constantExpression, '}'), '{');
|
||||
$tokenValues = GeneralUtility::trimExplode(' ?? ', $innerExpression, true);
|
||||
if ($tokenValues === []) {
|
||||
return null;
|
||||
}
|
||||
$tokenValueNames = [];
|
||||
foreach ($tokenValues as $tokenValue) {
|
||||
if (!str_starts_with($tokenValue, '$')) {
|
||||
return null;
|
||||
}
|
||||
$tokenValueNames[] = substr($tokenValue, 1);
|
||||
}
|
||||
return $tokenValueNames;
|
||||
}
|
||||
}
|
||||
@@ -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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A special token if this token is a T_IDENTIFIER token:
|
||||
* With a line like "foo = bar", "foo" is created as TokenIdentifier TokenInterface
|
||||
* (as opposed to Token) having a TokenType::T_IDENTIFIER token.
|
||||
* The only difference to all other tokens is that TokenIdentifier tokens
|
||||
* quote any "." (dots) in their value with a backslash when output. This is
|
||||
* mostly used in backend when rendering source of TokenLine's.
|
||||
*
|
||||
* Note we do *not* explicitly check if TokenType::T_IDENTIFIER is given in
|
||||
* __construct() at the moment for performance reasons and inheritance considerations.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierToken extends AbstractToken
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return str_replace('.', '\.', $this->value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A list of single identifier (!) tokens: TokenType::T_IDENTIFIER, and only of those.
|
||||
*
|
||||
* This is used in TS lines that know certain parts have to be lists of identifier tokens only.
|
||||
* For instance a LineIdentifierAssignment "foo.bar = barValue" return this stream for getIdentifierTokenStream():
|
||||
* The left side of an assignment line is a list of identifier tokens.
|
||||
*
|
||||
* Identifiers can be "relative" on the right side for "<" (LineIdentifierCopy) and "=<" (LineIdentifierReference).
|
||||
* Examples are "foo.bar < .baz" and "foo.bar =< .baz". These are identified by having a "." (dot) at the beginning
|
||||
* on the right side. For these places, the toggle "relative" is set to true for the AST-builder to look for relative
|
||||
* copy and copy-reference. The generic example are "relative" references in TS menus: 'RO < .NO'
|
||||
*
|
||||
* For example, with "foo.bar < baz", the Tokenizer creates a LineIdentifierCopy line, having a TokenStreamIdentifier
|
||||
* list of the T_IDENTIFIER tokens for 'foo' and 'bar' for getIdentifierTokenStream(), plus a TokenStreamIdentifier list
|
||||
* of T_IDENTIFIER tokens for 'baz' for getValueTokenStream().
|
||||
*
|
||||
* Note identifier streams on the left side (foo.bar = ...) are never relative, this toggle is true for "<" and "=<" only.
|
||||
*
|
||||
* Lines that know they can only return TokenStreamIdentifier's - they are more specific than just TokenStream, are
|
||||
* type-hinted as such. For instance getIdentifierTokenStream() type hints TokenStreamIdentifier.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class IdentifierTokenStream extends AbstractTokenStream
|
||||
{
|
||||
private bool $relative = false;
|
||||
|
||||
/**
|
||||
* When rendering a source string from multiple identifiers, dots between single identifiers need to be added again.
|
||||
* This is used in RootNode->toArray() to create that insane '< lib.whatever' as value when using the
|
||||
* reference operator: "foo =< lib.whatever". See ContentObjectRenderer cObjGetSingle() and mergeTSRef().
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$source = [];
|
||||
$this->reset();
|
||||
while ($token = $this->getNext()) {
|
||||
$source[] = (string)$token;
|
||||
}
|
||||
$source = implode('.', $source);
|
||||
if ($this->relative) {
|
||||
$source = '.' . $source;
|
||||
}
|
||||
return $source;
|
||||
}
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result = parent::serialize();
|
||||
if ($this->isRelative()) {
|
||||
$result['relative'] = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a token to the stream.
|
||||
*/
|
||||
public function append(TokenInterface $token): self
|
||||
{
|
||||
if ($token->getType() !== TokenType::T_IDENTIFIER) {
|
||||
throw new \LogicException(
|
||||
'Trying to add a token of type TokenType::' . $token->getType()->name . ' to class TokenStreamIdentifier, but only TokenType::T_IDENTIFIERS are allowed.',
|
||||
1655138907
|
||||
);
|
||||
}
|
||||
$this->tokens[] = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This identifier token stream is relative! There is a dot on the right side of something like "foo.bar < .baz"
|
||||
*/
|
||||
public function setRelative(): self
|
||||
{
|
||||
$this->relative = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this identifier stream is relative to given context.
|
||||
*/
|
||||
public function isRelative(): bool
|
||||
{
|
||||
return $this->relative;
|
||||
}
|
||||
}
|
||||
@@ -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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A casual token created from TypoScript source:
|
||||
* When having a TypoScript line like "# a comment", then a LineComment
|
||||
* is created having a token "T_COMMENT_ONELINE_HASH" and value "# a comment" as
|
||||
* assigned TokenStream.
|
||||
* See TokenType for on overview on which TokenTypes can exist.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class Token extends AbstractToken {}
|
||||
@@ -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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A readonly token: Each line of TypoScript is split into a list of lines consisting of
|
||||
* tokens by the tokenizers.
|
||||
*
|
||||
* As example, a "foo.bar = baz" line creates a LineIdentifierAssignment line, having
|
||||
* TokenType::T_IDENTIFIER 'foo', plus TokenType::T_IDENTIFIER 'bar' as TokenStream for
|
||||
* LineIdentifierAssignment->getIdentifierTokenStream(), plus a TokenType::T_VALUE 'baz'
|
||||
* as LineIdentifierAssignment->getValueTokenStream().
|
||||
*
|
||||
* We have two different Token implementations: The casual "Token" class for everything, plus
|
||||
* the "TokenIdentifier" class for identifier tokens. Identifier tokens are those "left" of
|
||||
* for instance an assignment like "foo.bar = baz" ("foo" and "bar" are TokenIdentifier instances),
|
||||
* and also on the right side when using expression with "<" and "=<" operator: Example "foo.bar < baz":
|
||||
* "baz" is an instance of a TokenIdentifier ("foo" and "bar" as well).
|
||||
*
|
||||
* The reason to have two implementations is that TokenIdentifier needs to be handled slightly
|
||||
* different when cast to string: For identifiers, all "." (dots) within a single identifier token
|
||||
* need to be quoted with "\" (backslash), to not confuse the parser. The classic use-case is having dots in
|
||||
* FlexForm identifiers for PageTS:
|
||||
* "foo.bar\.baz.foobar = value" - three identifier tokens (not four!): "foo", "bar.baz" and "foobar".
|
||||
* So the difference between "TokenIdentifier" and "Token" is just that "TokenIdentifier" quotes dots
|
||||
* in its value when string'ified, while Token does not and __toString() on Token simply says ->getValue().
|
||||
*
|
||||
* Multiple tokens are encapsulated in TokenStreamInterface. TokenStreamInterface has a __toString()
|
||||
* method as well, which calls __toString() on all assigned tokens. This way, a TokenIdentifier will
|
||||
* do its quoting magic, and casual Token instances return their value.
|
||||
*
|
||||
* The idea is here that TokenStreams are cast to string quite often. For instance, an assignment line
|
||||
* like "foo = bar" creates a token stream of one token for the right side (things after "="):
|
||||
* A T_VALUE Token instance with value "bar". The AST builder then at some point needs to resolve this
|
||||
* TokenStream to string. This will directly call __toString on token "bar", and does not deal with quoting,
|
||||
* since its no TokenIdentifier and just a Token.
|
||||
*
|
||||
* Note on getLine() and getColumn(): These two represent the position of a token in the source file:
|
||||
* We start counting at 0 (zero): The first token on the first line is line 0, column 0.
|
||||
* Only the LosslessTokenizer sets these, it's too expensive and of no relevance for the LossyTokenizer
|
||||
* that is used for instance in FE TS tokenizing. That's why these two properties are optional
|
||||
* and 0 (zero) by default.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
interface TokenInterface
|
||||
{
|
||||
public function __toString(): string;
|
||||
public function getType(): TokenType;
|
||||
public function getValue(): string;
|
||||
public function getLine(): int;
|
||||
public function getColumn(): int;
|
||||
}
|
||||
@@ -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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A list of single tokens. These are typically used in TokenLines: A TypoScript
|
||||
* line consists of one or more streams of tokens, depending on the line type.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
final class TokenStream extends AbstractTokenStream {}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* A generic stream of tokens used in single LineInterface lines.
|
||||
*
|
||||
* The tokenizers create these streams for various lists of tokens, the generic
|
||||
* implementation is class TokenStream. For lists of identifier tokens the special
|
||||
* class TokenStreamIdentifier is created.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
interface TokenStreamInterface
|
||||
{
|
||||
/**
|
||||
* Create a source string from given tokens.
|
||||
*/
|
||||
public function __toString(): string;
|
||||
|
||||
/**
|
||||
* Stream creation.
|
||||
*/
|
||||
public function append(TokenInterface $token): self;
|
||||
|
||||
/**
|
||||
* We sometimes create a stream but don't add tokens.
|
||||
* This method returns true if tokens have been added.
|
||||
*/
|
||||
public function isEmpty(): bool;
|
||||
|
||||
/**
|
||||
* Reset current pointer. Typically, call this before iterating with getNext().
|
||||
*/
|
||||
public function reset(): self;
|
||||
|
||||
/**
|
||||
* Get next token and raise pointer.
|
||||
*/
|
||||
public function getNext(): ?TokenInterface;
|
||||
|
||||
/**
|
||||
* Get next token but do not raise pointer.
|
||||
*/
|
||||
public function peekNext(): ?TokenInterface;
|
||||
|
||||
/**
|
||||
* Only used internally when one Stream is transferred to another,
|
||||
* in particular when a TokenStream is turned into TokenStreamConstantAware.
|
||||
*
|
||||
* @return TokenInterface[]
|
||||
*/
|
||||
public function getAll(): array;
|
||||
|
||||
/**
|
||||
* Only used internally when one Stream is transferred to another,
|
||||
* in particular when a TokenStream is turned into TokenStreamConstantAware.
|
||||
*
|
||||
* @param TokenInterface[] $tokens
|
||||
*/
|
||||
public function setAll(array $tokens): self;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Tokenizer\Token;
|
||||
|
||||
/**
|
||||
* Each TokenInterface instance is a type of this Enum.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
enum TokenType: int
|
||||
{
|
||||
case T_NONE = 0; // tokenizer internal handling
|
||||
|
||||
case T_IDENTIFIER = 100; // single word left of an operator. 'foo.bar' are two identifiers
|
||||
case T_VALUE = 200; // right side of an assignment, does not contain line breaks, also used as 'comment' body
|
||||
|
||||
case T_OPERATOR_ASSIGNMENT = 300; // '='
|
||||
case T_OPERATOR_REFERENCE = 301; // '=<'
|
||||
case T_OPERATOR_COPY = 302; // '<'
|
||||
case T_OPERATOR_UNSET = 303; // '>'
|
||||
case T_OPERATOR_FUNCTION = 304; // ':='
|
||||
case T_OPERATOR_ASSIGNMENT_MULTILINE_START = 310; // '('
|
||||
case T_OPERATOR_ASSIGNMENT_MULTILINE_STOP = 311; // ')'
|
||||
|
||||
case T_BLOCK_START = 400; // '{'
|
||||
case T_BLOCK_STOP = 401; // '}'
|
||||
|
||||
case T_DOT = 500; // '.' identifier separator
|
||||
|
||||
case T_BLANK = 600; // list of ' ' and "\t"
|
||||
|
||||
case T_NEWLINE = 700; // "\n" or "\r\n"
|
||||
|
||||
case T_COMMENT_ONELINE_HASH = 800; // '#...'
|
||||
case T_COMMENT_ONELINE_DOUBLESLASH = 801; // '//'
|
||||
case T_COMMENT_MULTILINE_START = 802; // '/*'
|
||||
case T_COMMENT_MULTILINE_STOP = 803; // '*/'
|
||||
|
||||
case T_FUNCTION_NAME = 900; // 'addToList' and others
|
||||
case T_FUNCTION_VALUE_START = 901; // '(' after T_FUNCTION_NAME
|
||||
case T_FUNCTION_VALUE_STOP = 902; // ')' after T_FUNCTION_NAME
|
||||
|
||||
case T_CONDITION_START = 1000; // '[' at start of line
|
||||
case T_CONDITION_STOP = 1001; // ']' after '[' in same line, body is a T_VALUE
|
||||
case T_CONDITION_ELSE = 1002; // 'ELSE' surrounded by '[' and ']'
|
||||
case T_CONDITION_END = 1003; // 'END' surrounded by '[' and ']'
|
||||
case T_CONDITION_GLOBAL = 1004; // 'GLOBAL' surrounded by '[' and ']'
|
||||
|
||||
case T_CONSTANT = 1100; // '{$...}'
|
||||
|
||||
case T_IMPORT_KEYWORD = 1200; // '@import'
|
||||
case T_IMPORT_START = 1201; // ''' (tick) or '"' (doubletick) after @import
|
||||
case T_IMPORT_STOP = 1202; // ''' (tick) or '"' (doubletick) after T_IMPORT_START
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Tokenizer;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
|
||||
/**
|
||||
* A lossless tokenizer for TypoScript syntax.
|
||||
*
|
||||
* tokenize() creates a stream of LineInterface objects from a TypoScript string, each line
|
||||
* contains the important streams or tokens of a single line.
|
||||
*
|
||||
* There are two tokenizer implementations:
|
||||
* - LossyTokenizer: This one skip all invalid lines and comments and everything that is
|
||||
* not needed for AST building.
|
||||
* - LosslessTokenizer: This one creates a stream of lines useful for backend template module
|
||||
* to elaborate on details and failures in TypoScript.
|
||||
*
|
||||
* The tokenizer *does not* parse conditions or includes itself (no file / db lookups),
|
||||
* this is part of the IncludeTree parser.
|
||||
*
|
||||
* @internal: Internal tokenizer structure.
|
||||
*/
|
||||
interface TokenizerInterface
|
||||
{
|
||||
public function tokenize(string $source): LineStream;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Helper class to manage and convert TypoScript into differently shaped arrays.
|
||||
* Also contains the functionality in TypoScript called "optionSplit".
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class TypoScriptService
|
||||
{
|
||||
/**
|
||||
* Removes all trailing dots recursively from TS settings array
|
||||
*
|
||||
* Extbase converts the "classical" TypoScript (with trailing dot) to a format without trailing dot,
|
||||
* to be more future-proof and not to have any conflicts with Fluid object accessor syntax.
|
||||
*
|
||||
* @param array<string|int, mixed> $typoScriptArray for example `['foo' => 'TEXT', 'foo.' => ['bar' => 'baz']]`
|
||||
* @return array<string|int, mixed> for example `['foo' => ['_typoScriptNodeValue' => 'TEXT', 'bar' => 'baz']]`
|
||||
* @internal Avoid using this method. This has been invented for Extbase, which decided to move TypoScript
|
||||
* arrays around in just another different way.
|
||||
*/
|
||||
public function convertTypoScriptArrayToPlainArray(array $typoScriptArray): array
|
||||
{
|
||||
foreach ($typoScriptArray as $key => $value) {
|
||||
if (str_ends_with((string)$key, '.')) {
|
||||
$keyWithoutDot = substr((string)$key, 0, -1);
|
||||
$typoScriptNodeValue = $typoScriptArray[$keyWithoutDot] ?? null;
|
||||
if (is_array($value)) {
|
||||
$typoScriptArray[$keyWithoutDot] = $this->convertTypoScriptArrayToPlainArray($value);
|
||||
if ($typoScriptNodeValue !== null) {
|
||||
$typoScriptArray[$keyWithoutDot]['_typoScriptNodeValue'] = $typoScriptNodeValue;
|
||||
}
|
||||
unset($typoScriptArray[$key]);
|
||||
} else {
|
||||
$typoScriptArray[$keyWithoutDot] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with Typoscript the old way (with dot).
|
||||
*
|
||||
* Extbase converts the "classical" TypoScript (with trailing dot) to a format without trailing dot,
|
||||
* to be more future-proof and not to have any conflicts with Fluid object accessor syntax.
|
||||
* However, if you want to call legacy TypoScript objects, you somehow need the "old" syntax (because this is what TYPO3 is used to).
|
||||
* With this method, you can convert the extbase TypoScript to classical TYPO3 TypoScript which is understood by the rest of TYPO3.
|
||||
*
|
||||
* @param array $plainArray A TypoScript Array with Extbase Syntax (without dot but with _typoScriptNodeValue)
|
||||
* @return array Array with TypoScript as usual (with dot)
|
||||
* @internal Avoid using this method. This has been invented for Extbase, which decided to move TypoScript
|
||||
* arrays around in just another different way.
|
||||
*/
|
||||
public function convertPlainArrayToTypoScriptArray(array $plainArray): array
|
||||
{
|
||||
$typoScriptArray = [];
|
||||
foreach ($plainArray as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (isset($value['_typoScriptNodeValue'])) {
|
||||
$typoScriptArray[$key] = $value['_typoScriptNodeValue'];
|
||||
unset($value['_typoScriptNodeValue']);
|
||||
}
|
||||
$typoScriptArray[$key . '.'] = $this->convertPlainArrayToTypoScriptArray($value);
|
||||
} else {
|
||||
$typoScriptArray[$key] = $value ?? '';
|
||||
}
|
||||
}
|
||||
return $typoScriptArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the "optionSplit" feature in TypoScript (used eg. for MENU objects)
|
||||
* What it does is to split the incoming TypoScript array so that the values are exploded by certain
|
||||
* strings ("||" and "|*|") and each part distributed into individual TypoScript arrays with a similar structure,
|
||||
* but individualized values.
|
||||
* The concept is known as "optionSplit" and is rather advanced to handle but quite powerful, in particular
|
||||
* for creating menus in TYPO3.
|
||||
*
|
||||
* @param array $originalConfiguration A TypoScript array
|
||||
* @param int $splitCount The number of items for which to generate individual TypoScript arrays
|
||||
* @return array The individualized TypoScript array.
|
||||
*/
|
||||
public function explodeConfigurationForOptionSplit(array $originalConfiguration, int $splitCount): array
|
||||
{
|
||||
$finalConfiguration = [];
|
||||
if (!$splitCount) {
|
||||
return $finalConfiguration;
|
||||
}
|
||||
// Initialize output to carry at least the keys
|
||||
for ($aKey = 0; $aKey < $splitCount; $aKey++) {
|
||||
$finalConfiguration[$aKey] = [];
|
||||
}
|
||||
// Recursive processing of array keys
|
||||
foreach ($originalConfiguration as $cKey => $val) {
|
||||
if (is_array($val)) {
|
||||
$tempConf = $this->explodeConfigurationForOptionSplit($val, $splitCount);
|
||||
foreach ($tempConf as $aKey => $val2) {
|
||||
$finalConfiguration[$aKey][$cKey] = $val2;
|
||||
}
|
||||
} elseif (is_string($val)) {
|
||||
// Splitting of all values on this level of the TypoScript object tree:
|
||||
if ($cKey === 'noTrimWrap' || (!str_contains($val, '|*|') && !str_contains($val, '||'))) {
|
||||
for ($aKey = 0; $aKey < $splitCount; $aKey++) {
|
||||
$finalConfiguration[$aKey][$cKey] = $val;
|
||||
}
|
||||
} else {
|
||||
$main = explode('|*|', $val);
|
||||
$lastC = 0;
|
||||
$middleC = 0;
|
||||
$firstC = 0;
|
||||
if ($main[0]) {
|
||||
$first = explode('||', $main[0]);
|
||||
$firstC = count($first);
|
||||
}
|
||||
$middle = [];
|
||||
if (!empty($main[1])) {
|
||||
$middle = explode('||', $main[1]);
|
||||
$middleC = count($middle);
|
||||
}
|
||||
$last = [];
|
||||
$value = '';
|
||||
if (!empty($main[2])) {
|
||||
$last = explode('||', $main[2]);
|
||||
$lastC = count($last);
|
||||
$value = $last[0];
|
||||
}
|
||||
for ($aKey = 0; $aKey < $splitCount; $aKey++) {
|
||||
if ($firstC && isset($first[$aKey])) {
|
||||
$value = $first[$aKey];
|
||||
} elseif ($middleC) {
|
||||
$value = $middle[($aKey - $firstC) % $middleC];
|
||||
}
|
||||
if ($lastC && $lastC >= $splitCount - $aKey) {
|
||||
$value = $last[$lastC - ($splitCount - $aKey)];
|
||||
}
|
||||
$finalConfiguration[$aKey][$cKey] = trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $finalConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten TypoScript label array; converting a hierarchical array into a flat
|
||||
* array with the keys separated by dots.
|
||||
*
|
||||
* Example Input: array('k1' => array('subkey1' => 'val1'))
|
||||
* Example Output: array('k1.subkey1' => 'val1')
|
||||
*
|
||||
* @param array $labelValues Hierarchical array of labels
|
||||
* @param string $parentKey the name of the parent key in the recursion; is only needed for recursion.
|
||||
* @return array flattened array of labels.
|
||||
*/
|
||||
public function flattenTypoScriptLabelArray(array $labelValues, string $parentKey = ''): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($labelValues as $key => $labelValue) {
|
||||
if (!empty($parentKey)) {
|
||||
if ($key === '_typoScriptNodeValue') {
|
||||
$key = $parentKey;
|
||||
} else {
|
||||
$key = $parentKey . '.' . $key;
|
||||
}
|
||||
}
|
||||
if (is_array($labelValue)) {
|
||||
$labelValue = $this->flattenTypoScriptLabelArray($labelValue, $key);
|
||||
$result = array_merge($result, $labelValue);
|
||||
} else {
|
||||
$result[$key] = $labelValue;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\StringTreeBuilder;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface;
|
||||
|
||||
/**
|
||||
* A factory to create the AST object tree for a given TypoScript snippet.
|
||||
*
|
||||
* This is used by some consumers in the core that parse a TypoScript a-like
|
||||
* syntax that is not Frontend TypoScript and TsConfig directly.
|
||||
*/
|
||||
final readonly class TypoScriptStringFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ContainerInterface $container,
|
||||
private TokenizerInterface $tokenizer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a single string and support imports and conditions, cache optionally.
|
||||
*
|
||||
* @param non-empty-string $name A name used as cache identifier, [a-z,A-Z,-] only
|
||||
*/
|
||||
public function parseFromStringWithIncludes(string $name, string $typoScript): RootNode
|
||||
{
|
||||
$cacheManager = $this->container->get(CacheManager::class);
|
||||
/** @var PhpFrontend $cache */
|
||||
$cache = $cacheManager->getCache('typoscript');
|
||||
$stringTreeBuilder = $this->container->get(StringTreeBuilder::class);
|
||||
$includeTree = $stringTreeBuilder->getTreeFromString($name, $typoScript, $this->tokenizer, $cache);
|
||||
$includeTreeTraverserConditionVerdictAware = new IncludeTreeTraverser();
|
||||
$astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
|
||||
$includeTreeTraverserConditionVerdictAware->traverse($includeTree, [$astBuilderVisitor]);
|
||||
return $astBuilderVisitor->getAst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single string *not* supporting imports, conditions and caching.
|
||||
* Detail method used in install tool and in a couple of other special cases.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function parseFromString(string $typoScript, AstBuilderInterface $astBuilder): RootNode
|
||||
{
|
||||
$lineStream = $this->tokenizer->tokenize($typoScript);
|
||||
return $astBuilder->build($lineStream, new RootNode());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user