352 lines
16 KiB
PHP
352 lines
16 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\Backend\View;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
|
|
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection;
|
|
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
|
|
use TYPO3\CMS\Backend\View\Event\ManipulateBackendLayoutColPosConfigurationForPageEvent;
|
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Page\PageLayoutResolver;
|
|
use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* Backend layout for CMS
|
|
*
|
|
* @todo: This class name is unfortunate and the scope of this class in general convoluted and unclear.
|
|
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
#[Autoconfigure(public: true)]
|
|
readonly class BackendLayoutView
|
|
{
|
|
private const string SELECTED_COMBINED_CACHE_IDENTIFIER = 'backend-layout-view-selected-combined-identifiers';
|
|
private const string SELECTED_BACKEND_LAYOUTS_CACHE_IDENTIFIER = 'backend-layout-view-selected-backend-layouts';
|
|
|
|
public function __construct(
|
|
private EventDispatcherInterface $eventDispatcher,
|
|
#[Autowire(service: 'cache.runtime')]
|
|
private FrontendInterface $runtimeCache,
|
|
private DataProviderCollection $dataProviderCollection,
|
|
private TypoScriptStringFactory $typoScriptStringFactory,
|
|
private PageLayoutResolver $pageLayoutResolver,
|
|
private ConnectionPool $connectionPool,
|
|
) {}
|
|
|
|
/**
|
|
* This method is called as "itemsProcFunc" with the accordant context
|
|
* for pages.backend_layout and pages.backend_layout_next_level.
|
|
* Also used in the info module, since we need those items with
|
|
* the appropriate labels and backend layout identifiers there, too.
|
|
*
|
|
* @todo This method should return the items array instead of
|
|
* using the whole parameters array as reference. This
|
|
* has to be adjusted, as soon as the itemsProcFunc
|
|
* functionality is changed in this regard.
|
|
*/
|
|
public function addBackendLayoutItems(array &$parameters): void
|
|
{
|
|
$pageId = $this->determinePageId($parameters['table'], $parameters['row']) ?: 0;
|
|
$pageTsConfig = BackendUtility::getPagesTSconfig($pageId);
|
|
$identifiersToBeExcluded = [];
|
|
if (isset($pageTsConfig['options.']['backendLayout.']['exclude'])) {
|
|
$identifiersToBeExcluded = GeneralUtility::trimExplode(',', $pageTsConfig['options.']['backendLayout.']['exclude'], true);
|
|
}
|
|
$dataProviderContext = new DataProviderContext(
|
|
pageId: $pageId,
|
|
tableName: $parameters['table'],
|
|
fieldName: $parameters['field'],
|
|
data: $parameters['row'],
|
|
pageTsConfig: $pageTsConfig,
|
|
);
|
|
$backendLayoutCollections = $this->dataProviderCollection->getBackendLayoutCollections($dataProviderContext);
|
|
foreach ($backendLayoutCollections as $backendLayoutCollection) {
|
|
$combinedIdentifierPrefix = '';
|
|
if ($backendLayoutCollection->getIdentifier() !== 'default') {
|
|
$combinedIdentifierPrefix = $backendLayoutCollection->getIdentifier() . '__';
|
|
}
|
|
foreach ($backendLayoutCollection->getAll() as $backendLayout) {
|
|
$combinedIdentifier = $combinedIdentifierPrefix . $backendLayout->getIdentifier();
|
|
if (in_array($combinedIdentifier, $identifiersToBeExcluded, true)) {
|
|
continue;
|
|
}
|
|
$parameters['items'][] = [
|
|
'label' => $backendLayout->getTitle(),
|
|
'value' => $combinedIdentifier,
|
|
'icon' => $backendLayout->getIconPath(),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets colPos items to be shown in form engine. This method is called
|
|
* as "itemsProcFunc" with the accordant context for tt_content.colPos.
|
|
*/
|
|
public function colPosListItemProcFunc(array &$parameters): void
|
|
{
|
|
$pageId = $this->determinePageId($parameters['table'], $parameters['row']);
|
|
if ($pageId !== false) {
|
|
$layout = $this->getSelectedBackendLayout($pageId);
|
|
if ($layout && !empty($layout['__items'])) {
|
|
$parameters['items'] = $layout['__items'];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the selected backend layout structure as an array
|
|
*/
|
|
public function getSelectedBackendLayout(int $pageId): array
|
|
{
|
|
return $this->getBackendLayoutForPage($pageId)->getStructure();
|
|
}
|
|
|
|
/**
|
|
* Get the BackendLayout object and parse the structure based on the UserTSconfig
|
|
*/
|
|
public function getBackendLayoutForPage(int $pageId): BackendLayout
|
|
{
|
|
$selectedBackendLayoutsByPageId = $this->runtimeCache->get(self::SELECTED_BACKEND_LAYOUTS_CACHE_IDENTIFIER);
|
|
if (($selectedBackendLayoutsByPageId[$pageId] ?? null) instanceof BackendLayout) {
|
|
return $selectedBackendLayoutsByPageId[$pageId];
|
|
}
|
|
if (!is_array($selectedBackendLayoutsByPageId)) {
|
|
$selectedBackendLayoutsByPageId = [];
|
|
}
|
|
$selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId);
|
|
if (empty($selectedCombinedIdentifier)) {
|
|
// If no backend layout is selected, use default
|
|
$selectedCombinedIdentifier = 'default';
|
|
}
|
|
$backendLayout = $this->dataProviderCollection->getBackendLayout($selectedCombinedIdentifier, $pageId);
|
|
if ($backendLayout === null) {
|
|
// If backend layout is not found available anymore, use default
|
|
$backendLayout = $this->dataProviderCollection->getBackendLayout('default', $pageId);
|
|
}
|
|
if ($backendLayout === null) {
|
|
// The 'default' backend layout must *always* return something. We must have some layout at this
|
|
// point and the method always returns a BackendLayout instance.
|
|
throw new \RuntimeException('Fallback to default backend layout failed', 1768151069);
|
|
}
|
|
$selectedBackendLayoutsByPageId[$pageId] = $backendLayout;
|
|
$this->runtimeCache->set(self::SELECTED_BACKEND_LAYOUTS_CACHE_IDENTIFIER, $selectedBackendLayoutsByPageId);
|
|
return $backendLayout;
|
|
}
|
|
|
|
/**
|
|
* This method is mainly used to retrieve the final allowed/disallowed content element configuration per colPos. It
|
|
* is an implementation of what ext:content_defender provided as extension for a long time already. This method is
|
|
* embedded in the overall rather convoluted handling around backend layouts. It is - at least for now - subject to change.
|
|
* The method emits an event that is declared internal as well.
|
|
*
|
|
* When calling the method, the optional request argument should be hand whenever available to give event listeners
|
|
* as much context as possible.
|
|
*
|
|
* @internal as the entire class. Note ManipulateBackendLayoutColPosConfigurationForPageEvent is declared internal
|
|
* as well. The event is needed for extensions like ext:container, but may still change when backend layout
|
|
* related code is consolidated.
|
|
*/
|
|
public function getColPosConfigurationForPage(BackendLayout $backendLayout, int $colPos, int $pageUid, ?ServerRequestInterface $request = null): array
|
|
{
|
|
$configuration = [];
|
|
$backendLayoutStructure = $backendLayout->getStructure();
|
|
if (in_array($colPos, array_map('intval', $backendLayoutStructure['__colPosList']), true)) {
|
|
foreach ($backendLayoutStructure['__config']['backend_layout.']['rows.'] as $row) {
|
|
if (empty($row['columns.'])) {
|
|
continue;
|
|
}
|
|
foreach ($row['columns.'] as $column) {
|
|
if (isset($column['colPos']) && $column['colPos'] !== '' && $colPos === (int)$column['colPos']) {
|
|
$configuration = $column;
|
|
// Compatibility layer for ext:content_defender: allowed.CType is now allowedContentTypes and
|
|
// disallowed.CType is now disallowedContentTypes. Copy the old content_defender settings to
|
|
// the new names if new names are not set.
|
|
// @todo: These fallbacks could potentially be deprecated at some point. Maybe v15?
|
|
if (!empty($configuration['allowed.']['CType'] ?? '') && !isset($configuration['allowedContentTypes'])) {
|
|
$configuration['allowedContentTypes'] = $configuration['allowed.']['CType'];
|
|
}
|
|
if (!empty($configuration['disallowed.']['CType'] ?? '') && !isset($configuration['disallowedContentTypes'])) {
|
|
$configuration['disallowedContentTypes'] = $configuration['disallowed.']['CType'];
|
|
}
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$event = new ManipulateBackendLayoutColPosConfigurationForPageEvent(
|
|
configuration: $configuration,
|
|
backendLayout: $backendLayout,
|
|
colPos: $colPos,
|
|
pageUid: $pageUid,
|
|
request: $request,
|
|
);
|
|
return $this->eventDispatcher->dispatch($event)->configuration;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
public function parseStructure(BackendLayout $backendLayout): array
|
|
{
|
|
$typoScriptTree = $this->typoScriptStringFactory->parseFromStringWithIncludes('backend-layout', $backendLayout->getConfiguration());
|
|
|
|
$backendLayoutData = [];
|
|
$backendLayoutData['config'] = $backendLayout->getConfiguration();
|
|
$backendLayoutData['__config'] = $typoScriptTree->toArray();
|
|
$backendLayoutData['__items'] = [];
|
|
$backendLayoutData['__colPosList'] = [];
|
|
$backendLayoutData['usedColumns'] = [];
|
|
$backendLayoutData['colCount'] = (int)($backendLayoutData['__config']['backend_layout.']['colCount'] ?? 0);
|
|
$backendLayoutData['rowCount'] = (int)($backendLayoutData['__config']['backend_layout.']['rowCount'] ?? 0);
|
|
|
|
// create items and colPosList
|
|
if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) {
|
|
$rows = $backendLayoutData['__config']['backend_layout.']['rows.'];
|
|
ksort($rows);
|
|
foreach ($rows as $row) {
|
|
if (!empty($row['columns.'])) {
|
|
foreach ($row['columns.'] as $column) {
|
|
if (!isset($column['colPos'])) {
|
|
continue;
|
|
}
|
|
$backendLayoutData['__items'][] = [
|
|
'label' => $column['name'],
|
|
'value' => $column['colPos'],
|
|
'icon' => null,
|
|
];
|
|
$backendLayoutData['__colPosList'][] = $column['colPos'];
|
|
$backendLayoutData['usedColumns'][(int)$column['colPos']] = $column['name'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $backendLayoutData;
|
|
}
|
|
|
|
public function isCTypeAllowedInColPosByPage(string $cType, int $colPos, int $pageUid): bool
|
|
{
|
|
$backendLayout = $this->getBackendLayoutForPage($pageUid);
|
|
$colPosConfiguration = $this->getColPosConfigurationForPage($backendLayout, $colPos, $pageUid);
|
|
|
|
if (!empty($colPosConfiguration['disallowedContentTypes'])) {
|
|
$disallowedContentTypes = GeneralUtility::trimExplode(',', $colPosConfiguration['disallowedContentTypes']);
|
|
|
|
if (in_array($cType, $disallowedContentTypes, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!empty($colPosConfiguration['allowedContentTypes'])) {
|
|
$allowedContentTypes = GeneralUtility::trimExplode(',', $colPosConfiguration['allowedContentTypes']);
|
|
|
|
if (!in_array($cType, $allowedContentTypes, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determines the page id for a given record of a database table.
|
|
*
|
|
* @return int|false Returns page id or false on error
|
|
*/
|
|
protected function determinePageId(string $tableName, array $data): int|false
|
|
{
|
|
if ($data === []) {
|
|
return false;
|
|
}
|
|
|
|
if (str_starts_with((string)$data['uid'], 'NEW')) {
|
|
// Negative uid_pid values of content elements indicate that the element
|
|
// has been inserted after an existing element so there is no pid to get
|
|
// the backendLayout for, and we have to get that first.
|
|
if ($data['pid'] < 0) {
|
|
$queryBuilder = $this->connectionPool
|
|
->getQueryBuilderForTable($tableName);
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll();
|
|
$pageId = $queryBuilder
|
|
->select('pid')
|
|
->from($tableName)
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
'uid',
|
|
$queryBuilder->createNamedParameter(abs($data['pid']), Connection::PARAM_INT)
|
|
)
|
|
)
|
|
->executeQuery()
|
|
->fetchOne();
|
|
} else {
|
|
$pageId = $data['pid'];
|
|
}
|
|
} elseif ($tableName === 'pages') {
|
|
$pageId = $data['uid'];
|
|
} else {
|
|
$pageId = $data['pid'];
|
|
}
|
|
|
|
return (int)$pageId;
|
|
}
|
|
|
|
/**
|
|
* Returns the backend layout which should be used for this page.
|
|
*
|
|
* @return false|string Identifier of the backend layout to be used, or FALSE if none
|
|
*/
|
|
protected function getSelectedCombinedIdentifier(int $pageId): string|false
|
|
{
|
|
$selectedCombinedIdentifiers = $this->runtimeCache->get(self::SELECTED_COMBINED_CACHE_IDENTIFIER);
|
|
if (is_array($selectedCombinedIdentifiers) && array_key_exists($pageId, $selectedCombinedIdentifiers)) {
|
|
return $selectedCombinedIdentifiers[$pageId];
|
|
}
|
|
if (!is_array($selectedCombinedIdentifiers)) {
|
|
$selectedCombinedIdentifiers = [];
|
|
}
|
|
// If it not set check the rootline for a layout on next level and use this: Rootline
|
|
// starts with current page and has page "0" at the end.
|
|
$rootLine = BackendUtility::BEgetRootLine($pageId, '', true);
|
|
if ($rootLine === []) {
|
|
// Return for invalid rootline
|
|
return false;
|
|
}
|
|
// Use first element as current page and remove last element (root page / pid=0)
|
|
$page = reset($rootLine);
|
|
array_pop($rootLine);
|
|
$selectedLayout = $this->pageLayoutResolver->getLayoutIdentifierForPage($page, $rootLine);
|
|
if ($selectedLayout === 'none') {
|
|
// If it is set to "none" - don't use any
|
|
$selectedLayout = false;
|
|
} elseif ($selectedLayout === 'default') {
|
|
$selectedLayout = '0';
|
|
}
|
|
$selectedCombinedIdentifiers[$pageId] = $selectedLayout;
|
|
$this->runtimeCache->set(self::SELECTED_COMBINED_CACHE_IDENTIFIER, $selectedCombinedIdentifiers);
|
|
return $selectedLayout;
|
|
}
|
|
}
|