TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:41 +02:00
commit 621587f02d
50 changed files with 4650 additions and 0 deletions
@@ -0,0 +1,317 @@
<?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\Tstemplate\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\VisibilityAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Abstract class with helper methods for single 3rd level Template module controllers.
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
abstract class AbstractTemplateModuleController
{
protected IconFactory $iconFactory;
protected UriBuilder $uriBuilder;
protected ConnectionPool $connectionPool;
protected SiteFinder $siteFinder;
protected ComponentFactory $componentFactory;
private DataHandler $dataHandler;
private TcaSchemaFactory $tcaSchemaFactory;
public function injectIconFactory(IconFactory $iconFactory): void
{
$this->iconFactory = $iconFactory;
}
public function injectUriBuilder(UriBuilder $uriBuilder)
{
$this->uriBuilder = $uriBuilder;
}
public function injectConnectionPool(ConnectionPool $connectionPool)
{
$this->connectionPool = $connectionPool;
}
public function injectDataHandler(DataHandler $dataHandler)
{
$this->dataHandler = $dataHandler;
}
public function injectSiteFinder(SiteFinder $siteFinder)
{
$this->siteFinder = $siteFinder;
}
public function injectTcaSchemaFactory(TcaSchemaFactory $tcaSchemaFactory)
{
$this->tcaSchemaFactory = $tcaSchemaFactory;
}
public function injectComponentFactory(ComponentFactory $componentFactory): void
{
$this->componentFactory = $componentFactory;
}
/**
* Action shared by info/modify ond constant editor to create a new "extension template"
*/
protected function createExtensionTemplateAction(ServerRequestInterface $request, string $redirectTarget): ResponseInterface
{
$pageUid = (int)($request->getQueryParams()['id'] ?? 0);
if ($pageUid === 0) {
throw new \RuntimeException('No proper page uid given', 1661333864);
}
$recordData['sys_template']['NEW'] = [
'pid' => $pageUid,
'title' => '+ext',
];
$this->dataHandler->start($recordData, []);
$this->dataHandler->process_datamap();
return new RedirectResponse($this->uriBuilder->buildUriFromRoute($redirectTarget, ['id' => $pageUid]));
}
/**
* Action shared by info/modify ond constant editor to create a new "site template"
*/
protected function createNewWebsiteTemplateAction(ServerRequestInterface $request, string $redirectTarget): ResponseInterface
{
$languageService = $this->getLanguageService();
$pageUid = (int)($request->getQueryParams()['id'] ?? 0);
if ($pageUid === 0) {
throw new \RuntimeException('No proper page uid given', 1661333863);
}
$recordData['sys_template']['NEW'] = [
'pid' => $pageUid,
'title' => $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang.xlf:noRecordFound.createRootTypoScriptRecord.title.placeholder'),
'sorting' => 0,
'root' => 1,
'clear' => 3,
'config' => "\n"
. "# Default PAGE object:\n"
. "page = PAGE\n"
. "page.10 = TEXT\n"
. "page.10.value = HELLO WORLD!\n",
];
$this->dataHandler->start($recordData, []);
$this->dataHandler->process_datamap();
return new RedirectResponse($this->uriBuilder->buildUriFromRoute($redirectTarget, ['id' => $pageUid]));
}
protected function addPreviewButtonToDocHeader(ModuleTemplate $view, array $pageRecord): void
{
$previewUriBuilder = PreviewUriBuilder::create($pageRecord);
if ($previewUriBuilder->isPreviewable()) {
$view->addButtonToButtonBar($this->componentFactory->createViewButton(
$previewUriBuilder
->withRootLine(BackendUtility::BEgetRootLine($pageRecord['uid']))
->buildDispatcherDataAttributes() ?? []
), ButtonBar::BUTTON_POSITION_LEFT, 99);
}
}
protected function addShortcutButtonToDocHeader(ModuleTemplate $view, string $moduleIdentifier, array $pageInfo, int $pageUid, string $moduleTitle): void
{
$shortcutTitle = sprintf(
'%s: %s [%d]',
$moduleTitle,
BackendUtility::getRecordTitle('pages', $pageInfo),
$pageUid
);
$view->getDocHeaderComponent()->setShortcutContext(
$moduleIdentifier,
$shortcutTitle,
['id' => $pageUid]
);
}
/**
* Get the closest page row that has a template up in rootline
*/
protected function getClosestAncestorPageWithTemplateRecord(int $pageId): array
{
$rootLine = BackendUtility::BEgetRootLine($pageId);
foreach ($rootLine as $rootlineNode) {
if ($this->getFirstTemplateRecordOnPage((int)$rootlineNode['uid'])) {
return $rootlineNode;
}
}
return [];
}
protected function getScopedRootline(SiteInterface $site, array $fullRootLine): array
{
if (!$site instanceof Site) {
return $fullRootLine;
}
if (!$site->isTypoScriptRoot()) {
return $fullRootLine;
}
$rootLineUntilSite = [];
foreach ($fullRootLine as $index => $rootlinePage) {
$rootlinePageId = (int)($rootlinePage['uid'] ?? 0);
$rootLineUntilSite[$index] = $rootlinePage;
if ($rootlinePageId === $site->getRootPageId()) {
break;
}
}
return $rootLineUntilSite;
}
/**
* Get an array of all template records on a page.
*/
protected function getAllTemplateRecordsOnPage(int $pageId): array
{
if (!$pageId) {
return [];
}
$templateRecords = [];
try {
$site = $this->siteFinder->getSiteByRootPageId($pageId);
if ($site->isTypoScriptRoot()) {
$typoScript = $site->getTypoScript();
$templateRecords[] = [
'type' => 'site',
'pid' => $pageId,
'constants' => $typoScript->constants ?? '',
'config' => $typoScript->setup ?? '',
'root' => 1,
'clear' => 1,
'sorting' => -1,
'uid' => -1,
'site' => $site,
'title' => $site->getConfiguration()['websiteTitle'] ?? '',
];
}
} catch (SiteNotFoundException) {
// ignore
}
$result = $this->getTemplateQueryBuilder($pageId)->executeQuery();
while ($row = $result->fetchAssociative()) {
$templateRecords[] = [...$row, 'type' => 'sys_template'];
}
return $templateRecords;
}
/**
* Get a single sys_template record attached to a single page.
* If multiple template records are on this page, the first (order by sorting)
* record will be returned, unless a specific template uid is specified via $templateUid
*
* @param int $pageId The pid to select sys_template records from
* @param int $templateUid Optional template uid
* @return array<string,mixed>|false Returns the template record or false if none was found
*/
protected function getFirstTemplateRecordOnPage(int $pageId, int $templateUid = 0): array|false
{
if (empty($pageId)) {
return false;
}
$queryBuilder = $this->getTemplateQueryBuilder($pageId)->setMaxResults(1);
if ($templateUid) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($templateUid, Connection::PARAM_INT))
);
}
return $queryBuilder->executeQuery()->fetchAssociative();
}
/**
* Helper method to prepare the query builder for getting sys_template records from a given pid.
*/
protected function getTemplateQueryBuilder(int $pid): QueryBuilder
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryBuilder->select('*')
->from('sys_template')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT))
);
$schema = $this->tcaSchemaFactory->has('sys_template')
? $this->tcaSchemaFactory->get('sys_template')
: null;
if ($schema && $schema->hasCapability(TcaSchemaCapability::SortByField)) {
$queryBuilder
->orderBy($schema->getCapability(TcaSchemaCapability::SortByField)->getFieldName());
}
return $queryBuilder;
}
/**
* Create a VisibilityAspect that simulates frontend-like behavior:
* hidden templates and templates outside their scheduled time window
* are excluded, as they would be in the frontend.
*/
protected function createVisibilityAspect(): VisibilityAspect
{
// For the context of the TypoScript management backend, we want to
// edit TypoScript records that are hidden. But in a backend submodule like
// the ActiveTypoScriptController / TemplateAnalyzerController, only
// non-hidden records with matching time constraints should be evaluated,
// just like in the frontend.
return new VisibilityAspect(
includeHiddenPages: true,
includeHiddenContent: false,
includeDeletedRecords: false,
includeScheduledRecords: false,
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,495 @@
<?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\Tstemplate\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstNodeFinderVisitor;
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstSortChildrenVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeCommentAwareAstBuilderVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionAggregatorVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionEnforcerVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* The "TypoScript -> Active TypoScript" Backend module
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
final class ActiveTypoScriptController extends AbstractTemplateModuleController
{
public function __construct(
private readonly ContainerInterface $container,
private readonly ModuleTemplateFactory $moduleTemplateFactory,
private readonly FlashMessageService $flashMessageService,
private readonly SysTemplateRepository $sysTemplateRepository,
private readonly SysTemplateTreeBuilder $treeBuilder,
) {}
/**
* Default view renders options, constant and setup conditions, constant and setup tree.
*/
public function indexAction(ServerRequestInterface $request): ResponseInterface
{
$backendUser = $this->getBackendUser();
$languageService = $this->getLanguageService();
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$moduleData = $request->getAttribute('moduleData');
$pageUid = (int)($queryParams['id'] ?? 0);
if ($pageUid === 0) {
// Redirect to template record overview if on page 0.
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
// @todo: Switch to BU::BEgetRootLine($pageUid, '', true) as in PageTsConfig? Similar in other controllers and actions.
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$site = $request->getAttribute('site');
$rootLine = $this->getScopedRootline($site, $rootLine);
// Template selection handling for this page
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($parsedBody['selectedTemplate'] ?? $selectedTemplateFromModuleData[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if (($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0) !== $selectedTemplateUid) {
$selectedTemplateFromModuleData[$pageUid] = $selectedTemplateUid;
$moduleData->set('selectedTemplatePerPage', $selectedTemplateFromModuleData);
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$templateTitle = '';
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$templateTitle = $templateRow['title'];
}
}
// Force boolean toggles to bool and init further get/post vars
if ($moduleData->clean('sortAlphabetically', [true, false])) {
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$sortAlphabetically = $moduleData->get('sortAlphabetically');
if ($moduleData->clean('displayConstantSubstitutions', [true, false])) {
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$displayConstantSubstitutions = $moduleData->get('displayConstantSubstitutions');
if ($moduleData->clean('displayComments', [true, false])) {
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$displayComments = $moduleData->get('displayComments');
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
// Build the constant include tree
$constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, new LosslessTokenizer(), $site);
// Set enabled conditions in constant include tree
$constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, $parsedBody);
$conditionEnforcerVisitor = new IncludeTreeConditionEnforcerVisitor();
$conditionEnforcerVisitor->setEnabledConditions(array_column(array_filter($constantConditions, static fn($condition) => $condition['active']), 'value'));
$treeTraverser = new ConditionVerdictAwareIncludeTreeTraverser();
$treeTraverserVisitors = [];
$treeTraverserVisitors[] = $conditionEnforcerVisitor;
$constantAstBuilderVisitor = $this->container->get(IncludeTreeCommentAwareAstBuilderVisitor::class);
$treeTraverserVisitors[] = $constantAstBuilderVisitor;
$treeTraverser->traverse($constantIncludeTree, $treeTraverserVisitors);
$constantAst = $constantAstBuilderVisitor->getAst();
$constantAst->setIdentifier('TypoScript constants');
if ($sortAlphabetically) {
$astTraverser = new AstTraverser();
$astTraverser->traverse($constantAst, [new AstSortChildrenVisitor()]);
}
// Flatten constant AST. Needed for setup condition display and setup AST constant substitution.
$flattenedConstants = $constantAst->flatten();
// Build the setup include tree
$setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, new LosslessTokenizer(), $site);
// Set enabled conditions in setup include tree and let it handle constant substitutions in setup conditions.
$setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, $parsedBody, $flattenedConstants);
$conditionEnforcerVisitor = new IncludeTreeConditionEnforcerVisitor();
$conditionEnforcerVisitor->setEnabledConditions(array_column(array_filter($setupConditions, static fn($condition) => $condition['active']), 'value'));
$treeTraverser = new ConditionVerdictAwareIncludeTreeTraverser();
$treeTraverserVisitors = [];
$treeTraverserVisitors[] = $conditionEnforcerVisitor;
$setupAstBuilderVisitor = $this->container->get(IncludeTreeCommentAwareAstBuilderVisitor::class);
$setupAstBuilderVisitor->setFlatConstants($flattenedConstants);
$treeTraverserVisitors[] = $setupAstBuilderVisitor;
$treeTraverser->traverse($setupIncludeTree, $treeTraverserVisitors);
// Build the setup AST
$setupAst = $setupAstBuilderVisitor->getAst();
$setupAst->setIdentifier('TypoScript setup');
if ($sortAlphabetically) {
$astTraverser = new AstTraverser();
$astTraverser->traverse($setupAst, [new AstSortChildrenVisitor()]);
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_active.xlf:submodule.title'));
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'templateTitle' => $templateTitle,
'selectedTemplateUid' => $selectedTemplateUid,
'pageUid' => $pageUid,
'allTemplatesOnPage' => $allTemplatesOnPage,
'sortAlphabetically' => $sortAlphabetically,
'displayConstantSubstitutions' => $displayConstantSubstitutions,
'displayComments' => $displayComments,
'constantConditions' => $constantConditions,
'constantConditionsActiveCount' => count(array_filter($constantConditions, static fn($condition) => $condition['active'])),
'constantAst' => $constantAst,
'setupConditions' => $setupConditions,
'setupConditionsActiveCount' => count(array_filter($setupConditions, static fn($condition) => $condition['active'])),
'setupAst' => $setupAst,
]);
return $view->renderResponse('ActiveMain');
}
/**
* Edit a single property. Linked from "show" view when clicking a property.
*/
public function editAction(ServerRequestInterface $request): ResponseInterface
{
$languageService = $this->getLanguageService();
$queryParams = $request->getQueryParams();
$currentModule = $request->getAttribute('module');
$moduleData = $request->getAttribute('moduleData');
$pageUid = (int)($queryParams['id'] ?? 0);
$type = $queryParams['type'] ?? '';
$nodeIdentifier = $queryParams['nodeIdentifier'] ?? '';
if (empty($pageUid) || !in_array($type, ['constant', 'setup']) || empty($nodeIdentifier)) {
throw new \RuntimeException('Required action argument missing or invalid', 1658562276);
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
// @todo: Switch to BU::BEgetRootLine($pageUid, '', true) as in PageTsConfig? Similar in other controllers and actions.
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$site = $request->getAttribute('site');
$rootLine = $this->getScopedRootline($site, $rootLine);
// Template selection handling
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($selectedTemplateFromModuleData[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
$hasTemplate = false;
$templateTitle = '';
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$hasTemplate = true;
$templateTitle = $templateRow['title'];
}
}
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
// Get current value of to-edit object path
// Build the constant include tree
$constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, new LosslessTokenizer(), $site);
// Set enabled conditions in constant include tree
$constantConditions = $this->handleToggledConstantConditions($constantIncludeTree, $moduleData, null);
$conditionEnforcerVisitor = new IncludeTreeConditionEnforcerVisitor();
$conditionEnforcerVisitor->setEnabledConditions(array_column(array_filter($constantConditions, static fn($condition) => $condition['active']), 'value'));
$treeTraverser = new ConditionVerdictAwareIncludeTreeTraverser();
$treeTraverserVisitors = [];
$treeTraverserVisitors[] = $conditionEnforcerVisitor;
$constantAstBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
$treeTraverserVisitors[] = $constantAstBuilderVisitor;
$treeTraverser->traverse($constantIncludeTree, $treeTraverserVisitors);
$astNodeFinderVisitor = new AstNodeFinderVisitor();
$astNodeFinderVisitor->setNodeIdentifier($nodeIdentifier);
if ($type === 'constant') {
$constantAst = $constantAstBuilderVisitor->getAst();
$constantAst->setIdentifier('TypoScript constants');
$astTraverser = new AstTraverser();
$astTraverser->traverse($constantAst, [$astNodeFinderVisitor]);
} else {
// Build the setup include tree
$setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, new LosslessTokenizer(), $site);
$flattenedConstants = $constantAstBuilderVisitor->getAst()->flatten();
// Set enabled conditions in setup include tree
$setupConditions = $this->handleToggledSetupConditions($setupIncludeTree, $moduleData, null, $flattenedConstants);
$conditionEnforcerVisitor = new IncludeTreeConditionEnforcerVisitor();
$conditionEnforcerVisitor->setEnabledConditions(array_column(array_filter($setupConditions, static fn($condition) => $condition['active']), 'value'));
$treeTraverser = new ConditionVerdictAwareIncludeTreeTraverser();
$treeTraverserVisitors = [];
$treeTraverserVisitors[] = $conditionEnforcerVisitor;
$setupAstBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
$setupAstBuilderVisitor->setFlatConstants($flattenedConstants);
$treeTraverserVisitors[] = $setupAstBuilderVisitor;
$treeTraverser->traverse($setupIncludeTree, $treeTraverserVisitors);
$setupAst = $setupAstBuilderVisitor->getAst();
$setupAst->setIdentifier('TypoScript setup');
$astTraverser = new AstTraverser();
$astTraverser->traverse($setupAst, [$astNodeFinderVisitor]);
}
$foundNode = $astNodeFinderVisitor->getFoundNode();
$foundNodeCurrentObjectPath = $astNodeFinderVisitor->getFoundNodeCurrentObjectPath();
if ($foundNode === null || $foundNodeCurrentObjectPath === null) {
throw new \RuntimeException('Node with identifier ' . $nodeIdentifier . ' to edit not found', 1675241994);
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'pageUid' => $pageUid,
'hasTemplate' => $hasTemplate,
'templateTitle' => $templateTitle,
'type' => $type,
'currentObjectPath' => $foundNodeCurrentObjectPath->getPathAsString(),
'currentValue' => $foundNode->getValue(),
]);
return $view->renderResponse('ActiveEdit');
}
/**
* Add a line to selected sys_template record of given page after editing or clearing a
* property or adding a child in 'edit' view. Update either 'constants' or 'config' field
* using DataHandler, add a flash message and redirect to default "show" action.
*/
public function updateAction(ServerRequestInterface $request): ResponseInterface
{
$languageService = $this->getLanguageService();
$moduleData = $request->getAttribute('moduleData');
$parsedBody = $request->getParsedBody();
$pageUid = (int)($parsedBody['pageUid'] ?? 0);
$type = $parsedBody['type'] ?? '';
$currentObjectPath = $parsedBody['currentObjectPath'] ?? '';
$command = null;
if (isset($parsedBody['updateValue'])) {
$command = 'updateValue';
} elseif (isset($parsedBody['addChild'])) {
$command = 'addChild';
} elseif (isset($parsedBody['clear'])) {
$command = 'clear';
}
if (empty($pageUid) || !in_array($type, ['constant', 'setup']) || empty($currentObjectPath) || empty($command)) {
throw new \RuntimeException('Required action argument missing or invalid', 1658568446);
}
// Template selection handling
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($selectedTemplateFromModuleData[$pageUid] ?? 0);
$templateRow = null;
foreach ($allTemplatesOnPage as $template) {
if ($selectedTemplateUid === (int)$template['uid']) {
$templateRow = $template;
}
}
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$templateRow = $allTemplatesOnPage[0] ?? [];
$selectedTemplateUid = (int)($templateRow['uid'] ?? 0);
}
if ($selectedTemplateUid < 1) {
throw new \RuntimeException('No template on page found', 1658568794);
}
$newLine = null;
$flashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
switch ($command) {
case 'updateValue':
$newLine = $currentObjectPath . ' = ' . trim($parsedBody['value'] ?? '');
break;
case 'addChild':
$childName = str_replace('\\', '', $parsedBody['childName'] ?? '');
if (empty($childName) || preg_replace('/[^a-zA-Z0-9_\.]*/', '', $childName) != $childName) {
$flashMessage = new FlashMessage(
$languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_active.xlf:updateAction.noSpaces'),
$languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_active.xlf:updateAction.lineNotAdded'),
ContextualFeedbackSeverity::WARNING,
true
);
$flashMessageQueue->enqueue($flashMessage);
break;
}
$childName = addcslashes($parsedBody['childName'] ?? '', '.');
$childValue = trim($parsedBody['childValue'] ?? '');
$newLine = $currentObjectPath . '.' . $childName . ' = ' . $childValue;
break;
case 'clear':
$newLine = $currentObjectPath . ' >';
break;
}
if ($newLine) {
$fieldName = $type === 'constant' ? 'constants' : 'config';
$recordData = [
'sys_template' => [
$selectedTemplateUid => [
$fieldName => ($templateRow[$fieldName] ?? '') . LF . $newLine,
],
],
];
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($recordData, []);
$dataHandler->process_datamap();
$flashMessage = new FlashMessage(
$newLine,
$languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_active.xlf:updateAction.lineAdded'),
ContextualFeedbackSeverity::OK,
true
);
$flashMessageQueue->enqueue($flashMessage);
}
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('typoscript_active', ['id' => $pageUid]));
}
/**
* Align module data active constant conditions with toggled conditions from POST,
* write updated active conditions to user's module data if needed and
* prepare a list of active conditions for view.
*/
private function handleToggledConstantConditions(RootInclude $constantTree, ModuleData $moduleData, ?array $parsedBody): array
{
$conditionAggregatorVisitor = new IncludeTreeConditionAggregatorVisitor();
$treeTraverser = new IncludeTreeTraverser();
$treeTraverser->traverse($constantTree, [$conditionAggregatorVisitor]);
$constantConditions = $conditionAggregatorVisitor->getConditions();
$conditionsFromPost = $parsedBody['constantConditions'] ?? [];
$conditionsFromModuleData = array_flip((array)$moduleData->get('constantConditions'));
$typoscriptConditions = [];
foreach ($constantConditions as $condition) {
$conditionHash = sha1($condition['value']);
$conditionActive = array_key_exists($conditionHash, $conditionsFromModuleData);
// Note we're not feeding the post values directly to module data, but filter
// them through available conditions to prevent polluting module data with
// manipulated post values.
if (($conditionsFromPost[$conditionHash] ?? null) === '0') {
unset($conditionsFromModuleData[$conditionHash]);
$conditionActive = false;
} elseif (($conditionsFromPost[$conditionHash] ?? null) === '1') {
$conditionsFromModuleData[$conditionHash] = true;
$conditionActive = true;
}
$typoscriptConditions[] = [
'value' => $condition['value'],
'hash' => $conditionHash,
'active' => $conditionActive,
];
}
if ($conditionsFromPost) {
$moduleData->set('constantConditions', array_keys($conditionsFromModuleData));
$this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
}
return $typoscriptConditions;
}
/**
* Align module data active setup conditions with toggled conditions from POST,
* write updated active conditions to user's module data if needed and
* prepare a list of active conditions for view.
*/
private function handleToggledSetupConditions(RootInclude $setupTree, ModuleData $moduleData, ?array $parsedBody, array $flattenedConstants): array
{
$setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
$setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flattenedConstants);
$treeTraverser = new IncludeTreeTraverser();
$treeTraverserVisitors = [];
$treeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor;
$conditionAggregatorVisitor = new IncludeTreeConditionAggregatorVisitor();
$treeTraverserVisitors[] = $conditionAggregatorVisitor;
$treeTraverser->traverse($setupTree, $treeTraverserVisitors);
$setupConditions = $conditionAggregatorVisitor->getConditions();
$conditionsFromPost = $parsedBody['setupConditions'] ?? [];
$conditionsFromModuleData = array_flip((array)$moduleData->get('setupConditions'));
$typoscriptConditions = [];
foreach ($setupConditions as $condition) {
$conditionHash = sha1($condition['value']);
$conditionActive = array_key_exists($conditionHash, $conditionsFromModuleData);
// Note we're not feeding the post values directly to module data, but filter
// them through available conditions to prevent polluting module data with
// manipulated post values.
if (($conditionsFromPost[$conditionHash] ?? null) === '0') {
unset($conditionsFromModuleData[$conditionHash]);
$conditionActive = false;
} elseif (($conditionsFromPost[$conditionHash] ?? null) === '1') {
$conditionsFromModuleData[$conditionHash] = true;
$conditionActive = true;
}
$typoscriptConditions[] = [
'value' => $condition['value'],
'originalValue' => $condition['originalValue'],
'hash' => $conditionHash,
'active' => $conditionActive,
];
}
if ($conditionsFromPost) {
$moduleData->set('setupConditions', array_keys($conditionsFromModuleData));
$this->getBackendUser()->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray());
}
return $typoscriptConditions;
}
}
@@ -0,0 +1,459 @@
<?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\Tstemplate\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\TypoScript\AST\AstBuilderInterface;
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
use TYPO3\CMS\Core\TypoScript\AST\Traverser\AstTraverser;
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstConstantCommentVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeCommentAwareAstBuilderVisitor;
use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* TypoScript Constant editor
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
class ConstantEditorController extends AbstractTemplateModuleController
{
public function __construct(
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
private readonly SysTemplateRepository $sysTemplateRepository,
private readonly SysTemplateTreeBuilder $treeBuilder,
private readonly IncludeTreeTraverser $treeTraverser,
private readonly AstTraverser $astTraverser,
private readonly AstBuilderInterface $astBuilder,
private readonly LosslessTokenizer $losslessTokenizer,
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$pageUid = (int)($queryParams['id'] ?? 0);
if ($pageUid === 0) {
// Redirect to template record overview if on page 0.
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
if (($parsedBody['action'] ?? '') === 'createExtensionTemplate') {
return $this->createExtensionTemplateAction($request, 'web_typoscript_constanteditor');
}
if (($parsedBody['action'] ?? '') === 'createNewWebsiteTemplate') {
return $this->createNewWebsiteTemplateAction($request, 'web_typoscript_constanteditor');
}
if (($parsedBody['_savedok'] ?? false) === '1') {
return $this->saveAction($request);
}
$pageUid = (int)($queryParams['id'] ?? 0);
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
if (empty($allTemplatesOnPage)) {
return $this->noTemplateAction($request);
}
return $this->showAction($request);
}
private function showAction(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUser();
$pageUid = (int)($queryParams['id'] ?? 0);
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$moduleData = $request->getAttribute('moduleData');
if ($moduleData->cleanUp([])) {
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
// Template selection handling for this page
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($parsedBody['selectedTemplate'] ?? $selectedTemplateFromModuleData[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if (($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0) !== $selectedTemplateUid) {
$selectedTemplateFromModuleData[$pageUid] = $selectedTemplateUid;
$moduleData->set('selectedTemplatePerPage', $selectedTemplateFromModuleData);
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$templateTitle = '';
$currentTemplateConstants = '';
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$templateTitle = $templateRow['title'];
$currentTemplateConstants = $templateRow['constants'] ?? '';
}
}
$currentTemplateRecord = [];
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$currentTemplateRecord = $templateRow;
break;
}
}
// Build the constant include tree
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$site = $request->getAttribute('site');
$rootLine = $this->getScopedRootline($site, $rootLine);
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
$constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->losslessTokenizer, $site);
$constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class);
$this->treeTraverser->traverse($constantIncludeTree, [$constantAstBuilderVisitor]);
$constantAst = $constantAstBuilderVisitor->getAst();
$astConstantCommentVisitor = GeneralUtility::makeInstance(AstConstantCommentVisitor::class);
$currentTemplateFlatConstants = $this->astBuilder->build($this->losslessTokenizer->tokenize($currentTemplateConstants), new RootNode())->flatten();
$astConstantCommentVisitor->setCurrentTemplateFlatConstants($currentTemplateFlatConstants);
$this->astTraverser->traverse($constantAst, [$astConstantCommentVisitor]);
$constants = $astConstantCommentVisitor->getConstants();
$categories = $astConstantCommentVisitor->getCategories();
$relevantCategories = [];
foreach ($categories as $categoryKey => $aCategory) {
if ($aCategory['usageCount'] > 0) {
$relevantCategories[$categoryKey] = $aCategory;
}
}
$selectedCategory = array_key_first($relevantCategories) ?? '';
$selectedCategoryFromModuleData = (string)$moduleData->get('selectedCategory');
if (array_key_exists($selectedCategoryFromModuleData, $relevantCategories)) {
$selectedCategory = $selectedCategoryFromModuleData;
}
if (($parsedBody['selectedCategory'] ?? '') && array_key_exists($parsedBody['selectedCategory'], $relevantCategories)) {
$selectedCategory = (string)$parsedBody['selectedCategory'];
}
if ($selectedCategory && $selectedCategory !== $selectedCategoryFromModuleData) {
$moduleData->set('selectedCategory', $selectedCategory);
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$displayConstants = [];
foreach ($constants as $constant) {
if ($constant['cat'] === $selectedCategory) {
$displayConstants[$constant['subcat_sorting_first']]['label'] = $constant['subcat_label'];
$displayConstants[$constant['subcat_sorting_first']]['items'][$constant['subcat_sorting_second']][] = $constant;
}
}
ksort($displayConstants);
foreach ($displayConstants as &$constant) {
ksort($constant['items']);
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_ceditor.xlf:submodule.title'));
if (!empty($relevantCategories)) {
$view->addButtonToButtonBar($this->componentFactory->createSaveButton('TypoScriptConstantEditorController'));
}
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'templateTitle' => $templateTitle,
'pageUid' => $pageUid,
'templateRecord' => $currentTemplateRecord,
'allTemplatesOnPage' => $allTemplatesOnPage,
'selectedTemplateUid' => $selectedTemplateUid,
'relevantCategories' => $relevantCategories,
'selectedCategory' => $selectedCategory,
'displayConstants' => $displayConstants,
]);
return $view->renderResponse('ConstantEditorMain');
}
private function saveAction(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$moduleData = $request->getAttribute('moduleData');
$pageUid = (int)($queryParams['id'] ?? 0);
if ($pageUid === 0) {
throw new \RuntimeException('No proper page uid given', 1661333862);
}
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($selectedTemplateFromModuleData[$pageUid] ?? 0);
$templateRow = null;
foreach ($allTemplatesOnPage as $template) {
if ($selectedTemplateUid === (int)$template['uid']) {
$templateRow = $template;
}
}
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$templateRow = $allTemplatesOnPage[0] ?? [];
$selectedTemplateUid = (int)($templateRow['uid'] ?? 0);
}
if ($selectedTemplateUid < 1) {
throw new \RuntimeException('No template found on page', 1661350211);
}
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$site = $request->getAttribute('site');
$rootLine = $this->getScopedRootline($site, $rootLine);
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
$constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->losslessTokenizer, $site);
$constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeCommentAwareAstBuilderVisitor::class);
$this->treeTraverser->traverse($constantIncludeTree, [$constantAstBuilderVisitor]);
$constantAst = $constantAstBuilderVisitor->getAst();
$astConstantCommentVisitor = GeneralUtility::makeInstance(AstConstantCommentVisitor::class);
$this->astTraverser->traverse($constantAst, [$astConstantCommentVisitor]);
$constants = $astConstantCommentVisitor->getConstants();
$updatedTemplateConstantsArray = $this->updateTemplateConstants($request, $constants, $templateRow['constants'] ?? '');
if ($updatedTemplateConstantsArray) {
$templateUid = empty($templateRow['_ORIG_uid']) ? $templateRow['uid'] : $templateRow['_ORIG_uid'];
$recordData = [];
$recordData['sys_template'][$templateUid]['constants'] = implode(LF, $updatedTemplateConstantsArray);
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($recordData, []);
$dataHandler->process_datamap();
}
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_constanteditor', ['id' => $pageUid]));
}
private function noTemplateAction(ServerRequestInterface $request): ResponseInterface
{
$languageService = $this->getLanguageService();
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$pageUid = (int)($request->getQueryParams()['id'] ?? 0);
if ($pageUid === 0) {
throw new \RuntimeException('No proper page uid given', 1661365944);
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_ceditor.xlf:submodule.title'));
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'pageUid' => $pageUid,
'moduleIdentifier' => $currentModuleIdentifier,
'previousPage' => $this->getClosestAncestorPageWithTemplateRecord($pageUid),
]);
return $view->renderResponse('ConstantEditorNoTemplate');
}
private function updateTemplateConstants(ServerRequestInterface $request, array $constantDefinitions, string $rawTemplateConstants): ?array
{
$rawTemplateConstantsArray = explode(LF, $rawTemplateConstants);
$constantPositions = $this->calculateConstantPositions($rawTemplateConstantsArray);
$parsedBody = $request->getParsedBody();
$data = $parsedBody['data'] ?? null;
$check = $parsedBody['check'] ?? [];
$valuesHaveChanged = false;
if (is_array($data)) {
foreach ($data as $key => $value) {
if (!isset($constantDefinitions[$key])) {
// Ignore if there is no constant definition for this constant key
continue;
}
if (!isset($check[$key]) || ($check[$key] !== 'checked' && isset($constantPositions[$key]))) {
// Remove value if the checkbox is not set, indicating "value to be dropped from template"
$rawTemplateConstantsArray = $this->removeValueFromConstantsArray($rawTemplateConstantsArray, $constantPositions, $key);
$valuesHaveChanged = true;
continue;
}
if ($check[$key] !== 'checked') {
// Don't process if this value is not set
continue;
}
$constantDefinition = $constantDefinitions[$key];
switch ($constantDefinition['type']) {
case 'int':
$min = $constantDefinition['typeIntMin'] ?? PHP_INT_MIN;
$max = $constantDefinition['typeIntMax'] ?? PHP_INT_MAX;
$value = (string)MathUtility::forceIntegerInRange((int)$value, (int)$min, (int)$max);
break;
case 'int+':
$min = $constantDefinition['typeIntMin'] ?? 0;
$max = $constantDefinition['typeIntMax'] ?? PHP_INT_MAX;
$value = (string)MathUtility::forceIntegerInRange((int)$value, (int)$min, (int)$max);
break;
case 'color':
$col = [];
if ($value) {
$value = preg_replace('/[^A-Fa-f0-9]*/', '', $value) ?? '';
$useFulHex = strlen($value) > 3;
$col[] = (int)hexdec($value[0]);
$col[] = (int)hexdec($value[1]);
$col[] = (int)hexdec($value[2]);
if ($useFulHex) {
$col[] = (int)hexdec($value[3]);
$col[] = (int)hexdec($value[4]);
$col[] = (int)hexdec($value[5]);
}
$value = substr('0' . dechex($col[0]), -1) . substr('0' . dechex($col[1]), -1) . substr('0' . dechex($col[2]), -1);
if ($useFulHex) {
$value .= substr('0' . dechex($col[3]), -1) . substr('0' . dechex($col[4]), -1) . substr('0' . dechex($col[5]), -1);
}
$value = '#' . strtoupper($value);
}
break;
case 'comment':
if ($value) {
$value = '';
} else {
$value = '#';
}
break;
case 'wrap':
if (($data[$key]['left'] ?? false) || $data[$key]['right']) {
$value = $data[$key]['left'] . '|' . $data[$key]['right'];
} else {
$value = '';
}
break;
case 'offset':
$value = rtrim(implode(',', $value), ',');
if (trim($value, ',') === '') {
$value = '';
}
break;
case 'boolean':
if ($value) {
$value = ($constantDefinition['trueValue'] ?? false) ?: '1';
}
break;
}
if ((string)($constantDefinition['value'] ?? '') !== (string)$value) {
// Put value in, if changed.
$rawTemplateConstantsArray = $this->addOrUpdateValueInConstantsArray($rawTemplateConstantsArray, $constantPositions, $key, $value);
$valuesHaveChanged = true;
}
}
}
if ($valuesHaveChanged) {
return $rawTemplateConstantsArray;
}
return null;
}
private function calculateConstantPositions(
array $rawTemplateConstantsArray,
array &$constantPositions = [],
string $prefix = '',
int $braceLevel = 0,
int &$lineCounter = 0
): array {
while (isset($rawTemplateConstantsArray[$lineCounter])) {
$line = ltrim($rawTemplateConstantsArray[$lineCounter]);
$lineCounter++;
if (!$line || $line[0] === '[') {
// Ignore empty lines and conditions
continue;
}
if (strcspn($line, '}#/') !== 0) {
$operatorPosition = strcspn($line, ' {=<');
$key = substr($line, 0, $operatorPosition);
$line = ltrim(substr($line, $operatorPosition));
if ($line[0] === '=') {
$constantPositions[$prefix . $key] = $lineCounter - 1;
} elseif ($line[0] === '{') {
$braceLevel++;
$this->calculateConstantPositions($rawTemplateConstantsArray, $constantPositions, $prefix . $key . '.', $braceLevel, $lineCounter);
}
} elseif ($line[0] === '}') {
$braceLevel--;
if ($braceLevel < 0) {
$braceLevel = 0;
} else {
// Leaving this brace level: Force return to caller recursion
break;
}
}
}
return $constantPositions;
}
/**
* Update a constant value in current template constants if key exists already,
* or add key/value at the end if it does not exist yet.
*/
private function addOrUpdateValueInConstantsArray(array $templateConstantsArray, array $constantPositions, string $constantKey, string $value): array
{
$theValue = ' ' . trim($value);
if (isset($constantPositions[$constantKey])) {
$lineNum = $constantPositions[$constantKey];
$parts = explode('=', $templateConstantsArray[$lineNum], 2);
if (count($parts) === 2) {
$parts[1] = $theValue;
}
$templateConstantsArray[$lineNum] = implode('=', $parts);
} else {
$templateConstantsArray[] = $constantKey . ' =' . $theValue;
}
return $templateConstantsArray;
}
/**
* Remove a key from constant array.
*/
private function removeValueFromConstantsArray(array $templateConstantsArray, array $constantPositions, string $constantKey): array
{
if (isset($constantPositions[$constantKey])) {
$lineNum = $constantPositions[$constantKey];
unset($templateConstantsArray[$lineNum]);
}
return $templateConstantsArray;
}
}
+150
View File
@@ -0,0 +1,150 @@
<?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\Tstemplate\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Http\RedirectResponse;
/**
* This class displays the Info/Modify screen of the Sites > TypoScript module
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
class InfoModifyController extends AbstractTemplateModuleController
{
public function __construct(
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$pageUid = (int)($queryParams['id'] ?? 0);
if ($pageUid === 0) {
// Redirect to template record overview if on page 0.
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
if (($parsedBody['action'] ?? '') === 'createExtensionTemplate') {
return $this->createExtensionTemplateAction($request, 'web_typoscript_infomodify');
}
if (($parsedBody['action'] ?? '') === 'createNewWebsiteTemplate') {
return $this->createNewWebsiteTemplateAction($request, 'web_typoscript_infomodify');
}
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
if (empty($allTemplatesOnPage)) {
return $this->noTemplateAction($request);
}
return $this->mainAction($request, $pageUid, $allTemplatesOnPage);
}
private function noTemplateAction(ServerRequestInterface $request): ResponseInterface
{
$languageService = $this->getLanguageService();
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$pageUid = (int)($request->getQueryParams()['id'] ?? 0);
if ($pageUid === 0) {
throw new \RuntimeException('No proper page uid given', 1661769346);
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_info.xlf:submodule.title'));
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'pageUid' => $pageUid,
'moduleIdentifier' => $currentModuleIdentifier,
'previousPage' => $this->getClosestAncestorPageWithTemplateRecord($pageUid),
]);
return $view->renderResponse('InfoModifyNoTemplate');
}
private function mainAction(ServerRequestInterface $request, int $pageUid, array $allTemplatesOnPage): ResponseInterface
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$backendUser = $this->getBackendUser();
$languageService = $this->getLanguageService();
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$moduleData = $request->getAttribute('moduleData');
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($parsedBody['selectedTemplate'] ?? $queryParams['selectedTemplate'] ?? $selectedTemplateFromModuleData[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if (($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0) !== $selectedTemplateUid) {
$selectedTemplateFromModuleData[$pageUid] = $selectedTemplateUid;
$moduleData->set('selectedTemplatePerPage', $selectedTemplateFromModuleData);
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$currentTemplateRecord = [];
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$currentTemplateRecord = $templateRow;
break;
}
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_info.xlf:submodule.title'));
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'pageUid' => $pageUid,
'previousPage' => $this->getClosestAncestorPageWithTemplateRecord($pageUid),
'templateRecord' => $currentTemplateRecord,
'allTemplatesOnPage' => $allTemplatesOnPage,
'numberOfConstantsLines' => trim((string)($currentTemplateRecord['constants'] ?? '')) ? count(explode(LF, (string)$currentTemplateRecord['constants'])) : 0,
'numberOfSetupLines' => trim((string)($currentTemplateRecord['config'] ?? '')) ? count(explode(LF, (string)$currentTemplateRecord['config'])) : 0,
]);
return $view->renderResponse('InfoModifyMain');
}
}
@@ -0,0 +1,210 @@
<?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\Tstemplate\Controller;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeNodeFinderVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSourceAggregatorVisitor;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSyntaxScannerVisitor;
use TYPO3\CMS\Core\TypoScript\Tokenizer\LosslessTokenizer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
/**
* TypoScript template analyzer.
* Show TypoScript constants and setup include tree of current page.
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
final class TemplateAnalyzerController extends AbstractTemplateModuleController
{
public function __construct(
private readonly ModuleTemplateFactory $moduleTemplateFactory,
private readonly SysTemplateRepository $sysTemplateRepository,
private readonly IncludeTreeTraverser $treeTraverser,
private readonly SysTemplateTreeBuilder $treeBuilder,
private readonly LosslessTokenizer $losslessTokenizer,
private readonly ResponseFactoryInterface $responseFactory,
private readonly StreamFactoryInterface $streamFactory,
) {}
public function indexAction(ServerRequestInterface $request): ResponseInterface
{
$backendUser = $this->getBackendUser();
$languageService = $this->getLanguageService();
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$moduleData = $request->getAttribute('moduleData');
$pageUid = (int)($queryParams['id'] ?? 0);
if ($pageUid === 0) {
// Redirect to template record overview if on page 0.
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if (empty($pageRecord)) {
// Redirect to records overview if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
// Template selection handling for this page
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateFromModuleData = (array)$moduleData->get('selectedTemplatePerPage');
$selectedTemplateUid = (int)($parsedBody['selectedTemplate'] ?? $selectedTemplateFromModuleData[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if (($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0) !== $selectedTemplateUid) {
$selectedTemplateFromModuleData[$pageUid] = $selectedTemplateUid;
$moduleData->set('selectedTemplatePerPage', $selectedTemplateFromModuleData);
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$templateTitle = '';
foreach ($allTemplatesOnPage as $templateRow) {
if ((int)$templateRow['uid'] === $selectedTemplateUid) {
$templateTitle = $templateRow['title'];
}
}
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$site = $request->getAttribute('site');
$rootLine = $this->getScopedRootline($site, $rootLine);
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
// Build the constant include tree
$constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->losslessTokenizer, $site);
$constantIncludeTree->setIdentifier('constants tstemplate includes');
$treeTraverserVisitors = [];
$constantSyntaxScannerVisitor = new IncludeTreeSyntaxScannerVisitor();
$treeTraverserVisitors[] = $constantSyntaxScannerVisitor;
$this->treeTraverser->traverse($constantIncludeTree, $treeTraverserVisitors);
// Build the setup include tree
$setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->losslessTokenizer, $site);
$setupIncludeTree->setIdentifier('setup tstemplate includes');
$treeTraverserVisitors = [];
$setupSyntaxScannerVisitor = new IncludeTreeSyntaxScannerVisitor();
$treeTraverserVisitors[] = $setupSyntaxScannerVisitor;
$this->treeTraverser->traverse($setupIncludeTree, $treeTraverserVisitors);
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($languageService->sL($currentModule->getTitle()), $pageRecord['title']);
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $languageService->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_analyzer.xlf:submodule.title'));
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
$view->assignMultiple([
'pageUid' => $pageUid,
'allTemplatesOnPage' => $allTemplatesOnPage,
'selectedTemplateUid' => $selectedTemplateUid,
'templateTitle' => $templateTitle,
'constantIncludeTree' => $constantIncludeTree,
'constantErrors' => $constantSyntaxScannerVisitor->getErrors(),
'constantErrorCount' => count($constantSyntaxScannerVisitor->getErrors()),
'setupIncludeTree' => $setupIncludeTree,
'setupErrors' => $setupSyntaxScannerVisitor->getErrors(),
'setupErrorCount' => count($setupSyntaxScannerVisitor->getErrors()),
]);
return $view->renderResponse('Analyzer');
}
public function sourceAction(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$pageUid = (int)($queryParams['id'] ?? 0);
$type = $queryParams['includeType'] ?? null;
$includeIdentifier = $queryParams['identifier'] ?? null;
$moduleData = $request->getAttribute('moduleData');
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateUid = (int)($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if ($pageUid === 0 || $includeIdentifier === null || !in_array($type, ['constants', 'setup'], true)) {
return $this->responseFactory->createResponse(400);
}
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
$site = $request->getAttribute('site');
$includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite($type, $sysTemplateRows, $this->losslessTokenizer, $site);
$includeTree->setIdentifier($type . ' tstemplate includes');
$nodeFinderVisitor = GeneralUtility::makeInstance(IncludeTreeNodeFinderVisitor::class);
$nodeFinderVisitor->setNodeIdentifier($includeIdentifier);
$this->treeTraverser->traverse($includeTree, [$nodeFinderVisitor]);
$foundNode = $nodeFinderVisitor->getFoundNode();
if ($foundNode?->getLineStream() === null) {
return $this->responseFactory->createResponse(400);
}
return $this->responseFactory
->createResponse()
->withHeader('Content-Type', 'text/plain')
->withBody($this->streamFactory->createStream((string)$foundNode->getLineStream()));
}
public function sourceWithIncludesAction(ServerRequestInterface $request): ResponseInterface
{
$queryParams = $request->getQueryParams();
$pageUid = (int)($queryParams['id'] ?? 0);
$type = $queryParams['includeType'] ?? null;
$includeIdentifier = $queryParams['identifier'] ?? null;
$moduleData = $request->getAttribute('moduleData');
$allTemplatesOnPage = $this->getAllTemplateRecordsOnPage($pageUid);
$selectedTemplateUid = (int)($moduleData->get('selectedTemplatePerPage')[$pageUid] ?? 0);
if (!in_array($selectedTemplateUid, array_column($allTemplatesOnPage, 'uid'))) {
$selectedTemplateUid = (int)($allTemplatesOnPage[0]['uid'] ?? 0);
}
if ($pageUid === 0 || $includeIdentifier === null || !in_array($type, ['constants', 'setup'], true)) {
return $this->responseFactory->createResponse(400);
}
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid)->get();
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootlineWithUidOverride($rootLine, $request, $selectedTemplateUid, $this->createVisibilityAspect());
$site = $request->getAttribute('site');
$includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite($type, $sysTemplateRows, $this->losslessTokenizer, $site);
$includeTree->setIdentifier($type . ' tstemplate includes');
$sourceAggregatorVisitor = new IncludeTreeSourceAggregatorVisitor();
$sourceAggregatorVisitor->setStartNodeIdentifier($includeIdentifier);
$this->treeTraverser->traverse($includeTree, [$sourceAggregatorVisitor]);
$source = $sourceAggregatorVisitor->getSource();
return $this->responseFactory
->createResponse()
->withHeader('Content-Type', 'text/plain')
->withBody($this->streamFactory->createStream($source));
}
}
@@ -0,0 +1,145 @@
<?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\Tstemplate\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Overview of all sys_template records from site root
*
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
*/
#[AsController]
class TemplateRecordsOverviewController extends AbstractTemplateModuleController
{
public function __construct(
private readonly ModuleTemplateFactory $moduleTemplateFactory,
) {}
public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$backendUser = $this->getBackendUser();
$currentModule = $request->getAttribute('module');
$currentModuleIdentifier = $currentModule->getIdentifier();
$pageUid = (int)($request->getQueryParams()['id'] ?? 0);
$pageRecord = BackendUtility::readPageAccess($pageUid, '1=1') ?: [];
if ($pageUid > 0 && empty($pageRecord)) {
// Redirect to records overview of page 0 if page could not be determined.
// Edge case if page has been removed meanwhile.
BackendUtility::setUpdateSignal('updatePageTree');
return new RedirectResponse($this->uriBuilder->buildUriFromRoute('web_typoscript_recordsoverview'));
}
$moduleData = $request->getAttribute('moduleData');
if ($moduleData->cleanUp([])) {
$backendUser->pushModuleData($currentModuleIdentifier, $moduleData->toArray());
}
$pagesWithTemplates = [];
$sites = $this->siteFinder->getAllSites();
foreach ($sites as $site) {
if (!$site->isTypoScriptRoot()) {
continue;
}
$rootPageId = $site->getRootPageId();
$additionalFieldsForRootline = ['sorting', 'shortcut'];
$rootline = array_reverse(BackendUtility::BEgetRootLine($rootPageId, '', true, $additionalFieldsForRootline));
if ($rootline !== []) {
$pagesWithTemplates = $this->setInPageArray($pagesWithTemplates, $rootline, [
'type' => 'site',
'root' => 1,
'clear' => 1,
'pid' => $rootPageId,
'sorting' => -1,
'uid' => -1,
'title' => $site->getConfiguration()['websiteTitle'] ?? '',
'site' => $site,
]);
}
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_template');
$queryBuilder->getRestrictions()->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
->select('uid', 'pid', 'title', 'root', 'hidden', 'starttime', 'endtime')
->from('sys_template')
// sys_template shouldn't exist pid 0, they'll be ignored in FE anyway. Ignore them.
->where($queryBuilder->expr()->gt('pid', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)))
->orderBy('sys_template.pid')
->addOrderBy('sys_template.sorting')
->executeQuery();
while ($record = $result->fetchAssociative()) {
$additionalFieldsForRootline = ['sorting', 'shortcut'];
$rootline = array_reverse(BackendUtility::BEgetRootLine($record['pid'], '', true, $additionalFieldsForRootline));
if ($rootline !== []) {
$pagesWithTemplates = $this->setInPageArray($pagesWithTemplates, $rootline, [...$record, 'type' => 'sys_template']);
}
}
$view = $this->moduleTemplateFactory->create($request);
$view->setTitle($this->getLanguageService()->sL($currentModule->getTitle()), '');
$view->getDocHeaderComponent()->setPageBreadcrumb($pageRecord);
$this->addPreviewButtonToDocHeader($view, $pageRecord);
$this->addShortcutButtonToDocHeader($view, $currentModuleIdentifier, $pageRecord, $pageUid, $this->getLanguageService()->sL('LLL:EXT:tstemplate/Resources/Private/Language/locallang_overview.xlf:typoscriptRecords.title'));
if ($pageUid !== 0) {
$view->makeDocHeaderModuleMenu(['id' => $pageUid]);
}
$view->assign('pageTree', $pagesWithTemplates);
return $view->renderResponse('TemplateRecordsOverview');
}
/**
* Recursively add template row in pages tree array by given pages rootline to prepare tree rendering.
* @param non-empty-array $rootline
*/
private function setInPageArray(array $pages, array $rootline, array $row): array
{
if (!$rootline[0]['uid']) {
// Skip 'root'
array_shift($rootline);
}
$currentRootlineElement = current($rootline);
if (empty($pages[$currentRootlineElement['uid']])) {
// Page not in tree yet. Add it.
$pages[$currentRootlineElement['uid']] = $currentRootlineElement;
}
array_shift($rootline);
if (empty($rootline)) {
// Last rootline element: Add template row
$pages[$currentRootlineElement['uid']]['_templates'][] = $row;
} else {
// Recurse into sub array
$pages[$currentRootlineElement['uid']]['_nodes'] ??= [];
$pages[$currentRootlineElement['uid']]['_nodes'] = $this->setInPageArray($pages[$currentRootlineElement['uid']]['_nodes'], $rootline, $row);
}
// Tree node sorting by pages sorting field
uasort($pages, static fn($a, $b) => $a['sorting'] - $b['sorting']);
return $pages;
}
}