TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,137 @@
<?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\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceException;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Provide styling for backend authentication forms, customized via extension configuration.
*
* @internal
*/
#[Autoconfigure(public: true)]
readonly class AuthenticationStyleInformation
{
public function __construct(
private ExtensionConfiguration $extensionConfiguration,
private LoggerInterface $logger,
private SystemResourceFactory $resourceFactory,
private SystemResourcePublisherInterface $resourcePublisher,
) {}
public function getBackgroundImageStyles(ServerRequestInterface $request): string
{
$backgroundImageResource = (string)($this->getBackendExtensionConfiguration()['loginBackgroundImage'] ?? '');
if ($backgroundImageResource === '') {
return '';
}
try {
$backgroundImageUri = (string)$this->resourcePublisher->generateUri(
$this->resourceFactory->createPublicResource($backgroundImageResource),
$request,
);
} catch (SystemResourceException) {
$this->logger->warning('The configured TYPO3 backend login background image "{image_resource}" can\'t be resolved. Please check if the file exists and the extension is activated.', [
'image_resource' => $backgroundImageResource,
]);
return '';
}
return '
.typo3-login-carousel-control.right,
.typo3-login-carousel-control.left,
.card-login { border: 0; }
.typo3-login { background-image: url("' . GeneralUtility::sanitizeCssVariableValue($backgroundImageUri) . '"); }
.typo3-login-footnote { background-color: #000000; color: #ffffff; }
';
}
public function getHighlightColorStyles(): string
{
$highlightColor = (string)($this->getBackendExtensionConfiguration()['loginHighlightColor'] ?? '');
if ($highlightColor === '') {
return '';
}
$highlightColor = GeneralUtility::sanitizeCssVariableValue($highlightColor);
return '
.typo3-login {
--typo3-login-highlight: ' . $highlightColor . ';
}
.btn-login {
--typo3-btn-color: #fff;
--typo3-btn-bg: ' . $highlightColor . ';
--typo3-btn-border-color: hsl(from ' . $highlightColor . ' h s calc(l - 5));
--typo3-btn-hover-color: #fff;
--typo3-btn-hover-bg: hsl(from ' . $highlightColor . ' h s calc(l - 3));
--typo3-btn-hover-border-color: hsl(from ' . $highlightColor . ' h s calc(l - 8));
--typo3-btn-focus-color: #fff;
--typo3-btn-focus-bg: hsl(from ' . $highlightColor . ' h s calc(l - 6));
--typo3-btn-focus-border-color: hsl(from ' . $highlightColor . ' h s calc(l - 11));
--typo3-btn-disabled-color: #fff;
--typo3-btn-disabled-bg: ' . $highlightColor . ';
--typo3-btn-disabled-border-color: hsl(from ' . $highlightColor . ' h s calc(l - 5));
}
.card-login .card-footer { border-color: ' . $highlightColor . '; }
';
}
public function getFooterNote(): string
{
$footerNote = (string)($this->getBackendExtensionConfiguration()['loginFootnote'] ?? '');
if ($footerNote === '') {
return '';
}
return strip_tags(trim($footerNote));
}
public function getLogo(): ?PublicResourceInterface
{
$logoIdentifier = $this->getBackendExtensionConfiguration()['loginLogo'] ?? '';
if ($logoIdentifier === '') {
return null;
}
try {
return $this->resourceFactory->createPublicResource($logoIdentifier);
} catch (SystemResourceException) {
return null;
}
}
public function getLogoAlt(): string
{
return trim((string)($this->getBackendExtensionConfiguration()['loginLogoAlt'] ?? ''));
}
public function getDefaultLogo(): PublicResourceInterface
{
return $this->resourceFactory->createPublicResource('PKG:typo3/cms-core:Resources/Public/Images/typo3_variable.svg');
}
protected function getBackendExtensionConfiguration(): array
{
return (array)$this->extensionConfiguration->get('backend');
}
}
@@ -0,0 +1,173 @@
<?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\BackendLayout;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class to represent a backend layout.
*/
class BackendLayout
{
protected string $identifier;
protected string $title;
protected string $description = '';
protected string $iconPath = '';
protected string $configuration = '';
/**
* The structured data of the configuration represented as array.
*/
protected array $structure = [];
protected array $data = [];
public static function create(string $identifier, string $title, string|array $configuration): BackendLayout
{
return GeneralUtility::makeInstance(
static::class,
$identifier,
$title,
$configuration
);
}
public function __construct(string $identifier, string $title, string|array $configuration)
{
$this->setIdentifier($identifier);
$this->setTitle($title);
if (is_array($configuration)) {
$this->structure = $configuration;
$this->configuration = $configuration['config'] ?? '';
} else {
$this->setConfiguration($configuration);
}
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getIdentifierCleaned(): string
{
return strtolower((string)preg_replace('/[^a-zA-Z0-9_-]/', '', $this->identifier));
}
public function setIdentifier(string $identifier): void
{
if (str_contains($identifier, '__')) {
throw new \UnexpectedValueException(
'Identifier "' . $identifier . '" must not contain "__"',
1381597630
);
}
$this->identifier = $identifier;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getIconPath(): string
{
return $this->iconPath;
}
public function setIconPath(string $iconPath): void
{
$this->iconPath = $iconPath;
}
public function getConfiguration(): string
{
return $this->configuration;
}
public function setConfiguration(string $configuration): void
{
$this->configuration = $configuration;
$this->structure = GeneralUtility::makeInstance(BackendLayoutView::class)->parseStructure($this);
}
/**
* Returns the columns registered for this layout as $key => $value pair where the key is the colPos
* and the value is the title.
* "1" => "Left" etc.
* Please note that the title can contain LLL references ready for translation.
*/
public function getUsedColumns(): array
{
return $this->structure['usedColumns'] ?? [];
}
public function getColCount(): int
{
return $this->structure['colCount'] ?? 0;
}
public function getRowCount(): int
{
return $this->structure['rowCount'] ?? 0;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): void
{
$this->data = $data;
}
public function setStructure(array $structure): void
{
$this->structure = $structure;
}
public function getStructure(): array
{
return $this->structure;
}
/**
* @return string[]
*/
public function getColumnPositionNumbers(): array
{
return $this->structure['__colPosList'] ?? [];
}
}
@@ -0,0 +1,117 @@
<?php
/*
* 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\BackendLayout;
/**
* Collection of backend layouts.
*/
class BackendLayoutCollection
{
/**
* @var string
*/
protected $identifier;
/**
* @var array|BackendLayout[]
*/
protected $backendLayouts = [];
/**
* @param string $identifier
*/
public function __construct($identifier)
{
$this->setIdentifier($identifier);
}
/**
* @return string
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* @param string $identifier
* @throws \UnexpectedValueException
*/
public function setIdentifier($identifier)
{
if (str_contains($identifier, '__')) {
throw new \UnexpectedValueException(
'Identifier "' . $identifier . '" must not contain "__"',
1381597631
);
}
$this->identifier = $identifier;
}
/**
* Adds a backend layout to this collection.
*
* @throws \LogicException
*/
public function add(BackendLayout $backendLayout)
{
$identifier = $backendLayout->getIdentifier();
if (str_contains($identifier, '__')) {
throw new \UnexpectedValueException(
'BackendLayout Identifier "' . $identifier . '" must not contain "__"',
1381597628
);
}
if (isset($this->backendLayouts[$identifier])) {
throw new \LogicException(
'Backend Layout ' . $identifier . ' is already defined',
1381559376
);
}
$this->backendLayouts[$identifier] = $backendLayout;
}
/**
* Gets a backend layout by (regular) identifier.
*
* @param string $identifier
* @return BackendLayout|null
*/
public function get($identifier)
{
$backendLayout = null;
if (isset($this->backendLayouts[$identifier])) {
$backendLayout = $this->backendLayouts[$identifier];
}
return $backendLayout;
}
/**
* Gets all backend layouts in this collection.
*
* @return array|BackendLayout[]
*/
public function getAll()
{
return $this->backendLayouts;
}
}
@@ -0,0 +1,306 @@
<?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\BackendLayout;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent;
use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
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\QueryHelper;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Class responsible for fetching the content data related to a BackendLayout
*
* - Reads content records
* - Performs workspace overlay on records
* - Capable of returning all records in active language as flat array
* - Capable of returning records for a given column in a given (optional) language
* - Capable of returning translation data (brief info about translation consistency)
*
* @internal
*/
#[Autoconfigure(public: true)]
readonly class ContentFetcher
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
#[Autowire(service: 'cache.runtime')]
private FrontendInterface $runtimeCache,
private ConnectionPool $connectionPool,
private TcaSchemaFactory $tcaSchemaFactory,
private FlashMessageService $flashMessageService,
private BackendLayoutView $backendLayoutView,
) {}
/**
* Gets content records per column.
* This is required for correct workspace overlays.
*
* @return array Associative array for each column (colPos) or for all columns if $columnNumber is null
*/
public function getContentRecordsPerColumn(PageLayoutContext $pageLayoutContext, ?int $columnNumber = null, ?int $languageId = null): array
{
$languageId = $languageId ?? $pageLayoutContext->getSiteLanguage()->getLanguageId();
$cachedFetchedContentRecords = $this->runtimeCache->get('ContentFetcher_fetchedContentRecords') ?: [];
if (empty($cachedFetchedContentRecords)) {
$fetchedContentRecords = [];
$isLanguageComparisonMode = $pageLayoutContext->getDrawingConfiguration()->isLanguageComparisonMode();
$queryBuilder = $this->getQueryBuilder($pageLayoutContext);
$result = $queryBuilder->executeQuery();
$records = $this->getResult($result);
foreach ($records as $record) {
$recordLanguage = $record['language_tag'] ?? '';
$recordColumnNumber = (int)$record['colPos'];
if ($recordLanguage === -1) {
// Record is set to "all languages", place it according to view mode.
if ($isLanguageComparisonMode) {
// Force the record to only be shown in default language in "Languages" view mode.
$recordLanguage = 0;
} else {
// Force the record to be shown in the currently active language in "Columns" view mode.
$recordLanguage = $languageId;
}
}
$fetchedContentRecords[$recordLanguage][$recordColumnNumber][] = $record;
}
$this->runtimeCache->set('ContentFetcher_fetchedContentRecords', $fetchedContentRecords);
} else {
$fetchedContentRecords = $cachedFetchedContentRecords;
}
$contentByLanguage = $fetchedContentRecords[$languageId] ?? [];
if ($columnNumber === null) {
return $contentByLanguage;
}
return $contentByLanguage[$columnNumber] ?? [];
}
public function getFlatContentRecords(PageLayoutContext $pageLayoutContext, int $languageId): iterable
{
$contentRecords = $this->getContentRecordsPerColumn($pageLayoutContext, null, $languageId);
return empty($contentRecords) ? [] : array_merge(...$contentRecords);
}
/**
* Allows to decide via an Event whether a custom type has children which were rendered or should not be rendered.
*/
public function getUnusedRecords(PageLayoutContext $pageLayoutContext): iterable
{
$unrendered = [];
$recordIdentityMap = $pageLayoutContext->getRecordIdentityMap();
$languageId = $pageLayoutContext->getDrawingConfiguration()->getPrimaryLanguageId();
// @todo consider to invoke the identity-map much earlier (to avoid fetching database records again)
foreach ($this->getContentRecordsPerColumn($pageLayoutContext, null, $languageId) as $contentRecordsInColumn) {
foreach ($contentRecordsInColumn as $contentRecord) {
$used = $recordIdentityMap->hasIdentifier('tt_content', (int)$contentRecord['uid']);
// A hook mentioned that this record is used somewhere, so this is in fact "rendered" already
$event = new IsContentUsedOnPageLayoutEvent($contentRecord, $used, $pageLayoutContext);
$event = $this->eventDispatcher->dispatch($event);
if (!$event->isRecordUsed()) {
$unrendered[] = $contentRecord;
}
}
}
return $unrendered;
}
public function getTranslationData(PageLayoutContext $pageLayoutContext, iterable $contentElements, int $language): array
{
if ($language === 0) {
return [];
}
$languageTranslationInfo = $this->runtimeCache->get('ContentFetcher_TranslationInfo_' . $language) ?: [];
if (empty($languageTranslationInfo)) {
$contentRecordsInDefaultLanguage = $this->getContentRecordsPerColumn($pageLayoutContext, null, 0);
if (!empty($contentRecordsInDefaultLanguage)) {
$contentRecordsInDefaultLanguage = array_merge(...$contentRecordsInDefaultLanguage);
}
$untranslatedRecordUids = array_flip(
array_column(
// Eliminate records with "-1" as sys_language_uid since they can not be translated
array_filter($contentRecordsInDefaultLanguage, static function (array $record): bool {
return true;
}),
'uid'
)
);
foreach ($contentElements as $contentElement) {
if (($contentElement['language_tag'] ?? '') === '') {
continue;
}
if ((int)$contentElement['l18n_parent'] === 0) {
$languageTranslationInfo['hasStandAloneContent'] = true;
$languageTranslationInfo['mode'] = 'free';
}
if ((int)$contentElement['l18n_parent'] > 0) {
$languageTranslationInfo['hasTranslations'] = true;
$languageTranslationInfo['mode'] = 'connected';
}
if ((int)$contentElement['l10n_source'] > 0) {
unset($untranslatedRecordUids[(int)$contentElement['l10n_source']]);
}
}
if (!isset($languageTranslationInfo['hasTranslations'])) {
$languageTranslationInfo['hasTranslations'] = false;
}
foreach ($untranslatedRecordUids as $uid => $index) {
$contentElementInDefaultLanguage = $contentRecordsInDefaultLanguage[$index];
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage($contentElementInDefaultLanguage['CType'], $contentElementInDefaultLanguage['colPos'], $contentElementInDefaultLanguage['pid'])) {
unset($untranslatedRecordUids[$uid]);
}
}
$untranslatedRecordUidsWithoutWorkspaceDeletedRecords = $this->removeWorkspaceDeletedPlaceholdersUidsFromUntranslatedRecordUids($pageLayoutContext, array_keys($untranslatedRecordUids), $language);
$languageTranslationInfo['untranslatedRecordUids'] = $untranslatedRecordUidsWithoutWorkspaceDeletedRecords;
if (array_keys($untranslatedRecordUids) !== $untranslatedRecordUidsWithoutWorkspaceDeletedRecords) {
$languageTranslationInfo['hasElementsWithWorkspaceDeletePlaceholders'] = true;
}
// Check for inconsistent translations, force "mixed" mode and dispatch a FlashMessage to user if such a case is encountered.
if (isset($languageTranslationInfo['hasStandAloneContent'])
&& $languageTranslationInfo['hasTranslations']
) {
$languageTranslationInfo['mode'] = 'mixed';
// We do not want to show the staleTranslationWarning if allowInconsistentLanguageHandling is enabled
if (!$pageLayoutContext->getDrawingConfiguration()->getAllowInconsistentLanguageHandling()) {
$siteLanguage = $pageLayoutContext->getSiteLanguage($language);
$languageService = $this->getLanguageService();
$message = FlashMessage::createFromArray([
'message' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleTranslationWarning'),
'title' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleTranslationWarningTitle'), $siteLanguage->getTitle()),
'severity' => ContextualFeedbackSeverity::WARNING->value,
]);
$queue = $this->flashMessageService->getMessageQueueByIdentifier();
$queue->addMessage($message);
}
}
$this->runtimeCache->set('ContentFetcher_TranslationInfo_' . $language, $languageTranslationInfo);
}
return $languageTranslationInfo;
}
protected function removeWorkspaceDeletedPlaceholdersUidsFromUntranslatedRecordUids(PageLayoutContext $pageLayoutContext, array $untranslatedRecordUids, int $language): array
{
if ($this->getBackendUser()->workspace <= 0) {
// Early return if we're not in a workspace to suppress some queries.
return $untranslatedRecordUids;
}
$queryBuilder = $this->getQueryBuilder($pageLayoutContext);
$queryBuilder->andWhere(
$queryBuilder->expr()->and(
$queryBuilder->expr()->in(
'l18n_parent',
$queryBuilder->createNamedParameter($untranslatedRecordUids, Connection::PARAM_INT_ARRAY)
),
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createNamedParameter($language, Connection::PARAM_INT)
)
)
);
$result = $queryBuilder->executeQuery();
$uidsToRemoveFromUntranslatedRecordUids = [];
while ($row = $result->fetchAssociative()) {
BackendUtility::workspaceOL('tt_content', $row, -99, true);
if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
$uidsToRemoveFromUntranslatedRecordUids[] = $row['l18n_parent'];
}
}
return array_diff($untranslatedRecordUids, $uidsToRemoveFromUntranslatedRecordUids);
}
protected function getQueryBuilder(PageLayoutContext $pageLayoutContext): QueryBuilder
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
$queryBuilder
->select('*')
->from('tt_content');
$queryBuilder->andWhere(
$queryBuilder->expr()->eq(
'tt_content.pid',
$queryBuilder->createNamedParameter($pageLayoutContext->getPageId(), Connection::PARAM_INT)
)
);
$schema = $this->tcaSchemaFactory->get('tt_content');
$sortBy = $schema->hasCapability(TcaSchemaCapability::SortByField) ? (string)$schema->getCapability(TcaSchemaCapability::SortByField) : '';
if ($sortBy === '' && $schema->hasCapability(TcaSchemaCapability::DefaultSorting)) {
$sortBy = (string)$schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue();
}
foreach (QueryHelper::parseOrderBy($sortBy) as $orderBy) {
$queryBuilder->addOrderBy($orderBy[0], $orderBy[1]);
}
$event = new ModifyDatabaseQueryForContentEvent($queryBuilder, 'tt_content', $pageLayoutContext->getPageId());
$event = $this->eventDispatcher->dispatch($event);
return $event->getQueryBuilder();
}
protected function getResult($result): array
{
$output = [];
while ($row = $result->fetchAssociative()) {
BackendUtility::workspaceOL('tt_content', $row, -99, true);
if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) {
$output[] = $row;
}
}
return $output;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
public function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,134 @@
<?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\BackendLayout;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Collection of backend layout data providers.
*/
class DataProviderCollection implements SingletonInterface
{
/**
* @var array<non-empty-string, DataProviderInterface>
*/
protected array $dataProviders = [];
protected array $results = [];
/**
* @param iterable<DataProviderInterface> $dataProviders
*/
public function __construct(
#[AutowireIterator('page_layout.data_provider')]
iterable $dataProviders = [],
) {
foreach ($dataProviders as $dataProvider) {
$this->validateDataProvider($dataProvider);
$identifier = $dataProvider->getIdentifier();
if (isset($this->dataProviders[$identifier])) {
throw new \LogicException(
sprintf(
'A backend layout data provider with identifier "%s" is already registered.',
$identifier
),
1762361129,
);
}
$this->dataProviders[$identifier] = $dataProvider;
}
}
protected function validateDataProvider(mixed $dataProvider): void
{
if (!($dataProvider instanceof DataProviderInterface)) {
throw new \LogicException(
sprintf(
'Data provider must implement interface %s, %s given.',
DataProviderInterface::class,
get_debug_type($dataProvider),
),
1381269811,
);
}
$identifier = $dataProvider->getIdentifier();
if (str_contains($identifier, '__')) {
throw new \UnexpectedValueException('Identifier "' . $identifier . '" must not contain "__"', 1381597629);
}
}
/**
* Gets all backend layout collections and thus, all
* backend layouts. Each data provider returns its own
* backend layout collection.
*
* @return BackendLayoutCollection[]
*/
public function getBackendLayoutCollections(DataProviderContext $dataProviderContext): array
{
$result = [];
foreach ($this->dataProviders as $identifier => $dataProvider) {
$backendLayoutCollection = $this->createBackendLayoutCollection($identifier);
$dataProvider->addBackendLayouts($dataProviderContext, $backendLayoutCollection);
$result[$identifier] = $backendLayoutCollection;
}
return $result;
}
/**
* Gets a backend layout by a combined identifier, which is
* e.g. "myextension_regular" and "myextension" is the identifier
* of the accordant data provider and "regular" the identifier of
* the accordant backend layout.
*/
public function getBackendLayout(string $combinedIdentifier, int $pageId): ?BackendLayout
{
$backendLayout = null;
if (!str_contains($combinedIdentifier, '__')) {
$dataProviderIdentifier = 'default';
$backendLayoutIdentifier = $combinedIdentifier;
} else {
[$dataProviderIdentifier, $backendLayoutIdentifier] = explode('__', $combinedIdentifier, 2);
}
if (isset($this->dataProviders[$dataProviderIdentifier])) {
$backendLayout = $this->dataProviders[$dataProviderIdentifier]->getBackendLayout($backendLayoutIdentifier, $pageId);
}
return $backendLayout;
}
/**
* Creates a new backend layout collection.
*/
protected function createBackendLayoutCollection(string $identifier): BackendLayoutCollection
{
return GeneralUtility::makeInstance(
BackendLayoutCollection::class,
$identifier
);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View\BackendLayout;
/**
* Context that is forwarded to backend layout data providers.
*/
final readonly class DataProviderContext
{
public function __construct(
public int $pageId,
public string $tableName,
public string $fieldName,
public array $data,
public array $pageTsConfig,
) {}
}
@@ -0,0 +1,55 @@
<?php
/*
* 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\BackendLayout;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
/**
* Interface for classes which hook into BackendLayoutDataProvider
* to provide additional backend layouts from various sources.
*/
#[AutoconfigureTag('page_layout.data_provider')]
interface DataProviderInterface
{
/**
* Adds backend layouts to the given backend layout collection.
*/
public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection);
/**
* Gets a backend layout by (regular) identifier.
*
* @param string $identifier
* @param int $pageId
* @return BackendLayout|null
*/
public function getBackendLayout($identifier, $pageId);
/**
* Returns the unique identifier for this backend layout data provider.
*
* This identifier is used to build combined identifiers in the format
* "providerIdentifier__layoutIdentifier" (e.g., "my_provider__my_layout").
*
* Requirements:
* - Must be a non-empty string
* - Must not contain double underscores "__" (reserved as separator)
* - Must be unique across all registered backend layout data providers
*
* @return non-empty-string The unique identifier for this data provider
*/
public function getIdentifier(): string;
}
@@ -0,0 +1,267 @@
<?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\BackendLayout;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Backend layout data provider class
*
* @internal Specific DataProviderInterface implementation, not considered public API.
*/
readonly class DefaultDataProvider implements DataProviderInterface
{
private const string DEFAULT_COLUMNS_LAYOUT = '
backend_layout {
colCount = 1
rowCount = 1
rows {
1 {
columns {
1 {
name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.1
colPos = 0
identifier = main
}
}
}
}
}
';
public function __construct(
private FileRepository $fileRepository,
private ConnectionPool $connectionPool,
private TcaSchemaFactory $tcsSchemaFactory,
private Context $context,
) {}
/**
* Adds backend layouts to the given backend layout collection.
* The default backend layout ('default_default') is not added
* since it's the default fallback if nothing is specified.
*/
public function addBackendLayouts(
DataProviderContext $dataProviderContext,
BackendLayoutCollection $backendLayoutCollection
): void {
$layoutData = $this->getLayoutData(
$dataProviderContext->fieldName,
$dataProviderContext->pageTsConfig,
$dataProviderContext->pageId
);
foreach ($layoutData as $data) {
$backendLayout = $this->createBackendLayout($data);
$backendLayoutCollection->add($backendLayout);
}
}
/**
* Gets a backend layout by (regular) identifier.
*
* @param string|int $identifier
* @param int $pageId
*/
public function getBackendLayout($identifier, $pageId): ?BackendLayout
{
$backendLayout = null;
if ((string)$identifier === 'default') {
return $this->createDefaultBackendLayout();
}
$data = BackendUtility::getRecordWSOL('backend_layout', (int)$identifier);
if (is_array($data)) {
$backendLayout = $this->createBackendLayout($data);
}
return $backendLayout;
}
public function getIdentifier(): string
{
return 'default';
}
/**
* Creates a backend layout with the default configuration.
*/
protected function createDefaultBackendLayout(): BackendLayout
{
return BackendLayout::create(
'default',
'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.backend_layout.default',
self::DEFAULT_COLUMNS_LAYOUT
);
}
/**
* Creates a new backend layout using the given record data.
*/
protected function createBackendLayout(array $data): BackendLayout
{
$backendLayout = BackendLayout::create((string)$data['uid'], $data['title'], $data['config']);
$backendLayout->setIconPath($this->getIconPath($data));
$backendLayout->setData($data);
return $backendLayout;
}
/**
* Resolves the icon from the database record
*/
protected function getIconPath(array $icon): string
{
$references = $this->fileRepository->findByRelation('backend_layout', 'icon', (int)$icon['uid']);
if (!empty($references)) {
$icon = reset($references);
return $icon->getPublicUrl() ?? '';
}
return '';
}
/**
* Get all layouts from the core's default data provider.
*
* @param string $fieldName the name of the field the layouts are provided for (either backend_layout or backend_layout_next_level)
* @param array $pageTsConfig PageTSconfig of the given page
* @param int $pageUid the ID of the page wea re getting the layouts for
* @return array $layouts A collection of layout data of the registered provider
*/
protected function getLayoutData(string $fieldName, array $pageTsConfig, int $pageUid): array
{
// @todo: This depends on backend_layout TCA being available for both the query and
// TcaSchemaFactory. backend_layout TCA comes from ext:frontend, so we have
// an indirect cross dependency between ext:backend and ext:frontend here.
// There should be an explicit exception here when core is decoupled to run
// an instance with ext:backend but without ext:frontend, or backend_layout TCA
// should be relocated to ext:core? There are probably many more cases like this.
$storagePid = $this->getStoragePid($pageTsConfig);
$pageTsConfigId = $this->getPageTSconfigIds($pageTsConfig);
// Add layout records
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('backend_layout');
$queryBuilder->getRestrictions()
->add(
GeneralUtility::makeInstance(
WorkspaceRestriction::class,
$this->context->getPropertyFromAspect('workspace', 'id')
)
);
$queryBuilder
->select('*')
->from('backend_layout')
->where(
$queryBuilder->expr()->or(
$queryBuilder->expr()->and(
$queryBuilder->expr()->comparison(
$queryBuilder->createNamedParameter($pageTsConfigId[$fieldName], Connection::PARAM_INT),
ExpressionBuilder::EQ,
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
),
$queryBuilder->expr()->comparison(
$queryBuilder->createNamedParameter($storagePid, Connection::PARAM_INT),
ExpressionBuilder::EQ,
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
)
),
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq(
'backend_layout.pid',
$queryBuilder->createNamedParameter($pageTsConfigId[$fieldName], Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'backend_layout.pid',
$queryBuilder->createNamedParameter($storagePid, Connection::PARAM_INT)
)
),
$queryBuilder->expr()->and(
$queryBuilder->expr()->comparison(
$queryBuilder->createNamedParameter($pageTsConfigId[$fieldName], Connection::PARAM_INT),
ExpressionBuilder::EQ,
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'backend_layout.pid',
$queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)
)
)
)
);
// Not catching UndefinedSchemaException here since backend_layout must exist at
// this point, or the entire query would fail already.
$schema = $this->tcsSchemaFactory->get('backend_layout');
if ($schema->hasCapability(TcaSchemaCapability::SortByField)) {
$queryBuilder->orderBy((string)$schema->getCapability(TcaSchemaCapability::SortByField));
}
$statement = $queryBuilder->executeQuery();
$results = [];
while ($record = $statement->fetchAssociative()) {
BackendUtility::workspaceOL('backend_layout', $record);
if (is_array($record)) {
$results[$record['t3ver_oid'] ?: $record['uid']] = $record;
}
}
return $results;
}
/**
* Returns the storage PID from TCEFORM.
*/
protected function getStoragePid(array $pageTsConfig): int
{
$storagePid = 0;
if (!empty($pageTsConfig['TCEFORM.']['pages.']['_STORAGE_PID'])) {
$storagePid = (int)$pageTsConfig['TCEFORM.']['pages.']['_STORAGE_PID'];
}
return $storagePid;
}
/**
* Returns the page TSconfig from TCEFORM.
*/
protected function getPageTSconfigIds(array $pageTsConfig): array
{
$pageTsConfigIds = [
'backend_layout' => 0,
'backend_layout_next_level' => 0,
];
if (!empty($pageTsConfig['TCEFORM.']['pages.']['backend_layout.']['PAGE_TSCONFIG_ID'])) {
$pageTsConfigIds['backend_layout'] = (int)$pageTsConfig['TCEFORM.']['pages.']['backend_layout.']['PAGE_TSCONFIG_ID'];
}
if (!empty($pageTsConfig['TCEFORM.']['pages.']['backend_layout_next_level.']['PAGE_TSCONFIG_ID'])) {
$pageTsConfigIds['backend_layout_next_level'] = (int)$pageTsConfig['TCEFORM.']['pages.']['backend_layout_next_level.']['PAGE_TSCONFIG_ID'];
}
return $pageTsConfigIds;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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\BackendLayout\Grid;
use TYPO3\CMS\Backend\View\PageLayoutContext;
/**
* Grid
*
* Main rows-and-columns structure representing the rows and columns of
* a BackendLayout in object form. Contains getter methods to return rows
* and sum of "colspan" values assigned to columns in rows.
*
* Contains a tree of grid-related objects:
*
* - Grid
* - GridRow
* - GridColumn
* - GridColumnItem (one per record)
*
* Accessed in Fluid templates.
*
* @internal
*/
class Grid
{
/**
* @var GridRow[]
*/
protected array $rows = [];
public function __construct(
protected readonly PageLayoutContext $context,
) {}
public function getContext(): PageLayoutContext
{
return $this->context;
}
public function addRow(GridRow $row): void
{
$this->rows[] = $row;
}
/**
* @return GridRow[]
*/
public function getRows(): iterable
{
return $this->rows;
}
public function getColumns(): iterable
{
$columns = [];
foreach ($this->rows as $gridRow) {
$columns += $gridRow->getColumns();
}
return $columns;
}
public function getSpan(): int
{
if (!isset($this->rows[0])
|| ($this->context->getDrawingConfiguration()->isLanguageComparisonMode()
&& count($this->context->getDrawingConfiguration()->getSelectedLanguageIds()) > 1)
) {
return 1;
}
$span = 0;
foreach ($this->rows[0]->getColumns() as $column) {
$span += $column->getColSpan();
}
return $span ?: 1;
}
}
@@ -0,0 +1,259 @@
<?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\BackendLayout\Grid;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\ContentSlideMode;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Grid Column
*
* Object representation (model/proxy) for a single column from a grid defined
* in a BackendLayout. Stores GridColumnItem representations of content records
* and provides getter methods which return various properties associated with
* a single column, e.g. the "edit all elements in content" URL and the "add
* new content element" URL of the button that is placed in the top of columns
* in the page layout.
*
* Accessed from Fluid templates.
*
* @internal
*/
class GridColumn
{
/**
* @var GridColumnItem[]
*/
protected array $items = [];
protected readonly ?int $columnNumber;
protected readonly string $columnName;
protected readonly string $icon;
protected readonly int $colSpan;
protected readonly int $rowSpan;
protected readonly ?string $identifier;
protected readonly ContentSlideMode $slideMode;
/**
* @param array<string, mixed> $definition
*/
public function __construct(
protected readonly PageLayoutContext $context,
protected readonly array $definition,
protected readonly string $table = 'tt_content'
) {
$this->columnNumber = isset($definition['colPos']) ? (int)$definition['colPos'] : null;
$this->columnName = (string)($definition['name'] ?? 'default');
$this->icon = (string)($definition['icon'] ?? '');
$this->colSpan = (int)($definition['colspan'] ?? 1);
$this->rowSpan = (int)($definition['rowspan'] ?? 1);
$this->identifier = isset($definition['identifier']) ? (string)$definition['identifier'] : null;
$this->slideMode = ContentSlideMode::tryFrom($definition['slideMode'] ?? null);
}
public function getContext(): PageLayoutContext
{
return $this->context;
}
/**
* @return array<string, mixed>
*/
public function getDefinition(): array
{
return $this->definition;
}
public function isActive(): bool
{
return $this->columnNumber !== null && in_array($this->columnNumber, $this->context->getDrawingConfiguration()->getActiveColumns());
}
public function addItem(GridColumnItem $item): void
{
$this->items[] = $item;
}
/**
* @return GridColumnItem[]
*/
public function getItems(): iterable
{
return $this->items;
}
public function getColumnNumber(): ?int
{
return $this->columnNumber;
}
public function getColumnName(): string
{
return $this->columnName;
}
public function getIcon(): string
{
return $this->icon;
}
public function getColSpan(): int
{
if ($this->context->getDrawingConfiguration()->isLanguageComparisonMode()) {
return 1;
}
return $this->colSpan;
}
public function getRowSpan(): int
{
if ($this->context->getDrawingConfiguration()->isLanguageComparisonMode()) {
return 1;
}
return $this->rowSpan;
}
public function getIdentifier(): ?string
{
return $this->identifier;
}
public function getIdentifierCleaned(): string
{
return strtolower((string)preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$this->identifier));
}
public function getSlideMode(): ContentSlideMode
{
return $this->slideMode;
}
/**
* @return int[]
*/
public function getAllContainedItemUids(): array
{
$uids = [];
foreach ($this->items as $columnItem) {
$uids[] = $columnItem->getRecord()->getUid();
}
return $uids;
}
public function getEditUrl(): ?string
{
if (empty($this->items)) {
return null;
}
$pageRecord = $this->context->getPageRecord();
if (!$this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
|| !$this->getBackendUser()->checkLanguageAccess($this->context->getSiteLanguage())) {
return null;
}
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return (string)$uriBuilder->buildUriFromRoute('record_edit', [
'edit' => [
$this->table => [
implode(',', $this->getAllContainedItemUids()) => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl(),
]);
}
public function getNewContentUrl(): string
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$pageId = $this->context->getPageId();
return (string)$uriBuilder->buildUriFromRoute('new_content_element_wizard', [
'id' => $pageId,
'sys_language_uid' => $this->context->getSiteLanguage()->getLanguageId(),
'colPos' => $this->getColumnNumber(),
'uid_pid' => $pageId,
'returnUrl' => $this->context->getReturnUrl(),
]);
}
public function getTitle(): string
{
$columnNumber = $this->getColumnNumber();
$colTitle = '';
foreach ($this->context->getBackendLayout()->getUsedColumns() as $colPos => $title) {
if ($colPos === $columnNumber) {
$colTitle = $this->getLanguageService()->sL($title);
}
}
return $colTitle;
}
public function getTitleInaccessible(): string
{
return $this->getLanguageService()->sL($this->columnName) . ' (' . $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:noAccess') . ')';
}
public function getTitleUnassigned(): string
{
return $this->getLanguageService()->sL($this->columnName) . ' (' . $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:notAssigned') . ')';
}
public function isUnassigned(): bool
{
return $this->columnName !== 'unused' && $this->columnNumber === null;
}
public function isUnused(): bool
{
return $this->columnName === 'unused' && $this->columnNumber === null;
}
public function isContentEditable(): bool
{
if ($this->columnName === 'unused' || $this->columnNumber === null) {
return false;
}
if ($this->getBackendUser()->isAdmin()) {
return true;
}
$pageRecord = $this->context->getPageRecord();
return $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
&& $this->getBackendUser()->checkLanguageAccess($this->context->getSiteLanguage())
&& (
!($pagesSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$pagesSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,475 @@
<?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\BackendLayout\Grid;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent;
use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\SchemaLabelResolver;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Grid Column Item
*
* Model/proxy around a single record which appears in a grid column
* in the page layout. Returns titles, urls etc. and performs basic
* assertions on the contained content element record such as
* is-versioned, is-editable, is-delible and so on.
*
* Accessed from Fluid templates.
*
* @internal
*/
class GridColumnItem
{
/**
* @var GridColumnItem[]
*/
protected array $translations = [];
protected TcaSchema $schema;
protected BackendLayoutView $backendLayoutView;
protected readonly IconFactory $iconFactory;
public function __construct(
protected readonly PageLayoutContext $context,
protected readonly GridColumn $column,
protected RecordInterface $record,
protected readonly string $table = 'tt_content'
) {
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->table);
$this->backendLayoutView = GeneralUtility::makeInstance(BackendLayoutView::class);
}
public function getContext(): PageLayoutContext
{
return $this->context;
}
public function isVersioned(): bool
{
return $this->record->getComputedProperties()->getVersionedUid() > 0 || (int)($this->getRow()['t3ver_state'] ?? 0) !== 0;
}
public function getPreview(): string
{
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class)->resolveRendererFor($this->record);
$previewHeader = $previewRenderer->renderPageModulePreviewHeader($this);
// Dispatch event to allow listeners adding an alternative content type
// specific preview or to manipulate the content elements' record data.
$event = $eventDispatcher->dispatch(
new PageContentPreviewRenderingEvent($this->table, $this->getRecordType(), $this->record, $this->context)
);
// Update the modified record data
$this->record = $event->getRecord();
// Get specific preview from listeners. In case non was added,
// fall back to the standard preview rendering workflow.
$previewContent = $event->getPreviewContent();
if ($previewContent === null) {
$previewContent = $previewRenderer->renderPageModulePreviewContent($this);
}
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage(
$this->getRecordType(),
$this->getColumn()->getColumnNumber() ?? 0,
$this->getRecord()->getPid()
)) {
return '<span class="badge badge-warning">' . sprintf($this->getLanguageService()->sL('core.core:labels.typeNotAllowedInColumn'), $this->getContentTypeLabel()) . '</span>';
}
$previewContent = $previewRenderer->wrapPageModulePreview($previewHeader, $previewContent, $this);
$event = $eventDispatcher->dispatch(
new AfterPageContentPreviewRenderedEvent($this->table, $this->getRecordType(), $this->record, $this->context, $previewContent)
);
return $event->getPreviewContent();
}
public function getWrapperClassName(): string
{
$wrapperClassNames = [];
if ($this->isDisabled()) {
$wrapperClassNames[] = 't3-page-ce-hidden t3js-hidden-record';
}
if ($this->isInconsistentLanguage()
|| !$this->backendLayoutView->isCTypeAllowedInColPosByPage(
$this->getRecordType(),
$this->getColumn()->getColumnNumber() ?? 0,
$this->getRecord()->getPid()
)
) {
$wrapperClassNames[] = 't3-page-ce-warning';
}
return implode(' ', $wrapperClassNames);
}
public function isDelible(): bool
{
$backendUser = $this->getBackendUser();
if (!$backendUser->doesUserHaveAccess($this->context->getPageRecord(), Permission::CONTENT_EDIT)) {
return false;
}
return !($backendUser->getTSConfig()['options.']['disableDelete.'][$this->table] ?? $backendUser->getTSConfig()['options.']['disableDelete'] ?? false);
}
public function getDeleteUrl(): string
{
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
'tce_db',
[
'cmd' => [
$this->table => [
$this->record->getUid() => [
'delete' => 1,
],
],
],
'redirect' => $this->context->getReturnUrl(),
]
);
}
public function getDeleteMessage(): string
{
$recordInfo = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->getRow()));
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$recordInfo .= ' [' . $this->table . ':' . $this->record->getUid() . ']';
}
$refCountMsg = BackendUtility::referenceCount(
$this->table,
$this->record->getUid(),
LF . $this->getLanguageService()->sL('core.core:labels.referencesToRecord'),
(string)$this->getReferenceCount($this->record->getUid())
);
$translationCount = count(GeneralUtility::makeInstance(LocalizationRepository::class)->getRecordTranslations($this->table, $this->record->getUid()));
if ($translationCount > 0) {
$refCountMsg .= LF . sprintf(
$this->getLanguageService()->sL('core.core:labels.translationsOfRecord'),
$translationCount
);
}
return sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:deleteWarning'), trim($recordInfo)) . $refCountMsg;
}
public function getFooterInfo(): string
{
$previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class)->resolveRendererFor($this->record);
return $previewRenderer->renderPageModulePreviewFooter($this);
}
public function getContentTypeLabel(): string
{
if (($recordType = $this->getRecordType()) === '') {
return '';
}
$contentTypeLabels = $this->context->getContentTypeLabels();
$contentTypeLabel = $contentTypeLabels[$recordType] ?? '';
if ($contentTypeLabel === '') {
$contentTypeLabel = $this->getLabelFromItemListMerged();
$contentTypeLabel = $this->getLanguageService()->sL($contentTypeLabel);
}
return $contentTypeLabel;
}
public function getIcons(): string
{
$row = $this->record->getRawRecord()?->toArray() ?? [];
$icons = [];
$icon = $this->iconFactory
->getIconForRecord($this->table, $row, IconSize::SMALL)
->setTitle(BackendUtility::getRecordIconAltText($row, $this->table, false))
->render();
if ($this->getBackendUser()->checkRecordEditAccess($this->table, $this->getRow())->isAllowed) {
$icon = BackendUtility::wrapClickMenuOnIcon($icon, $this->table, $this->record->getUid());
}
$icons[] = $icon;
if ($lockInfo = BackendUtility::isRecordLocked($this->table, $this->record->getUid())) {
$icons[] = '<a href="#" title="' . htmlspecialchars($lockInfo['msg']) . '">'
. $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render() . '</a>';
}
return implode(' ', $icons);
}
public function getSiteLanguage(): SiteLanguage
{
return $this->context->getSiteLanguage((int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()] ?? 0));
}
public function getRecord(): RecordInterface
{
return $this->record;
}
public function getRow(): array
{
return $this->record->getRawRecord()?->toArray(true) ?? [];
}
public function setRecord(RecordInterface $record): void
{
$this->record = $record;
}
public function getColumn(): GridColumn
{
return $this->column;
}
public function getTranslations(): array
{
return $this->translations;
}
public function addTranslation(int $languageId, GridColumnItem $translation): GridColumnItem
{
$this->translations[$languageId] = $translation;
return $this;
}
public function isDisabled(): bool
{
$row = $this->getRow();
return (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
&& ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)] ?? false)
)
|| (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)
&& ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionStartTime)] ?? 0) > $GLOBALS['EXEC_TIME']
)
|| (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)
&& (($endTime = ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionEndTime)] ?? 0)) !== 0 && $endTime < $GLOBALS['EXEC_TIME'])
);
}
public function isEditable(): bool
{
$backendUser = $this->getBackendUser();
if ($backendUser->isAdmin()) {
return true;
}
$pageRecord = $this->context->getPageRecord();
return $backendUser->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
&& $backendUser->checkRecordEditAccess($this->table, $this->record)->isAllowed
&& (
!($pagesSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$pagesSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
);
}
public function isDragAndDropAllowed(): bool
{
$pageRecord = $this->context->getPageRecord();
$typeColumn = $this->getTypeColumn();
return (int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) === 0
&& (
$this->getBackendUser()->isAdmin()
|| (
(
!($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
&& (
!($pagesSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$pagesSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
)
)
&& $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
&& $this->getBackendUser()->checkAuthMode($this->table, $typeColumn, $this->getRecordType())
)
)
;
}
public function isInconsistentLanguage(): bool
{
$allowInconsistentLanguageHandling = $this->context->getDrawingConfiguration()->getAllowInconsistentLanguageHandling();
return !$allowInconsistentLanguageHandling
&& $this->getSiteLanguage()->getLanguageId() !== 0
&& $this->context->getLanguageModeIdentifier() === 'mixed'
&& (int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) === 0;
}
public function getNewContentAfterUrl(): string
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return (string)$uriBuilder->buildUriFromRoute('new_content_element_wizard', [
'id' => $this->context->getPageId(),
'sys_language_uid' => $this->context->getSiteLanguage()->getLanguageId(),
'colPos' => $this->column->getColumnNumber(),
'uid_pid' => -$this->record->getUid(),
'returnUrl' => $this->context->getReturnUrl(),
]);
}
public function getVisibilityToggleUrl(): string
{
$disabledFieldName = $this->getDisabledFieldName();
if ($this->getRow()[$disabledFieldName] ?? false) {
$value = 0;
} else {
$value = 1;
}
return GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
'tce_db',
[
'data' => [
$this->table => [
$this->record->getComputedProperties()->getVersionedUid() ?: $this->record->getUid() => [
$disabledFieldName => $value,
],
],
],
'redirect' => $this->context->getReturnUrl(),
]
) . '#element-' . $this->table . '-' . $this->record->getUid();
}
public function getVisibilityToggleTitle(): string
{
if ($this->getRow()[$this->getDisabledFieldName()] ?? false) {
return $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:unHide');
}
return $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:hide');
}
public function getVisibilityToggleIconName(): string
{
return ($this->getRow()[$this->getDisabledFieldName()] ?? false) ? 'unhide' : 'hide';
}
public function isVisibilityToggling(): bool
{
$disabledFieldName = $this->getDisabledFieldName();
return $disabledFieldName
&& $this->schema->hasField($disabledFieldName)
&& (
!$this->schema->getField($disabledFieldName)->supportsAccessControl()
|| $this->getBackendUser()->check('non_exclude_fields', $this->table . ':' . $disabledFieldName)
)
;
}
public function getEditUrl(): string
{
$urlParameters = [
'edit' => [
$this->table => [
$this->record->getUid() => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl() . '#element-' . $this->table . '-' . $this->record->getUid(),
];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return $uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-' . $this->table . '-' . $this->record->getUid();
}
public function getContextualEditUrl(): string
{
$urlParameters = [
'edit' => [
$this->table => [
$this->record->getUid() => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl() . '#element-' . $this->table . '-' . $this->record->getUid(),
];
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit_contextual', $urlParameters);
}
public function getTypeColumn(): string
{
// @todo This only supports local record types due to usages in this class
return $this->schema->supportsSubSchema() && !$this->schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema()
? $this->schema->getSubSchemaTypeInformation()->getFieldName()
: '';
}
public function getRecordType(): string
{
return $this->record->getRecordType() ?? '';
}
public function getTable(): string
{
return $this->table;
}
/**
* Gets the number of records referencing the record with the UID $uid in
* the current table.
*
* @return int The number of references to record $uid in table
*/
protected function getReferenceCount(int $uid): int
{
return GeneralUtility::makeInstance(ReferenceIndex::class)->getNumberOfReferencedRecords($this->table, $uid);
}
protected function getLabelFromItemListMerged(): string
{
$table = $this->table;
$typeColumn = $this->getTypeColumn();
$recordType = $this->getRecordType();
$columnTsConfig = BackendUtility::getPagesTSconfig($this->record->getPid())['TCEFORM.'][$table . '.'][$typeColumn . '.'] ?? [];
return GeneralUtility::makeInstance(SchemaLabelResolver::class)
->getLabelForFieldValue($table, $typeColumn, $recordType, $this->getRow(), is_array($columnTsConfig) ? $columnTsConfig : []);
}
protected function getDisabledFieldName(): ?string
{
return $this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (string)$this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField) : null;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,60 @@
<?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\BackendLayout\Grid;
use TYPO3\CMS\Backend\View\PageLayoutContext;
/**
* Grid Row
*
* Object representation of a single row of a grid defined in a BackendLayout.
* Is solely responsible for grouping GridColumns.
*
* Accessed in Fluid templates.
*
* @internal
*/
class GridRow
{
/**
* @var GridColumn[]
*/
protected array $columns = [];
public function __construct(
protected readonly PageLayoutContext $context,
) {}
public function getContext(): PageLayoutContext
{
return $this->context;
}
public function addColumn(GridColumn $column): void
{
$this->columns[$column->getColumnNumber() ?? ''] = $column;
}
/**
* @return GridColumn[]
*/
public function getColumns(): iterable
{
return $this->columns;
}
}
@@ -0,0 +1,161 @@
<?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\BackendLayout\Grid;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Language Column
*
* Object representation of a site language selected in the "page" module
* to show translations of content elements.
*
* Contains getter methods to return various values associated with a single
* language, e.g. localized page title, associated SiteLanguage instance,
* edit URLs and link titles and so on.
*
* Stores a duplicated Grid object associated with the SiteLanguage.
*
* Accessed from Fluid templates - generated from within BackendLayout when
* "page" module is in "languages" mode.
*
* @internal
*/
class LanguageColumn
{
protected readonly IconFactory $iconFactory;
public function __construct(
protected readonly PageLayoutContext $context,
protected readonly Grid $grid,
protected readonly array $translationInfo
) {
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
}
public function getContext(): PageLayoutContext
{
return $this->context;
}
public function getGrid(): Grid
{
return $this->grid;
}
public function getPageIcon(): string
{
$localizedPageRecord = $this->context->getLocalizedPageRecord() ?? $this->context->getPageRecord();
return BackendUtility::wrapClickMenuOnIcon(
$this->iconFactory->getIconForRecord('pages', $localizedPageRecord, IconSize::SMALL)->render(),
'pages',
$localizedPageRecord['uid']
);
}
public function getAllowTranslate(): bool
{
return $this->context->getDrawingConfiguration()->translateModeForTranslationsAllowed() && !($this->getTranslationData()['hasStandAloneContent'] ?? false);
}
public function getTranslationData(): array
{
return $this->translationInfo;
}
public function getAllowTranslateCopy(): bool
{
return $this->context->getDrawingConfiguration()->copyModeForTranslationsAllowed() && !($this->getTranslationData()['hasTranslations'] ?? false);
}
public function getAllowEditPage(): bool
{
return $this->getBackendUser()->doesUserHaveAccess($this->context->getPageRecord(), Permission::PAGE_EDIT)
&& $this->getBackendUser()->check('tables_modify', 'pages')
&& $this->getBackendUser()->checkLanguageAccess($this->context->getSiteLanguage());
}
public function getPageRecordUid(): int
{
return $this->context->getLocalizedPageRecord()['uid'] ?? $this->context->getPageRecord()['uid'];
}
public function getPageRecord(): array
{
return $this->context->getLocalizedPageRecord() ?: $this->context->getPageRecord();
}
public function getPageEditUrl(): string
{
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit_contextual', $this->getPageEditUrlParameters());
}
public function getFullPageEditUrl(): string
{
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $this->getPageEditUrlParameters());
}
private function getPageEditUrlParameters(): array
{
$pageRecordUid = $this->getPageRecordUid();
$urlParameters = [
'edit' => [
'pages' => [
$pageRecordUid => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl(),
];
// Disallow manual adjustment of the language field for pages
if (($languageField = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages')->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()) !== '') {
$urlParameters['overrideVals']['pages'][$languageField] = $this->context->getSiteLanguage()->getLanguageId();
}
return $urlParameters;
}
public function getAllowViewPage(): bool
{
return PreviewUriBuilder::create($this->context->getLocalizedPageRecord() ?? $this->context->getPageRecord())->isPreviewable();
}
public function getPreviewUrlAttributes(): string
{
$pageId = $this->context->getPageId();
$languageId = $this->context->getSiteLanguage()->getLanguageId();
return (string)PreviewUriBuilder::create($this->context->getLocalizedPageRecord() ?? $this->context->getPageRecord())
->withRootLine(BackendUtility::BEgetRootLine($pageId))
->withLanguage($languageId)
->serializeDispatcherAttributes();
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,173 @@
<?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\BackendLayout;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* This Provider adds Backend Layouts based on page TSconfig
*
* = Example =
* mod {
* web_layout {
* BackendLayouts {
* example {
* title = Example
* config {
* backend_layout {
* colCount = 1
* rowCount = 2
* rows {
* 1 {
* columns {
* 1 {
* name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.3
* colPos = 3
* colspan = 1
* }
* }
* }
* 2 {
* columns {
* 1 {
* name = Main
* colPos = 0
* colspan = 1
* }
* }
* }
* }
* }
* }
* icon = content-container-columns-2
* }
* }
* }
* }
*
* @internal Specific DataProviderInterface implementation, not considered public API.
*/
final class PageTsBackendLayoutDataProvider implements DataProviderInterface
{
/**
* Internal Backend Layout stack
*/
private array $backendLayouts = [];
public function addBackendLayouts(DataProviderContext $dataProviderContext, BackendLayoutCollection $backendLayoutCollection): void
{
$this->generateBackendLayouts($dataProviderContext, null);
foreach ($this->backendLayouts as $backendLayoutConfig) {
$backendLayout = $this->createBackendLayout($backendLayoutConfig);
$backendLayoutCollection->add($backendLayout);
}
}
/**
* Gets a backend layout by (regular) identifier.
*
* @param string $identifier
* @param int $pageId
*/
public function getBackendLayout($identifier, $pageId): ?BackendLayout
{
$this->generateBackendLayouts(null, $pageId);
if (array_key_exists($identifier, $this->backendLayouts)) {
return $this->createBackendLayout($this->backendLayouts[$identifier]);
}
return null;
}
public function getIdentifier(): string
{
return 'pagets';
}
/**
* Gets page TSconfig from DataProviderContext if available from context,
* else fetch from BackendUtility by pageId.
*/
private function getPageTsConfig(?DataProviderContext $dataProviderContext, ?int $pageId): array
{
if ($dataProviderContext === null && $pageId === null) {
throw new \RuntimeException('Either $dataProviderContext or $pageId must be provided', 1676380686);
}
if ($dataProviderContext) {
return $dataProviderContext->pageTsConfig;
}
return BackendUtility::getPagesTSconfig($pageId);
}
/**
* Generate the Backend Layout configs
*/
private function generateBackendLayouts(?DataProviderContext $dataProviderContext, ?int $pageId): void
{
$pageTsConfig = $this->getPageTsConfig($dataProviderContext, $pageId);
if (!empty($pageTsConfig['mod.']['web_layout.']['BackendLayouts.'])) {
$backendLayouts = (array)$pageTsConfig['mod.']['web_layout.']['BackendLayouts.'];
foreach ($backendLayouts as $identifier => $data) {
$backendLayout = $this->generateBackendLayoutFromTsConfig($identifier, $data);
$this->attachBackendLayout($backendLayout);
}
}
}
/**
* Generates a Backend Layout from page TSconfig array
*/
private function generateBackendLayoutFromTsConfig(string $identifier, array $data): ?array
{
$backendLayout = [];
if (is_array($data['config.']['backend_layout.'] ?? null)) {
$backendLayout['uid'] = substr($identifier, 0, -1);
$backendLayout['title'] = $data['title'] ?? $backendLayout['uid'];
$backendLayout['icon'] = $data['icon'] ?? '';
// Convert PHP array back to plain TypoScript to process it
$config = ArrayUtility::flatten($data['config.']);
$backendLayout['config'] = '';
foreach ($config as $row => $value) {
$backendLayout['config'] .= $row . ' = ' . $value . "\r\n";
}
return $backendLayout;
}
return null;
}
/**
* Attach Backend Layout to internal Stack
*/
private function attachBackendLayout(mixed $backendLayout = null): void
{
if ($backendLayout) {
$this->backendLayouts[$backendLayout['uid']] = $backendLayout;
}
}
/**
* Creates a new backend layout using the given record data.
*/
private function createBackendLayout(array $data): BackendLayout
{
$backendLayout = BackendLayout::create((string)$data['uid'], $data['title'], $data['config']);
$backendLayout->setIconPath($data['icon'] ?? '');
$backendLayout->setData($data);
return $backendLayout;
}
}
+351
View File
@@ -0,0 +1,351 @@
<?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;
}
}
+120
View File
@@ -0,0 +1,120 @@
<?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\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\Route;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\View\ViewInterface as CoreViewInterface;
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
use TYPO3Fluid\Fluid\View\TemplateView as FluidTemplateView;
/**
* Creates a View for backend usage. This is a low level factory. Extensions typically use ModuleTemplate instead.
*/
final readonly class BackendViewFactory
{
public function __construct(
private RenderingContextFactory $renderingContextFactory,
private PackageManager $packageManager,
) {}
/**
* This backend view is capable of overriding templates, partials and layouts via TsConfig
* based on the composer package name of the route and optional additional package names.
*/
public function create(ServerRequestInterface $request, array $packageNames = []): CoreViewInterface
{
if (empty($packageNames)) {
// Extensions *may* provide path lookup package names as second argument. In most cases, this is not
// needed, and the package name will be fetched from current route. However, there are scenarios
// where extensions 'hook' into existing functionality of a different extension that defined a
// route, and then deliver own templates from the own extension. In those cases, they need to
// supply an additional base package name.
// Examples are backend toolbar items: The toolbar items are rendered through a typo3/cms-backend
// route, so this is picked as base from the route. workspaces delivers an additional toolbar item,
// so 'typo3/cms-workspaces' needs to be added as additional path to look up. The dashboard extension
// and FormEngine have similar cases.
/** @var Route $route */
$route = $request->getAttribute('route');
$packageNameFromRoute = $route->getOption('packageName');
if (!empty($packageNameFromRoute)) {
$packageNames[] = $packageNameFromRoute;
}
}
// Always add EXT:backend/Resources/Private/ as first default path to resolve
// default Layouts/Module.fluid.html and its partials.
if (!in_array('typo3/cms-backend', $packageNames, true)) {
array_unshift($packageNames, 'typo3/cms-backend');
}
// @todo: This assumes the pageId is *always* given as 'id' in request.
// @todo: It would be cool if a middleware adds final pageTS - already overlayed by userTS - as attribute to request, to use it here.
$pageTs = [];
$pageId = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0;
if (MathUtility::canBeInterpretedAsInteger($pageId)) {
// Some BE controllers misuse the 'id' argument for something else than the page-uid (especially filelist module).
// We check if 'id' is an integer here to skip pageTsConfig calculation if that is the case.
// @todo: Mid-term, misuses should vanish, making 'id' a Backend convention. Affected is
// at least ext:filelist, plus record linking modals that use 'pid'.
$pageTs = BackendUtility::getPagesTSconfig((int)$pageId);
}
$templatePaths = [
'templateRootPaths' => [],
'layoutRootPaths' => [],
'partialRootPaths' => [],
];
foreach ($packageNames as $packageName) {
// Add paths for package.
$packagePath = $this->packageManager->getPackage($packageName)->getPackagePath();
$templatePaths['templateRootPaths'][] = $packagePath . 'Resources/Private/Templates';
$templatePaths['layoutRootPaths'][] = $packagePath . 'Resources/Private/Layouts';
$templatePaths['partialRootPaths'][] = $packagePath . 'Resources/Private/Partials';
// Add possible overrides.
if (is_array($pageTs['templates.'][$packageName . '.'] ?? false)) {
$overrides = $pageTs['templates.'][$packageName . '.'];
ksort($overrides);
foreach ($overrides as $override) {
$pathParts = GeneralUtility::trimExplode(':', $override, true);
if (count($pathParts) < 2) {
throw new \RuntimeException(
'When overriding template paths, the syntax is "composer-package-name:path", example: "typo3/cms-seo:Resources/Private/TemplateOverrides/typo3/cms-backend"',
1643798660
);
}
$composerPackageName = $pathParts[0];
$overridePackagePath = $this->packageManager->getPackage($composerPackageName)->getPackagePath();
$overridePath = rtrim($pathParts[1], '/');
$templatePaths['templateRootPaths'][] = $overridePackagePath . $overridePath . '/Templates';
$templatePaths['layoutRootPaths'][] = $overridePackagePath . $overridePath . '/Layouts';
$templatePaths['partialRootPaths'][] = $overridePackagePath . $overridePath . '/Partials';
}
}
}
// @todo: Inject ViewFactoryInterface instead, and use it.
$renderingContext = $this->renderingContextFactory->create($templatePaths, $request);
$fluidView = new FluidTemplateView($renderingContext);
return new FluidViewAdapter($fluidView);
}
}
@@ -0,0 +1,250 @@
<?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\Drawing;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendLayout\ContentFetcher;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\Grid;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumn;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridRow;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\LanguageColumn;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Schema\Exception\UndefinedSchemaException;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewInterface;
/**
* Backend Layout Renderer
*
* Draws a page layout - essentially, behaves as a wrapper for a view
* which renders the Resources/Private/PageLayout/PageLayout template
* with necessary assigned template variables.
*
* @internal
*/
readonly class BackendLayoutRenderer
{
public function __construct(
protected BackendViewFactory $backendViewFactory,
protected RecordFactory $recordFactory,
protected FlashMessageService $flashMessageService,
) {}
public function getGridForPageLayoutContext(PageLayoutContext $context): Grid
{
$recordIdentityMap = $context->getRecordIdentityMap();
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class);
$grid = GeneralUtility::makeInstance(Grid::class, $context);
if ($context->getDrawingConfiguration()->isLanguageComparisonMode()) {
$languageId = $context->getSiteLanguage()->getLanguageId();
} else {
$languageId = $context->getDrawingConfiguration()->getPrimaryLanguageId();
}
$rows = $context->getBackendLayout()->getStructure()['__config']['backend_layout.']['rows.'] ?? [];
ksort($rows);
foreach ($rows as $row) {
$rowObject = GeneralUtility::makeInstance(GridRow::class, $context);
foreach ($row['columns.'] ?? [] as $column) {
$columnObject = GeneralUtility::makeInstance(GridColumn::class, $context, $column);
$rowObject->addColumn($columnObject);
if (isset($column['colPos'])) {
$records = $contentFetcher->getContentRecordsPerColumn($context, (int)$column['colPos'], $languageId);
foreach ($records as $contentRecord) {
try {
// By calling record factory to create the record, it is also stored in the identity map.
$contentRecord = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $contentRecord, null, $recordIdentityMap);
$columnItem = GeneralUtility::makeInstance(GridColumnItem::class, $context, $columnObject, $contentRecord);
$columnObject->addItem($columnItem);
} catch (UndefinedSchemaException) {
}
}
}
}
$grid->addRow($rowObject);
}
return $grid;
}
protected function createView(ServerRequestInterface $request, PageLayoutContext $pageLayoutContext): ViewInterface
{
$backendUser = $this->getBackendUser();
$view = $this->backendViewFactory->create($request);
$view->assignMultiple([
'context' => $pageLayoutContext,
'hideRestrictedColumns' => $pageLayoutContext->getDrawingConfiguration()->shouldHideRestrictedColumns(),
'allowEditContent' => $backendUser->check('tables_modify', 'tt_content'),
'maxTitleLength' => $backendUser->uc['titleLen'] ?? 20,
]);
return $view;
}
/**
* @param bool $renderUnused If true, renders the bottom column with unused records
*/
public function drawContent(ServerRequestInterface $request, PageLayoutContext $pageLayoutContext, bool $renderUnused = true): string
{
$view = $this->createView($request, $pageLayoutContext);
if ($pageLayoutContext->getDrawingConfiguration()->isLanguageComparisonMode()) {
$view->assign('languageColumns', $this->getLanguageColumnsForPageLayoutContext($pageLayoutContext));
} else {
$context = $pageLayoutContext;
// Check if we have to use a localized context for grid creation
$primaryLanguageId = $pageLayoutContext->getDrawingConfiguration()->getPrimaryLanguageId();
if ($primaryLanguageId > 0) {
// In case a localization is selected, clone the context with this language
$localizedContext = $pageLayoutContext->cloneForLanguage(
$pageLayoutContext->getSiteLanguage($primaryLanguageId)
);
if ($localizedContext->getLocalizedPageRecord()) {
// In case the localized context contains the corresponding
// localized page record use this context for grid creation.
$context = $localizedContext;
}
}
$grid = $this->getGridForPageLayoutContext($context);
$view->assign('grid', $grid);
$view->assign('gridColumns', array_fill(1, $grid->getContext()->getBackendLayout()->getColCount(), null));
}
$rendered = $view->render('PageLayout/PageLayout');
if ($renderUnused) {
$rendered .= $this->renderUnused($request, $pageLayoutContext);
}
return $rendered;
}
protected function renderUnused(ServerRequestInterface $request, PageLayoutContext $pageLayoutContext): string
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class);
$view = $this->createView($request, $pageLayoutContext);
$unusedRecords = $contentFetcher->getUnusedRecords($pageLayoutContext);
if (empty($unusedRecords)) {
return '';
}
$unusedElementsMessage = new FlashMessage(
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleUnusedElementsWarning'),
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:staleUnusedElementsWarningTitle'),
ContextualFeedbackSeverity::WARNING
);
$queue = $this->flashMessageService->getMessageQueueByIdentifier();
$queue->addMessage($unusedElementsMessage);
$unusedGrid = GeneralUtility::makeInstance(Grid::class, $pageLayoutContext);
$unusedRow = GeneralUtility::makeInstance(GridRow::class, $pageLayoutContext);
$unusedColumn = GeneralUtility::makeInstance(GridColumn::class, $pageLayoutContext, ['name' => 'unused']);
$unusedGrid->addRow($unusedRow);
$unusedRow->addColumn($unusedColumn);
foreach ($unusedRecords as $unusedRecord) {
$unusedRecord = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $unusedRecord, null, $pageLayoutContext->getRecordIdentityMap());
$item = GeneralUtility::makeInstance(GridColumnItem::class, $pageLayoutContext, $unusedColumn, $unusedRecord);
$unusedColumn->addItem($item);
}
$view->assign('grid', $unusedGrid);
$view->assign('gridColumns', null);
return $view->render('PageLayout/UnusedRecords');
}
protected function getLanguageColumnsForPageLayoutContext(PageLayoutContext $context): iterable
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class);
$languageColumns = [];
// default language
$translationInfo = $contentFetcher->getTranslationData(
$context,
$contentFetcher->getFlatContentRecords($context, 0),
0
);
$defaultLanguageColumnObject = GeneralUtility::makeInstance(
LanguageColumn::class,
$context,
$this->getGridForPageLayoutContext($context),
$translationInfo
);
foreach ($context->getLanguagesToShow() as $siteLanguage) {
$localizedLanguageId = $siteLanguage->getLanguageId();
if ($localizedLanguageId <= 0) {
continue;
}
$localizedContext = $context->cloneForLanguage($siteLanguage);
if (!$localizedContext->getLocalizedPageRecord()) {
continue;
}
$translationInfo = $contentFetcher->getTranslationData(
$context,
$contentFetcher->getFlatContentRecords($context, $localizedLanguageId),
$localizedContext->getSiteLanguage()->getLanguageId()
);
$translatedRows = $contentFetcher->getFlatContentRecords($context, $localizedLanguageId);
foreach ($defaultLanguageColumnObject->getGrid()->getRows() as $rows) {
foreach ($rows->getColumns() as $column) {
if (($translationInfo['mode'] ?? '') === 'connected') {
foreach ($column->getItems() as $item) {
// check if translation exists
foreach ($translatedRows as $translation) {
if ($translation['l18n_parent'] === $item->getRecord()->getUid()) {
$translation = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $translation, null, $context->getRecordIdentityMap());
$translatedItem = GeneralUtility::makeInstance(GridColumnItem::class, $localizedContext, $column, $translation);
$item->addTranslation($localizedLanguageId, $translatedItem);
}
}
}
}
}
}
$languageColumnObject = GeneralUtility::makeInstance(
LanguageColumn::class,
$localizedContext,
$this->getGridForPageLayoutContext($localizedContext),
$translationInfo
);
$languageColumns[$localizedLanguageId] = $languageColumnObject;
}
return [$defaultLanguageColumnObject] + $languageColumns;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View\Drawing;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
use TYPO3\CMS\Backend\View\PageViewMode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Drawing Configuration
*
* Attached to BackendLayout as storage for configuration options which
* determine how a page layout is rendered. Contains settings for active
* language, show-hidden, site languages etc. and returns TCA labels for
* tt_content fields and CTypes.
*
* Corresponds to legacy public properties from PageLayoutView.
*
* @internal
*/
class DrawingConfiguration
{
/**
* Array of selected language IDs for multi-language comparison view
* @var int[]
*/
protected array $selectedLanguageIds = [0];
/**
* Corresponds to web.layout.allowInconsistentLanguageHandling TSconfig property
*/
protected bool $allowInconsistentLanguageHandling;
/**
* Key => "Language ID", Value "Label of language"
*/
protected array $languageColumns = [];
/**
* Whether or not to show hidden records when rendering column contents.
*/
protected bool $showHidden = true;
/**
* An array list of currently active columns. Only column identifiers
* (colPos value) which are contained in this array will be rendered in
* the page module.
*/
protected array $activeColumns = [1, 0, 2, 3];
/**
* Whether or not to allow the translate mode for translations
*/
protected bool $allowTranslateModeForTranslations;
/**
* Whether or not to allow the copy mode for translations
*/
protected bool $allowCopyModeForTranslations;
protected bool $shouldHideRestrictedColumns;
protected PageViewMode $pageViewMode;
public static function create(BackendLayout $backendLayout, array $pageTsConfig, PageViewMode $pageViewMode): self
{
$obj = new self();
$obj->pageViewMode = $pageViewMode;
$obj->allowInconsistentLanguageHandling = (bool)($pageTsConfig['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false);
$obj->shouldHideRestrictedColumns = (bool)($pageTsConfig['mod.']['web_layout.']['hideRestrictedCols'] ?? false);
$availableColumnPositionsFromBackendLayout = array_unique($backendLayout->getColumnPositionNumbers());
$allowedColumnPositionsByTsConfig = array_unique(GeneralUtility::intExplode(',', (string)($pageTsConfig['mod.']['SHARED.']['colPos_list'] ?? ''), true));
// If there is no tsConfig colPos_list, no restriction. Else create intersection of available and allowed.
if (!empty($allowedColumnPositionsByTsConfig)) {
$obj->activeColumns = array_intersect($availableColumnPositionsFromBackendLayout, $allowedColumnPositionsByTsConfig);
} else {
$obj->activeColumns = $availableColumnPositionsFromBackendLayout;
}
$obj->allowTranslateModeForTranslations = (bool)($pageTsConfig['mod.']['web_layout.']['localization.']['enableTranslate'] ?? true);
$obj->allowCopyModeForTranslations = (bool)($pageTsConfig['mod.']['web_layout.']['localization.']['enableCopy'] ?? true);
return $obj;
}
/**
* Get all selected language IDs
* @return int[]
*/
public function getSelectedLanguageIds(): array
{
return $this->selectedLanguageIds;
}
/**
* Set selected language IDs
* @param int[] $selectedLanguageIds
*/
public function setSelectedLanguageIds(array $selectedLanguageIds): void
{
$this->selectedLanguageIds = $selectedLanguageIds !== [] ? array_map('intval', $selectedLanguageIds) : [0];
}
/**
* Get the primary (first) selected language ID
*/
public function getPrimaryLanguageId(): int
{
return $this->selectedLanguageIds[0] ?? 0;
}
public function getAllowInconsistentLanguageHandling(): bool
{
return $this->allowInconsistentLanguageHandling;
}
public function isLanguageComparisonMode(): bool
{
return $this->pageViewMode === PageViewMode::LanguageComparisonView;
}
public function getLanguageColumns(): array
{
if (empty($this->languageColumns)) {
return [0 => 'Default'];
}
return $this->languageColumns;
}
public function setLanguageColumns(array $languageColumns): void
{
$this->languageColumns = $languageColumns;
}
public function getShowHidden(): bool
{
return $this->showHidden;
}
public function setShowHidden(bool $showHidden): void
{
$this->showHidden = $showHidden;
}
public function getActiveColumns(): array
{
return $this->activeColumns;
}
public function translateModeForTranslationsAllowed(): bool
{
return $this->allowTranslateModeForTranslations;
}
public function copyModeForTranslationsAllowed(): bool
{
return $this->allowCopyModeForTranslations;
}
public function shouldHideRestrictedColumns(): bool
{
return $this->shouldHideRestrictedColumns;
}
}
@@ -0,0 +1,66 @@
<?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\Event;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Domain\RecordInterface;
/**
* Use this Event to modify a custom preview for a content type in the
* Page Module after PageContentPreviewRenderingEvent and content preview rendering is executed.
*/
final class AfterPageContentPreviewRenderedEvent
{
public function __construct(
private readonly string $table,
private readonly string $recordType,
private readonly RecordInterface $record,
private readonly PageLayoutContext $context,
private string $previewContent,
) {}
public function getTable(): string
{
return $this->table;
}
public function getRecordType(): string
{
return $this->recordType;
}
public function getRecord(): RecordInterface
{
return $this->record;
}
public function getPageLayoutContext(): PageLayoutContext
{
return $this->context;
}
public function getPreviewContent(): string
{
return $this->previewContent;
}
public function setPreviewContent(string $content): void
{
$this->previewContent = $content;
}
}
@@ -0,0 +1,57 @@
<?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\Event;
use TYPO3\CMS\Backend\View\PageLayoutContext;
/**
* Use this Event to identify whether a content element is used.
*/
final class IsContentUsedOnPageLayoutEvent
{
public function __construct(
private readonly array $record,
private bool $used,
private readonly PageLayoutContext $context
) {}
public function getRecord(): array
{
return $this->record;
}
public function isRecordUsed(): bool
{
return $this->used;
}
public function setUsed(bool $isUsed): void
{
$this->used = $isUsed;
}
public function getKnownColumnPositionNumbers(): array
{
return $this->context->getBackendLayout()->getColumnPositionNumbers();
}
public function getPageLayoutContext(): PageLayoutContext
{
return $this->context;
}
}
@@ -0,0 +1,44 @@
<?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\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
/**
* Event to change backend layout configuration based on colPos and pageUid of records. This is designed
* for extensions like ext:container to update allowed & disallowed restrictions if needed.
*
* @internal TYPO3 core v14 needs to emit *some* event at this point to enable extensions to hook in. However,
* this event is dispatched from within BackendLayoutView which is involved in a quite convoluted
* system around current backend layout handling. The backend layout handling should in general see
* more refactorings to model things more straight forward, and the method dispatching the event is not
* called as systematically as it should be and is declared internal as well. As such, this event is
* for now declared as "may change, use at your own risk" since it exposes the ugly internal structures
* of the backend layout implementation.
*/
final class ManipulateBackendLayoutColPosConfigurationForPageEvent
{
public function __construct(
public array $configuration,
public readonly BackendLayout $backendLayout,
public readonly int $colPos,
public readonly int $pageUid,
public readonly ?ServerRequestInterface $request = null,
) {}
}
@@ -0,0 +1,52 @@
<?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\Event;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
/**
* Use this Event to alter the database query when loading content for a page.
*/
final class ModifyDatabaseQueryForContentEvent
{
public function __construct(
private QueryBuilder $queryBuilder,
private readonly string $table,
private readonly int $pageId,
) {}
public function getQueryBuilder(): QueryBuilder
{
return $this->queryBuilder;
}
public function setQueryBuilder(QueryBuilder $queryBuilder): void
{
$this->queryBuilder = $queryBuilder;
}
public function getTable(): string
{
return $this->table;
}
public function getPageId(): int
{
return $this->pageId;
}
}
@@ -0,0 +1,79 @@
<?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\Event;
use TYPO3\CMS\Backend\RecordList\DatabaseRecordList;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
/**
* Use this Event to alter the database query when loading content for a page (usually in the records module)
* before it is executed.
* @todo This event should contain the $addSorting value, so listener knows when to add ORDER-BY stuff.
*/
final class ModifyDatabaseQueryForRecordListingEvent
{
public function __construct(
private QueryBuilder $queryBuilder,
private readonly string $table,
private readonly int $pageId,
private readonly array $fields,
private readonly int $firstResult,
private readonly int $maxResults,
private readonly DatabaseRecordList $recordList
) {}
public function getQueryBuilder(): QueryBuilder
{
return $this->queryBuilder;
}
public function setQueryBuilder(QueryBuilder $queryBuilder): void
{
$this->queryBuilder = $queryBuilder;
}
public function getTable(): string
{
return $this->table;
}
public function getPageId(): int
{
return $this->pageId;
}
public function getFields(): array
{
return $this->fields;
}
public function getFirstResult(): int
{
return $this->firstResult;
}
public function getMaxResults(): int
{
return $this->maxResults;
}
public function getDatabaseRecordList(): DatabaseRecordList
{
return $this->recordList;
}
}
@@ -0,0 +1,77 @@
<?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\Event;
use Psr\EventDispatcher\StoppableEventInterface;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Domain\RecordInterface;
/**
* Use this Event to have a custom preview for a content type in the Page Module
*/
final class PageContentPreviewRenderingEvent implements StoppableEventInterface
{
private ?string $content = null;
public function __construct(
private readonly string $table,
private readonly string $recordType,
private RecordInterface $record,
private readonly PageLayoutContext $context
) {}
public function getTable(): string
{
return $this->table;
}
public function getRecordType(): string
{
return $this->recordType;
}
public function getRecord(): RecordInterface
{
return $this->record;
}
public function setRecord(RecordInterface $record): void
{
$this->record = $record;
}
public function getPageLayoutContext(): PageLayoutContext
{
return $this->context;
}
public function getPreviewContent(): ?string
{
return $this->content;
}
public function setPreviewContent(string $content): void
{
$this->content = $content;
}
public function isPropagationStopped(): bool
{
return $this->content !== null;
}
}
+328
View File
@@ -0,0 +1,328 @@
<?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\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Context\PageContext;
use TYPO3\CMS\Backend\Domain\Model\Language\PageLanguageInformation;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
use TYPO3\CMS\Backend\View\BackendLayout\ContentFetcher;
use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
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\SiteInterface;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Page Module specific rendering context.
*
* Extends generic PageContext with module-specific rendering configuration
* for the page module (web_layout).
*
* This context provides:
* - Generic page data (via delegation to PageContext)
* - Backend layout configuration
* - Drawing configuration
* - Content type labels
* - Content fetcher
*
* @internal
*/
class PageLayoutContext
{
protected ContentFetcher $contentFetcher;
protected ?array $localizedPageRecord = null;
/**
* @var SiteLanguage[]
*/
protected array $siteLanguages = [];
protected SiteLanguage $siteLanguage;
/**
* Array of content type labels. Key is CType, value is either a plain text
* label or an LLL:EXT:... reference to a specific label.
*/
protected array $contentTypeLabels = [];
/**
* Labels for columns, in format of TCA select options. Numerically indexed
* array of numerically indexed value arrays, with each sub-array containing
* at least two values and one optional third value:
*
* - label (hardcoded or LLL:EXT:... reference. MANDATORY)
* - value (colPos of column. MANDATORY)
* - icon (icon name or file reference. OPTIONAL)
*/
protected array $itemLabels = [];
protected RecordIdentityMap $recordIdentityMap;
public function __construct(
protected readonly PageContext $pageContext,
protected readonly BackendLayout $backendLayout,
protected readonly DrawingConfiguration $drawingConfiguration,
protected readonly ServerRequestInterface $request,
) {
$this->contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class);
$this->siteLanguages = $this->pageContext->site->getAvailableLanguages($this->getBackendUser(), true, $this->pageContext->pageId);
$this->siteLanguage = $this->pageContext->site->getDefaultLanguage();
$this->recordIdentityMap = GeneralUtility::makeInstance(RecordIdentityMap::class);
}
public function cloneForLanguage(SiteLanguage $language): self
{
$copy = clone $this;
$copy->setSiteLanguage($language);
return $copy;
}
protected function setSiteLanguage(SiteLanguage $siteLanguage): void
{
$this->siteLanguage = $siteLanguage;
$languageId = $siteLanguage->getLanguageId();
if ($languageId > 0) {
$pageLocalizationRecord = GeneralUtility::makeInstance(LocalizationRepository::class)
->getPageTranslations($this->getPageId(), [$languageId], $this->getBackendUser()->workspace);
if ($pageLocalizationRecord !== []) {
// @todo: lets move to Record API soon
$pageLocalizationRecord = reset($pageLocalizationRecord);
$this->localizedPageRecord = $pageLocalizationRecord->toArray(true) ?: null;
}
}
}
public function getPageContext(): PageContext
{
return $this->pageContext;
}
public function getPageId(): int
{
return $this->pageContext->pageId;
}
public function getPageRecord(): array
{
return $this->pageContext->pageRecord;
}
public function getSite(): SiteInterface
{
return $this->pageContext->site;
}
public function getSelectedLanguageIds(): array
{
return $this->pageContext->selectedLanguageIds;
}
public function getPrimaryLanguageId(): int
{
return $this->pageContext->getPrimaryLanguageId();
}
public function getLanguageInformation(): PageLanguageInformation
{
return $this->pageContext->languageInformation;
}
public function getBackendLayout(): BackendLayout
{
return $this->backendLayout;
}
public function getDrawingConfiguration(): DrawingConfiguration
{
return $this->drawingConfiguration;
}
/**
* @return SiteLanguage[]
*/
public function getSiteLanguages(): iterable
{
return $this->siteLanguages;
}
/**
* @return SiteLanguage[]
*/
public function getLanguagesToShow(): iterable
{
$site = $this->pageContext->site;
$selectedLanguageIds = $this->drawingConfiguration->getSelectedLanguageIds();
// If multiple languages are selected, show default language + all selected languages
if (count($selectedLanguageIds) > 1 || (count($selectedLanguageIds) === 1 && $selectedLanguageIds[0] > 0)) {
$languagesToShow = [];
// Always include default language (0) first
$languagesToShow[] = $site->getDefaultLanguage();
// Add all selected languages, except default
foreach ($selectedLanguageIds as $languageId) {
try {
if ($languageId > 0) {
$languagesToShow[] = $site->getLanguageById($languageId);
}
} catch (\InvalidArgumentException $e) {
// Skip invalid language IDs
}
}
return $languagesToShow;
}
// Single language selected (default language only)
return [$site->getDefaultLanguage()];
}
public function hasMultiLanguages(): bool
{
return count($this->getLanguagesToShow()) > 1;
}
public function getSiteLanguage(?int $languageId = null): SiteLanguage
{
if ($languageId === null) {
return $this->siteLanguage;
}
if ($languageId === -1) {
return $this->siteLanguages[-1];
}
return $this->pageContext->site->getLanguageById($languageId);
}
public function isPageEditable(): bool
{
// TODO: refactor to page permissions container
if ($this->getBackendUser()->isAdmin()) {
return true;
}
$pageRecord = $this->pageContext->pageRecord;
return $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_EDIT)
&& (
!($schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
);
}
public function getAllowNewContent(): bool
{
$allowInconsistentLanguageHandling = $this->drawingConfiguration->getAllowInconsistentLanguageHandling();
if (!$allowInconsistentLanguageHandling && $this->getLanguageModeIdentifier() === 'connected') {
return false;
}
return true;
}
public function getContentTypeLabels(): array
{
if (empty($this->contentTypeLabels)) {
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
$schema = $schemaFactory->get('tt_content');
if ($schema->supportsSubSchema()) {
if (($schemaTypeInformation = $schema->getSubSchemaTypeInformation())->isPointerToForeignFieldInForeignSchema()) {
$typeField = $schemaFactory->get($schemaTypeInformation->getForeignSchemaName())->getField($schemaTypeInformation->getForeignFieldName());
} else {
$typeField = $schema->getField($schemaTypeInformation->getFieldName());
}
foreach ($typeField->getConfiguration()['items'] ?? [] as $val) {
$this->contentTypeLabels[$val['value']] = $this->getLanguageService()->sL($val['label']);
}
}
}
return $this->contentTypeLabels;
}
public function getItemLabels(): array
{
if (empty($this->itemLabels)) {
foreach (GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('tt_content')->getFields() as $field) {
$this->itemLabels[$field->getName()] = $this->getLanguageService()->sL($field->getLabel());
}
}
return $this->itemLabels;
}
public function getLanguageModeLabelClass(): string
{
$languageId = $this->siteLanguage->getLanguageId();
$contentRecordsPerColumn = $this->contentFetcher->getFlatContentRecords($this, $languageId);
$translationData = $this->contentFetcher->getTranslationData($this, $contentRecordsPerColumn, $languageId);
return $translationData['mode'] === 'mixed' ? 'danger' : 'info';
}
public function getLanguageMode(): string
{
return match ($this->getLanguageModeIdentifier()) {
'mixed' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:languageModeMixed'),
'connected' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:languageModeConnected'),
'free' => $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:languageModeFree'),
default => '',
};
}
public function getLanguageModeIdentifier(): string
{
$contentRecordsPerColumn = $this->contentFetcher->getContentRecordsPerColumn($this, null, $this->siteLanguage->getLanguageId());
$contentRecords = empty($contentRecordsPerColumn) ? [] : array_merge(...$contentRecordsPerColumn);
$translationData = $this->contentFetcher->getTranslationData($this, $contentRecords, $this->siteLanguage->getLanguageId());
return $translationData['mode'] ?? '';
}
public function getCurrentRequest(): ServerRequestInterface
{
return $this->request;
}
public function getLocalizedPageTitle(): string
{
return $this->localizedPageRecord['title'] ?? $this->pageContext->pageRecord['title'] ?? '';
}
public function getLocalizedPageRecord(): ?array
{
return $this->localizedPageRecord;
}
public function getRecordIdentityMap(): RecordIdentityMap
{
return $this->recordIdentityMap;
}
public function getReturnUrl(): string
{
return $this->getCurrentRequest()->getAttribute('normalizedParams')->getRequestUri();
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
public function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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;
/**
* @internal
*/
enum PageViewMode: int
{
case LayoutView = 1;
case LanguageComparisonView = 2;
/**
* Get the language label key for this view mode.
*/
public function getLabel(): string
{
return match ($this) {
self::LayoutView => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.layout',
self::LanguageComparisonView => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.language_comparison',
};
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View;
use Psr\Log\LogLevel;
/**
* An interface that tracks the progress of a longer-running progress.
* This acts as a wrapper for Symfony ProgressBar on CLI, so see the info here.
*
* The listener can be used multiple times when calling start() again a new progressbar starts.
*
* @internal this interface is still experimental, and not considered part of TYPO3 Public API.
*/
interface ProgressListenerInterface
{
/**
* Start a progress by using the maximum items, and an additional header message.
*
* @param int $maxSteps set the maximum amount of items to be processed
* @param string|null $additionalMessage a separate text message
*/
public function start(int $maxSteps = 0, ?string $additionalMessage = null): void;
/**
* Move the progress one step further
* @param int $step by default, this is "1" but can be used to skip further.
* @param string|null $additionalMessage a separate text message
*/
public function advance(int $step = 1, ?string $additionalMessage = null): void;
/**
* Stop the progress, automatically setting it to 100%.
*
* @param string|null $additionalMessage a separate text message
*/
public function finish(?string $additionalMessage = null): void;
/**
* Can be used to render custom messages during the progress.
*
* @param string $message the message to render
* @param string $logLevel used as severity
*/
public function log(string $message, string $logLevel = LogLevel::INFO): void;
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* Renders the search box for the record listing and the element browser.
*
* @internal
*/
#[Autoconfigure(public: true, shared: false)]
class RecordSearchBoxComponent
{
protected array $allowedSearchLevels = [];
protected string $searchWord = '';
protected int $searchLevel = 0;
public function __construct(protected readonly BackendViewFactory $backendViewFactory) {}
public function setSearchWord(string $searchWord): self
{
$this->searchWord = $searchWord;
return $this;
}
public function setSearchLevel(int $searchLevel): self
{
$this->searchLevel = $searchLevel;
return $this;
}
public function setAllowedSearchLevels(array $allowedSearchLevels): self
{
$this->allowedSearchLevels = $allowedSearchLevels;
return $this;
}
public function render(ServerRequestInterface $request, UriInterface|string|null $formUrl = null): string
{
$view = $this->backendViewFactory->create($request, ['typo3/cms-backend']);
return $view
->assignMultiple([
'formUrl' => $formUrl,
'availableSearchLevels' => $this->allowedSearchLevels,
'selectedSearchLevel' => $this->searchLevel,
'searchString' => $this->searchWord,
])
->render('RecordSearchBox');
}
}
+368
View File
@@ -0,0 +1,368 @@
<?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\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Tree\View\LinkParameterProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders utility forms for creating files and folder.
* Used in different views, e.g. FileList but also in Element and Link Browsers.
* @internal
*/
class ResourceUtilityRenderer
{
/**
* @var LinkParameterProviderInterface
*/
protected $parameterProvider;
protected UriBuilder $uriBuilder;
public function __construct(LinkParameterProviderInterface $parameterProvider)
{
$this->parameterProvider = $parameterProvider;
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
}
/**
* For TBE: Makes a form for creating new folders in the file mount the user is browsing.
* The folder creation request is sent to the tce_file.php script in the core which will handle the creation.
*
* @param Folder $folderObject Absolute filepath on server in which to create the new folder.
*
* @return string HTML for the create folder form.
*/
public function createFolder(ServerRequestInterface $request, Folder $folderObject)
{
$lang = $this->getLanguageService();
if (!$folderObject->checkActionPermission('write')) {
// Do not show create folder form if it is denied
return '';
}
$formAction = (string)$this->uriBuilder->buildUriFromRoute('tce_file');
$markup = [];
$markup[] = '<form class="pt-3 pb-3" action="' . htmlspecialchars($formAction)
. '" method="post" name="editform" enctype="multipart/form-data">';
$markup[] = '<h4>' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:create_folder.title')) . '</h4>';
$markup[] = '<div class="input-group">';
$markup[] = '<input class="form-control" type="text" name="data[newfolder][0][data]" placeholder="' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:create_folder.placeholder')) . '" />';
$markup[] = '<input class="btn btn-default" type="submit" name="submit" value="'
. htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:create_folder.submit')) . '" />';
$markup[] = '</div>';
$markup[] = '<input type="hidden" name="data[newfolder][0][target]" value="'
. htmlspecialchars($folderObject->getCombinedIdentifier()) . '" />';
// Make footer of upload form, including the submit button:
$redirectValue = (string)$this->uriBuilder->buildUriFromRequest(
$request,
$this->parameterProvider->getUrlParameters(
['identifier' => $folderObject->getCombinedIdentifier()]
)
);
$markup[] = '<input type="hidden" name="redirect" value="' . htmlspecialchars($redirectValue) . '" />';
$markup[] = '</form>';
return implode(LF, $markup);
}
/**
* For TBE: Creates a form for creating new text files.
*
* @param Folder $folderObject Folder object in which to create the text file.
*
* @return string HTML for the text file creation form.
*/
public function createRegularFile(ServerRequestInterface $request, Folder $folderObject): string
{
if (!$folderObject->checkActionPermission('write') || !$folderObject->getStorage()->checkUserActionPermission('add', 'File')) {
return '';
}
$markup = [];
$lang = $this->getLanguageService();
$fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
$redirectValue = (string)$this->uriBuilder->buildUriFromRequest(
$request,
$this->parameterProvider->getUrlParameters(
['identifier' => $folderObject->getCombinedIdentifier()]
)
);
// Create a list of allowed text file extensions
$textFileExt = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], true);
$allowedTextFileList = [];
foreach ($textFileExt as $fileExt) {
if ($fileNameVerifier->isValid('.' . $fileExt)) {
$allowedTextFileList[] = '<li class="badge badge-secondary">' . strtoupper(htmlspecialchars($fileExt)) . '</li>';
}
}
if (!empty($allowedTextFileList)) {
$formAction = (string)$this->uriBuilder->buildUriFromRoute('tce_file');
$markup[] = '<form class="pt-3 pb-3" action="' . htmlspecialchars($formAction) . '" method="post" name="newFileForm" enctype="multipart/form-data">';
$markup[] = '<input type="hidden" name="data[newfile][0][target]" value="' . htmlspecialchars($folderObject->getCombinedIdentifier()) . '" />';
$markup[] = '<input type="hidden" name="redirect" value="' . htmlspecialchars($redirectValue) . '" />';
$markup[] = '<input type="hidden" name="edit" value="true" />';
$markup[] = '<h4>' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:file_newfolder.php.newfile')) . '</h4>';
$markup[] = '<div class="input-group">';
$markup[] = '<input class="form-control" type="text" name="data[newfile][0][data]" placeholder="' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:file_newfolder.php.label_newfile')) . '" />';
$markup[] = '<button class="btn btn-default" type="submit" name="submitCreateFileForm" value="1">' . htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:file_newfolder.php.newfile_submit')) . '</button>';
$markup[] = '</div>';
$markup[] = '<div class="form-text mt-1">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowedEditableTextFileExtensions'));
$markup[] = '<ul class="badge-list">' . implode(' ', $allowedTextFileList) . '</ul>';
$markup[] = '</div>';
$markup[] = '</form>';
}
return implode(LF, $markup);
}
/**
* For TBE: Creates a form for adding online media files (YouTube, Vimeo, etc.).
*
* @param Folder $folderObject Folder object in which to create the online media file.
* @param FileExtensionFilter|null $fileExtensionFilter Optional filter for allowed/disallowed file extensions.
*
* @return string HTML for the online media form.
*/
public function addOnlineMedia(ServerRequestInterface $request, Folder $folderObject, ?FileExtensionFilter $fileExtensionFilter = null): string
{
if (!$folderObject->checkActionPermission('write') || !$folderObject->getStorage()->checkUserActionPermission('add', 'File')) {
return '';
}
$markup = [];
$lang = $this->getLanguageService();
$fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
$onlineMediaHelperRegistry = GeneralUtility::makeInstance(OnlineMediaHelperRegistry::class);
$redirectValue = (string)$this->uriBuilder->buildUriFromRequest(
$request,
$this->parameterProvider->getUrlParameters(
['identifier' => $folderObject->getCombinedIdentifier()]
)
);
// Create a list of allowed online media file extensions
$onlineMediaFileExt = $onlineMediaHelperRegistry->getSupportedFileExtensions();
$allowedOnlineMediaList = [];
foreach ($onlineMediaFileExt as $fileExt) {
if ($fileNameVerifier->isValid('.' . $fileExt)
&& ($fileExtensionFilter === null || $fileExtensionFilter->isAllowed($fileExt))
) {
$allowedOnlineMediaList[] = '<li class="badge badge-secondary">' . strtoupper(htmlspecialchars($fileExt)) . '</li>';
}
}
if (!empty($allowedOnlineMediaList)) {
$formAction = (string)$this->uriBuilder->buildUriFromRoute('online_media');
$markup[] = '<form class="pt-3 pb-3" action="' . htmlspecialchars($formAction) . '" method="post" name="newMediaForm" enctype="multipart/form-data">';
$markup[] = '<input type="hidden" name="data[newMedia][0][target]" value="' . htmlspecialchars($folderObject->getCombinedIdentifier()) . '" />';
$markup[] = '<input type="hidden" name="redirect" value="' . htmlspecialchars($redirectValue) . '" />';
$markup[] = '<h4>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media')) . '</h4>';
$markup[] = '<div class="input-group">';
$markup[] = '<input class="form-control" type="url" name="data[newMedia][0][url]" placeholder="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder')) . '" />';
$markup[] = '<button class="btn btn-default" type="submit">' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.submit')) . '</button>';
$markup[] = '</div>';
$markup[] = '<div class="form-text mt-1">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.allowedProviders'));
$markup[] = '<ul class="badge-list">' . implode(' ', $allowedOnlineMediaList) . '</ul>';
$markup[] = '</div>';
$markup[] = '</form>';
}
return implode(LF, $markup);
}
/**
* For TBE: Creates a drag-uploader trigger button for file uploads.
* Used in the element browser context with drag-uploader support.
*
* @param Folder $folderObject Folder object in which to upload files.
* @param FileExtensionFilter|null $fileExtensionFilter Optional filter for allowed/disallowed file extensions.
*
* @return string HTML for the drag-uploader trigger.
*/
public function createDragUpload(Folder $folderObject, ?FileExtensionFilter $fileExtensionFilter = null, bool $checkFileBrowserPermission = false): string
{
if (!$folderObject->checkActionPermission('write') || !$folderObject->getStorage()->checkUserActionPermission('add', 'File')) {
return '';
}
if ($checkFileBrowserPermission && !($this->getBackendUser()->getTSConfig()['options.']['folderTree.']['uploadFieldsInLinkBrowser'] ?? true)) {
return '';
}
$lang = $this->getLanguageService();
$fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
// Determine allowed/disallowed file extensions
$list = ['*'];
$denyList = false;
$allowedFileExtensionsList = [];
if ($fileExtensionFilter !== null) {
$resolvedFileExtensions = $fileExtensionFilter->getFilteredFileExtensions();
if (($resolvedFileExtensions['allowedFileExtensions'] ?? []) !== []) {
$list = $resolvedFileExtensions['allowedFileExtensions'];
} elseif (($resolvedFileExtensions['disallowedFileExtensions'] ?? []) !== []) {
$denyList = true;
$list = $resolvedFileExtensions['disallowedFileExtensions'];
}
}
foreach ($list as $fileExt) {
if (($fileExt === '*' && !$denyList) || $fileNameVerifier->isValid('.' . $fileExt)) {
$allowedFileExtensionsList[] = '<li class="badge ' . ($denyList ? 'badge-danger' : 'badge-secondary') . '">' . strtoupper(htmlspecialchars($fileExt)) . '</li>';
}
}
// Only render the form if there are allowed file extensions
if (empty($allowedFileExtensionsList)) {
return '';
}
$markup = [];
$markup[] = '<div class="pt-3 pb-3">';
$markup[] = '<h4>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.pagetitle')) . '</h4>';
$markup[] = '<button type="button" class="btn btn-default t3js-drag-uploader-trigger">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'));
$markup[] = '</button>';
// Only show file extension info if there is an actual limitation (not just '*')
if ($list !== ['*']) {
$markup[] = '<div class="form-text mt-1">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($denyList ? 'disallowedFileExtensions' : 'allowedFileExtensions')));
$markup[] = '<ul class="badge-list">' . implode(' ', $allowedFileExtensionsList) . '</ul>';
$markup[] = '</div>';
}
$markup[] = '</div>';
return implode(LF, $markup);
}
/**
* Makes an upload form for uploading files to the file mount the user is browsing.
* The files are uploaded to the tce_file.php script in the core which will handle the upload.
*
* @return string HTML for an upload form.
*/
public function uploadForm(ServerRequestInterface $request, Folder $folderObject, ?FileExtensionFilter $fileExtensionFilter = null)
{
if (!$folderObject->checkActionPermission('write')) {
return '';
}
$allowUpload = (bool)($this->getBackendUser()->getTSConfig()['options.']['folderTree.']['uploadFieldsInLinkBrowser'] ?? true);
if (!$allowUpload) {
return '';
}
$lang = $this->getLanguageService();
$fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
// Determine allowed/disallowed file extensions
$list = ['*'];
$denyList = false;
$allowedFileExtensionsList = [];
if ($fileExtensionFilter !== null) {
$resolvedFileExtensions = $fileExtensionFilter->getFilteredFileExtensions();
if (($resolvedFileExtensions['allowedFileExtensions'] ?? []) !== []) {
$list = $resolvedFileExtensions['allowedFileExtensions'];
} elseif (($resolvedFileExtensions['disallowedFileExtensions'] ?? []) !== []) {
$denyList = true;
$list = $resolvedFileExtensions['disallowedFileExtensions'];
}
}
foreach ($list as $fileExt) {
if (($fileExt === '*' && !$denyList) || $fileNameVerifier->isValid('.' . $fileExt)) {
$allowedFileExtensionsList[] = '<li class="badge ' . ($denyList ? 'badge-danger' : 'badge-secondary') . '">' . strtoupper(htmlspecialchars($fileExt)) . '</li>';
}
}
// Only render the form if there are allowed file extensions
if (empty($allowedFileExtensionsList)) {
return '';
}
$formAction = (string)$this->uriBuilder->buildUriFromRoute('tce_file');
$combinedIdentifier = $folderObject->getCombinedIdentifier();
$redirectValue = (string)$this->uriBuilder->buildUriFromRequest($request, $this->parameterProvider->getUrlParameters(['identifier' => $combinedIdentifier]));
$markup = [];
$markup[] = '<form class="pt-3 pb-3" action="' . htmlspecialchars($formAction) . '" method="post" name="editform" enctype="multipart/form-data">';
$markup[] = '<input type="hidden" name="data[upload][0][target]" value="' . htmlspecialchars($combinedIdentifier) . '" />';
$markup[] = '<input type="hidden" name="data[upload][0][data]" value="0" />';
$markup[] = '<input type="hidden" name="redirect" value="' . htmlspecialchars($redirectValue) . '" />';
$markup[] = '<div class="row">';
$markup[] = '<div class="col-auto me-auto">';
$markup[] = ' <h4>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.pagetitle')) . '</h4>';
$markup[] = '</div>';
$markup[] = '<div class="col-auto">';
$markup[] = '<div class="form-check form-switch">';
$markup[] = ' <input class="form-check-input" type="checkbox" name="overwriteExistingFiles" id="overwriteExistingFiles" value="replace" />';
$markup[] = ' <label class="form-check-label" for="overwriteExistingFiles">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:overwriteExistingFiles'));
$markup[] = ' </label>';
$markup[] = '</div>';
$markup[] = '</div>';
$markup[] = '<div class="col-12">';
$markup[] = '<div class="input-group">';
$markup[] = '<input type="file" multiple="multiple" name="upload_0[]" class="form-control" />';
$markup[] = '<input class="btn btn-default" type="submit" name="submit" value="'
. htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.submit')) . '" />';
$markup[] = '</div>';
$markup[] = '</div>';
$markup[] = '</div>';
// Only show file extension info if there is an actual limitation (not just '*')
if ($list !== ['*']) {
$markup[] = '<div class="form-text mt-1">';
$markup[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . ($denyList ? 'disallowedFileExtensions' : 'allowedFileExtensions')));
$markup[] = '<ul class="badge-list">' . implode(' ', $allowedFileExtensionsList) . '</ul>';
$markup[] = '</div>';
}
$markup[] = '</form>';
return implode(LF, $markup);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View;
/**
* List / Tile view decision used in Setup module.
*
* @internal
*/
enum SetupModuleViewMode: string
{
case LIST = 'list';
case TILES = 'tiles';
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\View;
/**
* basic / advanced view decision used in Setup module Site settings.
*
* @internal
*/
enum SetupSettingsViewMode: string
{
case BASIC = 'basic';
case ADVANCED = 'advanced';
}
@@ -0,0 +1,267 @@
<?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\ValueFormatter;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* The FlexFormValueFormatter formats a FlexForm value into a human-readable
* format. This is used internally to display changes in FlexForm values as a
* nicely formatted plain-text diff.
*
* @internal
*/
readonly class FlexFormValueFormatter
{
protected const VALUE_MAX_LENGTH = 50;
public function __construct(
private FlexFormTools $flexFormTools,
private TcaSchemaFactory $tcaSchemaFactory,
) {}
public function format(string $tableName, string $fieldName, ?string $value, int $uid, array $fieldConfiguration): string
{
if ($value === null || $value === '') {
return '';
}
$record = BackendUtility::getRecord($tableName, $uid);
if (is_null($record)) {
// Record is already deleted
return '';
}
// Get FlexForm data and structure
$flexFormDataArray = GeneralUtility::xml2array($value);
$flexFormDataStructure = $this->getFlexFormDataStructure($fieldConfiguration, $tableName, $fieldName, $record);
// Map data to FlexForm structure and build an easy to handle array
$processedSheets = $this->getProcessedSheets($flexFormDataStructure, $flexFormDataArray['data'] ?? []);
// Render a human-readable plain text representation of the FlexForm data
$renderedPlainValue = $this->renderFlexFormValuePlain($processedSheets);
return trim($renderedPlainValue, PHP_EOL);
}
/**
* @param array<string, mixed> $tcaConfiguration
* @param array<string, mixed> $record
*/
protected function getFlexFormDataStructure(array $tcaConfiguration, string $tableName, string $fieldName, array $record): array
{
$conf['config'] = $tcaConfiguration;
$schema = $this->tcaSchemaFactory->get($tableName);
return $this->flexFormTools->parseDataStructureByIdentifier(
$this->flexFormTools->getDataStructureIdentifier($conf, $tableName, $fieldName, $record, $schema),
$schema
);
}
/**
* @param array<string, mixed> $processedData
*/
protected function renderFlexFormValuePlain(array $processedData, int $currentHierarchy = 1): string
{
$value = '';
foreach ($processedData as $processedKey => $processedValue) {
$title = !empty($processedValue['section']) ? $processedKey : $processedValue['title'];
if (!empty($processedValue['children'])) {
$children = $this->renderFlexFormValuePlain($processedValue['children'], $currentHierarchy + 1);
if (empty($processedValue['section']) && empty($processedValue['container'])) {
$value .= $this->getSectionHeadline($title) . PHP_EOL . $children . PHP_EOL;
} elseif ($children) {
$value .= $children . PHP_EOL;
}
} elseif (isset($processedValue['value'])) {
$wrappedValue = $this->wrapValue($processedValue['value'], $title);
$colon = ':';
// Add space after colon, if it fits into one line.
if (!str_contains($wrappedValue, PHP_EOL)) {
$colon .= ' ';
}
$value .= $title . $colon . $wrappedValue . PHP_EOL . PHP_EOL;
}
}
return $value;
}
/**
* Get the formatted headline of a FlexForm section
*/
protected function getSectionHeadline(string $title): string
{
$sectionSpacer = str_repeat('-', self::VALUE_MAX_LENGTH);
return $title . PHP_EOL . $sectionSpacer;
}
protected function getProcessedSheets(array $dataStructure, array $valueStructure): array
{
$processedSheets = [];
foreach ($dataStructure['sheets'] as $sheetKey => $sheetStructure) {
if (!empty($sheetStructure['ROOT']['el'])) {
$sheetTitle = $sheetKey;
if (!empty($sheetStructure['ROOT']['sheetTitle'])) {
$sheetTitle = $this->getLanguageService()->sL($sheetStructure['ROOT']['sheetTitle']);
}
if (!empty($valueStructure[$sheetKey]['lDEF'])) {
$processedElements = $this->getProcessedElements($sheetStructure['ROOT']['el'], $valueStructure[$sheetKey]['lDEF']);
$processedSheets[$sheetKey] = [
'title' => $sheetTitle,
'children' => $processedElements,
];
}
}
}
return $processedSheets;
}
protected function getProcessedElements(array $dataStructure, array $valueStructure): array
{
$processedElements = [];
// Values used to fake TCA
$processingTableValue = StringUtility::getUniqueId('processing');
$processingColumnValue = StringUtility::getUniqueId('processing');
foreach ($dataStructure as $elementKey => $elementStructure) {
$elementTitle = $this->getElementTitle($elementKey, $elementStructure);
if (($elementStructure['type'] ?? '') === 'array') {
// Render section or container
if (empty($valueStructure[$elementKey]['el'])) {
continue;
}
if (!empty($elementStructure['section'])) {
// Render section
$processedElements[$elementKey] = [
'section' => true,
'title' => $elementTitle,
'children' => $this->getProcessedSections(
$elementStructure['el'],
$valueStructure[$elementKey]['el']
),
];
} else {
// Render container
$processedElements[$elementKey] = [
'container' => true,
'title' => $elementTitle,
'children' => $this->getProcessedElements(
$elementStructure['el'],
$valueStructure[$elementKey]['el']
),
];
}
} elseif (!empty($elementStructure['config'])) {
// Render plain elements
$relationTable = $this->getRelationTable($elementStructure['config']) ?? '';
if ($this->tcaSchemaFactory->has($relationTable)
&& ($userFunc = ($this->tcaSchemaFactory->get($relationTable)->getCapability(TcaSchemaCapability::Label)->getConfiguration()['generator'] ?? false))
) {
$parameters = [
'table' => $relationTable,
'row' => BackendUtility::getRecord($relationTable, $valueStructure[$elementKey]['vDEF'] ?? ''),
'title' => $valueStructure[$elementKey]['vDEF'] ?? '',
'options' => ($this->tcaSchemaFactory->get($relationTable)->getCapability(TcaSchemaCapability::Label)->getConfiguration()['generatorOptions'] ?? []),
];
GeneralUtility::callUserFunction($userFunc, $parameters);
$processedValue = $parameters['title'];
} else {
$processedValue = BackendUtility::getProcessedValue(
$processingTableValue,
$processingColumnValue,
$valueStructure[$elementKey]['vDEF'] ?? '',
0,
false,
false,
0,
true,
0,
[],
$elementStructure['config']
);
}
$processedElements[$elementKey] = [
'title' => $elementTitle,
'value' => $processedValue,
];
}
}
return $processedElements;
}
protected function getRelationTable(array $configuration): ?string
{
// If allowed tables is defined, but with only one(!) table:
if (($configuration['allowed'] ?? '') !== '' && !str_contains($configuration['allowed'], ',')) {
return $configuration['allowed'];
}
return $configuration['foreign_table'] ?? null;
}
protected function getProcessedSections(array $dataStructure, array $valueStructure): array
{
$processedSections = [];
foreach ($valueStructure as $sectionValueIndex => $sectionValueStructure) {
$processedSections[$sectionValueIndex] = [
'section' => true,
'children' => $this->getProcessedElements(
$dataStructure,
$sectionValueStructure
),
];
}
return $processedSections;
}
protected function getElementTitle(string $key, array $structure): string
{
if (!empty($structure['label'])) {
return $this->getLanguageService()->sL($structure['label']);
}
return $key;
}
protected function wrapValue(string $value, string $title): string
{
if ($value === '') {
return '';
}
// If the length of the value is equal or less than the maxlength, no wrapping is needed.
if ((mb_strlen($title) + mb_strlen($value)) <= self::VALUE_MAX_LENGTH) {
return $value;
}
// wordwrap the value and add an indention for each line.
$multilineIndention = "\t";
$value = PHP_EOL . $multilineIndention . $value;
$lines = explode(PHP_EOL, $value);
$newValue = '';
foreach (array_map(trim(...), $lines) as $line) {
if ($line !== '') {
$newValue .= PHP_EOL . $line;
}
}
$value = wordwrap($newValue, self::VALUE_MAX_LENGTH, PHP_EOL);
return str_replace(PHP_EOL, PHP_EOL . $multilineIndention, $value);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}