TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,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;
}
}
}
+109
View File
@@ -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;
}
}
+23
View File
@@ -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;
}
}
+109
View File
@@ -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;
}