TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user