TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user