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'];
}
}