318 lines
12 KiB
PHP
318 lines
12 KiB
PHP
<?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'];
|
|
}
|
|
}
|