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