Commit a7372ce6 authored by Claus Due's avatar Claus Due Committed by Claus Due
Browse files

[FEATURE] Fluid-based replacement for PageLayoutView

Provides a completely rewritten alternative to PageLayoutView
based on Fluid, and deprecates the old PageLayoutView. Adds
a feature flag TYPO3_CONF_VARS.SYS.features.fluidBasedPageModule
which can be used to switch back to the deprecated
PageLayoutView for installations which require this. The feature
flag is enabled by default in all new installations.

See included feature RST for more detailed information.

Releases: master
Resolves: #90348
Change-Id: Icf10b12130c238c63db74a608c67942ba912d307
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63186


Tested-by: Kevin Appelt's avatarKevin Appelt <kevin.appelt@icloud.com>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Manuel Selbach's avatarManuel Selbach <manuel_selbach@yahoo.de>
Tested-by: Claus Due's avatarClaus Due <claus@phpmind.net>
Reviewed-by: Kevin Appelt's avatarKevin Appelt <kevin.appelt@icloud.com>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Manuel Selbach's avatarManuel Selbach <manuel_selbach@yahoo.de>
Reviewed-by: Claus Due's avatarClaus Due <claus@phpmind.net>
parent f66149f7
......@@ -268,7 +268,7 @@ class LocalizationController
return [
'columns' => $columns,
'columnList' => $backendLayouts['__colPosList'],
'columnList' => array_values($backendLayouts['__colPosList']),
];
}
}
......@@ -25,6 +25,7 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\PageLayoutView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
......@@ -609,17 +610,58 @@ class PageLayoutController
protected function renderContent(): string
{
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
$dbList = GeneralUtility::makeInstance(PageLayoutView::class);
$dbList->doEdit = $this->isContentEditable($this->current_sys_language);
$dbList->option_newWizard = empty($this->modTSconfig['properties']['disableNewContentElementWizard']);
$dbList->defLangBinding = !empty($this->modTSconfig['properties']['defLangBinding']);
$tableOutput = '';
$tcaItems = $this->backendLayouts->getColPosListItemsParsed($this->id);
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/DragDrop');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste');
if ($this->getBackendUser()->check('tables_select', 'tt_content')) {
$h_func_b = '';
if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('fluidBasedPageModule')) {
$selectedCombinedIdentifier = $this->backendLayouts->getSelectedCombinedIdentifier($this->id);
// If no backend layout is selected, use default
if (empty($selectedCombinedIdentifier)) {
$selectedCombinedIdentifier = 'default';
}
$backendLayout = $this->backendLayouts->getDataProviderCollection()->getBackendLayout(
$selectedCombinedIdentifier,
$this->id
);
$configuration = $backendLayout->getDrawingConfiguration();
$configuration->setPageId($this->id);
$configuration->setDefaultLanguageBinding(!empty($this->modTSconfig['properties']['defLangBinding']));
$configuration->setActiveColumns(GeneralUtility::trimExplode(',', $this->activeColPosList));
$configuration->setShowHidden((bool)$this->MOD_SETTINGS['tt_content_showHidden']);
$configuration->setLanguageColumns(array_combine(array_keys($this->MOD_MENU['language']), array_keys($this->MOD_MENU['language'])));
$configuration->setLanguageColumnsPointer((int)$this->current_sys_language);
if ($this->MOD_SETTINGS['function'] == 2) {
$configuration->setLanguageMode($this->MOD_SETTINGS['function'] == 2);
}
$pageLayoutDrawer = $backendLayout->getBackendLayoutRenderer();
$configuration->setShowNewContentWizard(empty($this->modTSconfig['properties']['disableNewContentElementWizard']));
$pageActionsCallback = null;
if ($configuration->isPageEditable()) {
$languageOverlayId = 0;
$pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->current_sys_language);
if (is_array($pageLocalizationRecord)) {
$pageLocalizationRecord = reset($pageLocalizationRecord);
}
if (!empty($pageLocalizationRecord['uid'])) {
$languageOverlayId = $pageLocalizationRecord['uid'];
}
$pageActionsCallback = 'function(PageActions) {
PageActions.setPageId(' . (int)$this->id . ');
PageActions.setLanguageOverlayId(' . $languageOverlayId . ');
}';
}
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/PageActions', $pageActionsCallback);
$numberOfHiddenElements = $this->getNumberOfHiddenElements($configuration->getLanguageColumns());
$tableOutput = $pageLayoutDrawer->drawContent();
} else {
$dbList = GeneralUtility::makeInstance(PageLayoutView::class);
$dbList->doEdit = $this->isContentEditable($this->current_sys_language);
$dbList->option_newWizard = empty($this->modTSconfig['properties']['disableNewContentElementWizard']);
$dbList->defLangBinding = !empty($this->modTSconfig['properties']['defLangBinding']);
$tcaItems = $this->backendLayouts->getColPosListItemsParsed($this->id);
$numberOfHiddenElements = $this->getNumberOfHiddenElements(is_array($dbList->tt_contentConfig['languageCols']) ? $dbList->tt_contentConfig['languageCols'] : []);
// Setting up the tt_content columns to show:
if (is_array($GLOBALS['TCA']['tt_content']['columns']['colPos']['config']['items'])) {
$colList = [];
......@@ -644,8 +686,19 @@ class PageLayoutController
$dbList->tt_contentConfig['languageCols'] = $this->MOD_MENU['language'];
$dbList->tt_contentConfig['languageColsPointer'] = $this->current_sys_language;
}
$tableOutput = $dbList->getTable_tt_content($this->id);
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/DragDrop');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LayoutModule/Paste');
}
$this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
if ($this->getBackendUser()->check('tables_select', 'tt_content')) {
$h_func_b = '';
// Toggle hidden ContentElements
$numberOfHiddenElements = $this->getNumberOfHiddenElements($dbList->tt_contentConfig);
if ($numberOfHiddenElements > 0) {
$h_func_b = '
<div class="checkbox">
......@@ -655,9 +708,9 @@ class PageLayoutController
</label>
</div>';
}
// Generate the list of content elements
$tableOutput = $dbList->getTable_tt_content($this->id) . $h_func_b;
}
$tableOutput .= $h_func_b;
// Init the content
$content = '';
// Additional header content
......@@ -819,10 +872,10 @@ class PageLayoutController
* Returns the number of hidden elements (including those hidden by start/end times)
* on the current page (for the current sys_language)
*
* @param array $contentConfig
* @param array $languageColumns
* @return int
*/
protected function getNumberOfHiddenElements(array $contentConfig = []): int
protected function getNumberOfHiddenElements(array $languageColumns): int
{
$andWhere = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
......@@ -841,7 +894,7 @@ class PageLayoutController
)
);
if (!empty($contentConfig['languageCols']) && is_array($contentConfig['languageCols'])) {
if (!empty($languageColumns)) {
// Multi-language view is active
if ($this->current_sys_language > 0) {
$queryBuilder->andWhere(
......
......@@ -14,6 +14,17 @@ namespace TYPO3\CMS\Backend\View\BackendLayout;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\Grid;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumn;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridRow;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\LanguageColumn;
use TYPO3\CMS\Backend\View\Drawing\BackendLayoutRenderer;
use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class to represent a backend layout.
*/
......@@ -44,21 +55,46 @@ class BackendLayout
*/
protected $configuration;
/**
* @var array
*/
protected $configurationArray;
/**
* @var array
*/
protected $data;
/**
* @var DrawingConfiguration
*/
protected $drawingConfiguration;
/**
* @var ContentFetcher
*/
protected $contentFetcher;
/**
* @var LanguageColumn
*/
protected $languageColumns = [];
/**
* @var RecordRememberer
*/
protected $recordRememberer;
/**
* @param string $identifier
* @param string $title
* @param string $configuration
* @param string|array $configuration
* @return BackendLayout
*/
public static function create($identifier, $title, $configuration)
{
return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
self::class,
static::class,
$identifier,
$title,
$configuration
......@@ -68,13 +104,20 @@ class BackendLayout
/**
* @param string $identifier
* @param string $title
* @param string $configuration
* @param string|array $configuration
*/
public function __construct($identifier, $title, $configuration)
{
$this->drawingConfiguration = GeneralUtility::makeInstance(DrawingConfiguration::class);
$this->contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $this);
$this->recordRememberer = GeneralUtility::makeInstance(RecordRememberer::class);
$this->setIdentifier($identifier);
$this->setTitle($title);
$this->setConfiguration($configuration);
if (is_array($configuration)) {
$this->setConfigurationArray($configuration);
} else {
$this->setConfiguration($configuration);
}
}
/**
......@@ -157,12 +200,62 @@ class BackendLayout
return $this->configuration;
}
/**
* @param array $configurationArray
*/
public function setConfigurationArray(array $configurationArray): void
{
if (!isset($configurationArray['__colPosList'], $configurationArray['__items'])) {
// Backend layout configuration is unprocessed, process it now to extract counts and column item lists
$colPosList = [];
$items = [];
$rowIndex = 0;
foreach ($configurationArray['backend_layout.']['rows.'] as $row) {
$index = 0;
$colCount = 0;
$columns = [];
foreach ($row['columns.'] as $column) {
if (!isset($column['colPos'])) {
continue;
}
$colPos = $column['colPos'];
$colPos = (int)$colPos;
$colPosList[$colPos] = $colPos;
$key = ($index + 1) . '.';
$columns[$key] = $column;
$items[$colPos] = [
(string)$this->getLanguageService()->sL($column['name']),
$colPos,
$column['icon']
];
$colCount += $column['colspan'] ? $column['colspan'] : 1;
++ $index;
}
++ $rowIndex;
}
$configurationArray['__config'] = $configurationArray;
$configurationArray['__colPosList'] = $colPosList;
$configurationArray['__items'] = $items;
}
$this->configurationArray = $configurationArray;
}
/**
* @return array
*/
public function getConfigurationArray(): array
{
return $this->configurationArray;
}
/**
* @param string $configuration
*/
public function setConfiguration($configuration)
{
$this->configuration = $configuration;
$this->parseConfigurationStringAndSetConfigurationArray($configuration);
}
/**
......@@ -180,4 +273,102 @@ class BackendLayout
{
$this->data = $data;
}
/**
* @return LanguageColumn[]
*/
public function getLanguageColumns(): iterable
{
if (empty($this->languageColumns)) {
$defaultLanguageElements = [];
$contentByColumn = $this->getContentFetcher()->getContentRecordsPerColumn(null, 0);
if (!empty($contentByColumn)) {
$defaultLanguageElements = array_merge(...$contentByColumn);
}
foreach ($this->getDrawingConfiguration()->getSiteLanguages() as $siteLanguage) {
if (!in_array($siteLanguage->getLanguageId(), $this->getDrawingConfiguration()->getLanguageColumns())) {
continue;
}
$backendLayout = clone $this;
$backendLayout->getDrawingConfiguration()->setLanguageColumnsPointer($siteLanguage->getLanguageId());
$this->languageColumns[] = GeneralUtility::makeInstance(LanguageColumn::class, $backendLayout, $siteLanguage, $defaultLanguageElements);
}
}
return $this->languageColumns;
}
public function getGrid(): Grid
{
$grid = GeneralUtility::makeInstance(Grid::class, $this);
foreach ($this->getConfigurationArray()['__config']['backend_layout.']['rows.'] as $row) {
$rowObject = GeneralUtility::makeInstance(GridRow::class, $this);
foreach ($row['columns.'] as $column) {
$columnObject = GeneralUtility::makeInstance(GridColumn::class, $this, $column);
$rowObject->addColumn($columnObject);
}
$grid->addRow($rowObject);
}
$allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false);
if (!$allowInconsistentLanguageHandling && $this->getLanguageModeIdentifier() === 'connected') {
$grid->setAllowNewContent(false);
}
return $grid;
}
public function getColumnPositionNumbers(): array
{
return $this->getConfigurationArray()['__colPosList'];
}
public function getContentFetcher(): ContentFetcher
{
return $this->contentFetcher;
}
public function setContentFetcher(ContentFetcher $contentFetcher): void
{
$this->contentFetcher = $contentFetcher;
}
public function getDrawingConfiguration(): DrawingConfiguration
{
return $this->drawingConfiguration;
}
public function getBackendLayoutRenderer(): BackendLayoutRenderer
{
return GeneralUtility::makeInstance(BackendLayoutRenderer::class, $this);
}
public function getRecordRememberer(): RecordRememberer
{
return $this->recordRememberer;
}
public function getLanguageModeIdentifier(): string
{
$contentRecordsPerColumn = $this->contentFetcher->getContentRecordsPerColumn(null, $this->drawingConfiguration->getLanguageColumnsPointer());
$contentRecords = empty($contentRecordsPerColumn) ? [] : array_merge(...$contentRecordsPerColumn);
$translationData = $this->contentFetcher->getTranslationData($contentRecords, $this->drawingConfiguration->getLanguageColumnsPointer());
return $translationData['mode'] ?? '';
}
protected function parseConfigurationStringAndSetConfigurationArray(string $configuration): void
{
$parser = GeneralUtility::makeInstance(TypoScriptParser::class);
$conditionMatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class);
$parser->parse(TypoScriptParser::checkIncludeLines($configuration), $conditionMatcher);
$this->setConfigurationArray($parser->setup);
}
public function __clone()
{
$this->drawingConfiguration = clone $this->drawingConfiguration;
$this->contentFetcher->setBackendLayout($this);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Backend\View\BackendLayout;
/*
* 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!
*/
use Doctrine\DBAL\Driver\Statement;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\PageLayoutView;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* 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 consistenty)
*/
class ContentFetcher
{
/**
* @var BackendLayout
*/
protected $backendLayout;
/**
* @var array
*/
protected $fetchedContentRecords = [];
/**
* Stores whether a certain language has translations in it
*
* @var array
*/
protected $languageHasTranslationsCache = [];
public function __construct(BackendLayout $backendLayout)
{
$this->backendLayout = $backendLayout;
}
public function setBackendLayout(BackendLayout $backendLayout): void
{
$this->backendLayout = $backendLayout;
}
/**
* Gets content records per column.
* This is required for correct workspace overlays.
*
* @param int|null $columnNumber
* @param int|null $languageId
* @return array Associative array for each column (colPos) or for all columns if $columnNumber is null
*/
public function getContentRecordsPerColumn(?int $columnNumber = null, ?int $languageId = null): iterable
{
if (empty($this->fetchedContentRecords)) {
$queryBuilder = $this->getQueryBuilder();
$records = $this->getResult($queryBuilder->execute());
foreach ($records as $record) {
$recordLanguage = (int)$record['sys_language_uid'];
$recordColumnNumber = (int)$record['colPos'];
$this->fetchedContentRecords[$recordLanguage][$recordColumnNumber][] = $record;
}
}
$languageId = $languageId ?? $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer();
$contentByLanguage = &$this->fetchedContentRecords[$languageId];
if ($columnNumber === null) {
return $contentByLanguage ?? [];
}
return $contentByLanguage[$columnNumber] ?? [];
}
public function getFlatContentRecords(): iterable
{
$languageId = $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer();
$contentRecords = $this->getContentRecordsPerColumn(null, $languageId);
return empty($contentRecords) ? [] : array_merge(...$contentRecords);
}
public function getUnusedRecords(): iterable
{
$unrendered = [];
$knownColumnPositionNumbers = $this->backendLayout->getColumnPositionNumbers();
$rememberer = $this->backendLayout->getRecordRememberer();
foreach ($this->fetchedContentRecords[$this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer()] ?? [] as $contentRecordsInColumn) {
foreach ($contentRecordsInColumn as $contentRecord) {
if (!$rememberer->isRemembered($contentRecord['uid']) && !in_array($contentRecord['colPos'], $knownColumnPositionNumbers)) {
$unrendered[] = $contentRecord;
}
}
}
return $unrendered;
}
public function getTranslationData(iterable $contentElements, int $language): array
{
$configuration = $this->backendLayout->getDrawingConfiguration();
if ($language === 0) {
return [];
}
if (!isset($this->languageHasTranslationsCache[$language])) {
foreach ($contentElements as $contentElement) {
if ((int)$contentElement['l18n_parent'] === 0) {
$this->languageHasTranslationsCache[$language]['hasStandAloneContent'] = true;
$this->languageHasTranslationsCache[$language]['mode'] = 'free';
}
if ((int)$contentElement['l18n_parent'] > 0) {
$this->languageHasTranslationsCache[$language]['hasTranslations'] = true;
$this->languageHasTranslationsCache[$language]['mode'] = 'connected';
}
}
if (!isset($this->languageHasTranslationsCache[$language])) {
$this->languageHasTranslationsCache[$language]['hasTranslations'] = false;
}
// Check for inconsistent translations, force "mixed" mode and dispatch a FlashMessage to user if such a case is encountered.
if (isset($this->languageHasTranslationsCache[$language]['hasStandAloneContent'])
&& $this->languageHasTranslationsCache[$language]['hasTranslations']
) {
$this->languageHasTranslationsCache[$language]['mode'] = 'mixed';
$siteLanguage = $configuration->getSiteLanguage($language);
$message = GeneralUtility::makeInstance(
FlashMessage::class,
sprintf($this->getLanguageService()->getLL('staleTranslationWarning'), $siteLanguage->getTitle()),
sprintf($this->getLanguageService()->getLL('staleTranslationWarningTitle'), $siteLanguage->getTitle()),
FlashMessage::WARNING
);
$service = GeneralUtility::makeInstance(FlashMessageService::class);
$queue = $service->getMessageQueueByIdentifier();
$queue->addMessage($message);
}
}
return $this->languageHasTranslationsCache[$language];
}
protected function getQueryBuilder(): QueryBuilder
{
$fields = ['*'];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
$queryBuilder
->select(...$fields)
->from('tt_content');