diff --git a/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php b/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php index 88e67d0847050778d76eae4b3bdbd834cf3722f5..574384b1d4d8487b7395b16a1ae2b34fc8ac0dea 100644 --- a/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php +++ b/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php @@ -268,7 +268,7 @@ class LocalizationController return [ 'columns' => $columns, - 'columnList' => $backendLayouts['__colPosList'], + 'columnList' => array_values($backendLayouts['__colPosList']), ]; } } diff --git a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php index 7d3130b0c81346b87f3b02bc9752f792aed050c0..c4485d0dc1e0cc1e9560d73cabdc8789b7b952f8 100644 --- a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php +++ b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php @@ -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( diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php index 4585fcb0c958b2e1137c80b1912e875de3c48111..11ff0b6cd1f2e64d59da9ecff62c04f6b5ff795a 100644 --- a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php +++ b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php @@ -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']; + } } diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php b/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php new file mode 100644 index 0000000000000000000000000000000000000000..de78b4795f227d13605cbc0cdd9ffbe896cac7aa --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php @@ -0,0 +1,225 @@ +<?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'); + + $queryBuilder->andWhere( + $queryBuilder->expr()->eq( + 'tt_content.pid', + $queryBuilder->createNamedParameter($this->backendLayout->getDrawingConfiguration()->getPageId(), \PDO::PARAM_INT) + ) + ); + + $additionalConstraints = []; + $parameters = [ + 'table' => 'tt_content', + 'fields' => $fields, + 'groupBy' => null, + 'orderBy' => null + ]; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] ?? [] as $className) { + $hookObject = GeneralUtility::makeInstance($className); + if (method_exists($hookObject, 'modifyQuery')) { + $hookObject->modifyQuery( + $parameters, + 'tt_content', + $this->backendLayout->getDrawingConfiguration()->getPageId(), + $additionalConstraints, + $fields, + $queryBuilder + ); + } + } + + return $queryBuilder; + } + + protected function getResult(Statement $result): array + { + $output = []; + while ($row = $result->fetch()) { + BackendUtility::workspaceOL('tt_content', $row, -99, true); + if ($row) { + $output[] = $row; + } + } + return $output; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/AbstractGridObject.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/AbstractGridObject.php new file mode 100644 index 0000000000000000000000000000000000000000..3faf0b25be4f08d21d191046545c2e5317933a47 --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/AbstractGridObject.php @@ -0,0 +1,79 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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 TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\StringUtility; + +/** + * Base class for objects which constitute a page layout grid. + * + * Contains shared properties and functions available to all such objects. + * + * @see Grid + * @see GridRow + * @see GridColumn + * @see GridColumnItem + * @see LanguageColumn + */ +abstract class AbstractGridObject +{ + /** + * @var BackendLayout + */ + protected $backendLayout; + + /** + * @var IconFactory + */ + protected $iconFactory; + + /** + * @var string|null + */ + protected $uniqueId; + + public function __construct(BackendLayout $backendLayout) + { + $this->backendLayout = $backendLayout; + $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); + } + + public function getUniqueId(): string + { + return $this->uniqueId ?? $this->uniqueId = StringUtility::getUniqueId(); + } + + public function getBackendLayout(): BackendLayout + { + return $this->backendLayout; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/Grid.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/Grid.php new file mode 100644 index 0000000000000000000000000000000000000000..a9300753bdac1e15e86518a1445c6c49298050ac --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/Grid.php @@ -0,0 +1,90 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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! + */ + +/** + * 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. + */ +class Grid extends AbstractGridObject +{ + /** + * @var GridRow[] + */ + protected $rows = []; + + /** + * @var bool + */ + protected $allowNewContent = true; + + 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 isAllowNewContent(): bool + { + return $this->allowNewContent; + } + + public function setAllowNewContent(bool $allowNewContent): void + { + $this->allowNewContent = $allowNewContent; + } + + public function getSpan(): int + { + if (!isset($this->rows[0]) || $this->backendLayout->getDrawingConfiguration()->getLanguageMode()) { + return 1; + } + $span = 0; + foreach ($this->rows[0]->getColumns() ?? [] as $column) { + $span += $column->getColSpan(); + } + return $span ? $span : 1; + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php new file mode 100644 index 0000000000000000000000000000000000000000..3750fd7de332a96f52b992f096b19fbcd759672a --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php @@ -0,0 +1,228 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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 TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; +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. + */ +class GridColumn extends AbstractGridObject +{ + /** + * @var GridColumnItem[] + */ + protected $items = []; + + /** + * @var int|null + */ + protected $columnNumber; + + /** + * @var string + */ + protected $columnName = 'default'; + + /** + * @var string|null + */ + protected $icon; + + /** + * @var int + */ + protected $colSpan = 1; + + /** + * @var int + */ + protected $rowSpan = 1; + + /** + * @var array + */ + protected $records; + + public function __construct(BackendLayout $backendLayout, array $columnDefinition, ?array $records = null) + { + parent::__construct($backendLayout); + $this->columnNumber = isset($columnDefinition['colPos']) ? (int)$columnDefinition['colPos'] : $this->columnNumber; + $this->columnName = $columnDefinition['name'] ?? $this->columnName; + $this->icon = $columnDefinition['icon'] ?? $this->icon; + $this->colSpan = (int)($columnDefinition['colspan'] ?? $this->colSpan); + $this->rowSpan = (int)($columnDefinition['rowspan'] ?? $this->rowSpan); + if ($this->columnNumber !== null) { + $this->records = $records ?? $backendLayout->getContentFetcher()->getContentRecordsPerColumn($this->columnNumber, $backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer()); + foreach ($this->records as $contentRecord) { + $columnItem = GeneralUtility::makeInstance(GridColumnItem::class, $backendLayout, $this, $contentRecord); + $this->addItem($columnItem); + } + } + } + + public function isActive(): bool + { + return $this->columnNumber !== null && in_array($this->columnNumber, $this->backendLayout->getDrawingConfiguration()->getActiveColumns()); + } + + public function addItem(GridColumnItem $item): void + { + $this->items[] = $item; + } + + public function getRecords(): iterable + { + return $this->records; + } + + /** + * @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->backendLayout->getDrawingConfiguration()->getLanguageMode()) { + return 1; + } + return $this->colSpan; + } + + public function getRowSpan(): int + { + if ($this->backendLayout->getDrawingConfiguration()->getLanguageMode()) { + return 1; + } + return $this->rowSpan; + } + + public function getAllContainedItemUids(): iterable + { + $uids = []; + foreach ($this->items as $columnItem) { + $uids[] = $columnItem->getRecord()['uid']; + } + return $uids; + } + + public function getEditUrl(): ?string + { + if (empty($this->items)) { + return null; + } + $pageRecord = $this->backendLayout->getDrawingConfiguration()->getPageRecord(); + if (!$this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT) + && !$this->getBackendUser()->checkLanguageAccess(0)) { + return null; + } + $pageTitleParamForAltDoc = '&recTitle=' . rawurlencode( + BackendUtility::getRecordTitle('pages', $pageRecord, true) + ); + $editParam = '&edit[tt_content][' . implode(',', $this->getAllContainedItemUids()) . ']=edit' . $pageTitleParamForAltDoc; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + return $uriBuilder->buildUriFromRoute('record_edit') . $editParam . '&returnUrl=' . rawurlencode(GeneralUtility::getIndpEnv('REQUEST_URI')); + } + + public function getNewContentUrl(): string + { + $pageId = $this->backendLayout->getDrawingConfiguration()->getPageId(); + $urlParameters = [ + 'id' => $pageId, + 'sys_language_uid' => $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer(), + 'colPos' => $this->getColumnNumber(), + 'uid_pid' => $pageId, + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ]; + $routeName = BackendUtility::getPagesTSconfig($pageId)['mod.']['newContentElementWizard.']['override'] + ?? 'new_content_element_wizard'; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + return (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters); + } + + public function getTitle(): string + { + $columnNumber = $this->getColumnNumber(); + $colTitle = (string)BackendUtility::getProcessedValue('tt_content', 'colPos', $columnNumber); + $tcaItems = $this->backendLayout->getConfigurationArray()['__items']; + foreach ($tcaItems as $item) { + if ($item[1] === $columnNumber) { + $colTitle = (string)$this->getLanguageService()->sL($item[0]); + } + } + return $colTitle; + } + + public function getTitleInaccessible(): string + { + return $this->getLanguageService()->sL($this->columnName) . ' (' . $this->getLanguageService()->getLL('noAccess') . ')'; + } + + public function getTitleUnassigned(): string + { + return $this->getLanguageService()->getLL('notAssigned'); + } + + public function isUnassigned(): bool + { + return $this->columnNumber === null; + } + + public function isContentEditable(): bool + { + if ($this->columnName === 'unused' || $this->columnNumber === null) { + return false; + } + if ($this->getBackendUser()->isAdmin()) { + return true; + } + $pageRecord = $this->backendLayout->getDrawingConfiguration()->getPageRecord(); + return !$pageRecord['editlock'] + && $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT) + && $this->getBackendUser()->checkLanguageAccess($this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer()); + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php new file mode 100644 index 0000000000000000000000000000000000000000..024ee7780a116b71c8cf0d6138b01cf4c7a37692 --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php @@ -0,0 +1,486 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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 TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Type\Bitmask\Permission; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Versioning\VersionState; + +/** + * 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. + */ +class GridColumnItem extends AbstractGridObject +{ + protected $record = []; + + /** + * @var GridColumn + */ + protected $column; + + public function __construct(BackendLayout $backendLayout, GridColumn $column, array $record) + { + parent::__construct($backendLayout); + $this->column = $column; + $this->record = $record; + $backendLayout->getRecordRememberer()->rememberRecordUid($record['uid']); + $backendLayout->getRecordRememberer()->rememberRecordUid($record['l18n_parent']); + } + + public function isVersioned(): bool + { + return $this->record['_ORIG_uid'] > 0; + } + + public function getPreview(): string + { + $item = $this; + $row = $item->getRecord(); + $configuration = $this->backendLayout->getDrawingConfiguration(); + $out = ''; + $outHeader = ''; + + if ($row['header']) { + $hiddenHeaderNote = ''; + // If header layout is set to 'hidden', display an accordant note: + if ($row['header_layout'] == 100) { + $hiddenHeaderNote = ' <em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden')) . ']</em>'; + } + $outHeader = $row['date'] + ? htmlspecialchars($configuration->getItemLabels()['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />' + : ''; + $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row) + . $hiddenHeaderNote . '</strong><br />'; + } + + $drawItem = true; + + // Draw preview of the item depending on its CType (if not disabled by previous hook): + if ($drawItem) { + switch ($row['CType']) { + case 'header': + if ($row['subheader']) { + $out .= $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br />'; + } + break; + case 'bullets': + case 'table': + if ($row['bodytext']) { + $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />'; + } + break; + case 'uploads': + if ($row['media']) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'media'), $row) . '<br />'; + } + break; + case 'shortcut': + if (!empty($row['records'])) { + $shortcutContent = []; + $recordList = explode(',', $row['records']); + foreach ($recordList as $recordIdentifier) { + $split = BackendUtility::splitTable_Uid($recordIdentifier); + $tableName = empty($split[0]) ? 'tt_content' : $split[0]; + $shortcutRecord = BackendUtility::getRecord($tableName, $split[1]); + if (is_array($shortcutRecord)) { + $icon = $this->iconFactory->getIconForRecord($tableName, $shortcutRecord, Icon::SIZE_SMALL)->render(); + $icon = BackendUtility::wrapClickMenuOnIcon( + $icon, + $tableName, + $shortcutRecord['uid'] + ); + $shortcutContent[] = $icon + . htmlspecialchars(BackendUtility::getRecordTitle($tableName, $shortcutRecord)); + } + } + $out .= implode('<br />', $shortcutContent) . '<br />'; + } + break; + case 'list': + $hookOut = ''; + $_params = ['pObj' => &$this, 'row' => $row, 'infoArr' => []]; + foreach ( + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$row['list_type']] ?? + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['_DEFAULT'] ?? + [] as $_funcRef + ) { + $hookOut .= GeneralUtility::callUserFunction($_funcRef, $_params, $this); + } + if ((string)$hookOut !== '') { + $out .= $hookOut; + } elseif (!empty($row['list_type'])) { + $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']); + if (!empty($label)) { + $out .= $this->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL($label)) . '</strong>', $row) . '<br />'; + } else { + $message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $row['list_type']); + $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; + } + } else { + $out .= '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>'; + } + $out .= htmlspecialchars($this->getLanguageService()->sL( + BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages']) + )) . '<br />'; + break; + default: + $contentType = $this->backendLayout->getDrawingConfiguration()->getContentTypeLabels()[$row['CType']]; + if (!isset($contentType)) { + $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']); + } + + if ($contentType) { + $out .= $this->linkEditContent('<strong>' . htmlspecialchars($contentType) . '</strong>', $row) . '<br />'; + if ($row['bodytext']) { + $out .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />'; + } + if ($row['image']) { + $out .= $this->linkEditContent($this->getThumbCodeUnlinked($row, 'tt_content', 'image'), $row) . '<br />'; + } + } else { + $message = sprintf( + $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), + $row['CType'] + ); + $out .= '<span class="label label-warning">' . htmlspecialchars($message) . '</span>'; + } + } + } + $out = '<span class="exampleContent">' . $out . '</span>'; + $out = $outHeader . $out; + if ($item->isDisabled()) { + return '<span class="text-muted">' . $out . '</span>'; + } + return $out; + } + + public function getWrapperClassName(): string + { + $wrapperClassNames = []; + if ($this->isDisabled()) { + $wrapperClassNames[] = 't3-page-ce-hidden t3js-hidden-record'; + } elseif (!in_array($this->record['colPos'], $this->backendLayout->getColumnPositionNumbers())) { + $wrapperClassNames[] = 't3-page-ce-warning'; + } + + return implode(' ', $wrapperClassNames); + } + + public function isDelible(): bool + { + $backendUser = $this->getBackendUser(); + if (!$backendUser->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)) { + return false; + } + return !(bool)($backendUser->getTSConfig()['options.']['disableDelete.']['tt_content'] ?? $backendUser->getTSConfig()['options.']['disableDelete'] ?? false); + } + + public function getDeleteUrl(): string + { + $params = '&cmd[tt_content][' . $this->record['uid'] . '][delete]=1'; + return BackendUtility::getLinkToDataHandlerAction($params); + } + + public function getDeleteTitle(): string + { + return $this->getLanguageService()->getLL('deleteItem'); + } + + public function getDeleteConfirmText(): string + { + return $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title'); + } + + public function getDeleteCancelText(): string + { + return $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'); + } + + public function getFooterInfo(): iterable + { + $info = []; + $this->getProcessedValue('starttime,endtime,fe_group,space_before_class,space_after_class', $info); + + if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) { + $info[] = $this->record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]; + } + + return $info; + } + + /** + * Renders the language flag and language title, but only if an icon is given, otherwise just the language + * + * @param SiteLanguage $language + * @return string + */ + protected function renderLanguageFlag(SiteLanguage $language) + { + $title = htmlspecialchars($language->getTitle()); + if ($language->getFlagIdentifier()) { + $icon = $this->iconFactory->getIcon( + $language->getFlagIdentifier(), + Icon::SIZE_SMALL + )->render(); + return '<span title="' . $title . '">' . $icon . '</span> ' . $title; + } + return $title; + } + + /** + * Create thumbnail code for record/field but not linked + * + * @param mixed[] $row Record array + * @param string $table Table (record is from) + * @param string $field Field name for which thumbnail are to be rendered. + * @return string HTML for thumbnails, if any. + */ + protected function getThumbCodeUnlinked($row, $table, $field) + { + return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false); + } + + /** + * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE. + * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button + * + * @param string $str String to link. Must be prepared for HTML output. + * @param array $row The row. + * @return string If the whole thing was editable $str is return with link around. Otherwise just $str. + */ + public function linkEditContent($str, $row) + { + if ($this->getBackendUser()->recordEditAccessInternals('tt_content', $row)) { + $urlParameters = [ + 'edit' => [ + 'tt_content' => [ + $row['uid'] => 'edit' + ] + ], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $row['uid'] + ]; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>'; + } + return $str; + } + + /** + * Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping etc. + * + * @param string $input Input string + * @return string Output string + */ + public function renderText($input): string + { + $input = strip_tags($input); + $input = GeneralUtility::fixed_lgd_cs($input, 1500); + return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false)); + } + + protected function getProcessedValue(string $fieldList, array &$info): void + { + $itemLabels = $this->backendLayout->getDrawingConfiguration()->getItemLabels(); + $fieldArr = explode(',', $fieldList); + foreach ($fieldArr as $field) { + if ($this->record[$field]) { + $info[] = '<strong>' . htmlspecialchars($itemLabels[$field]) . '</strong> ' + . htmlspecialchars(BackendUtility::getProcessedValue('tt_content', $field, $this->record[$field])); + } + } + } + + public function getIcons(): string + { + $table = 'tt_content'; + $row = $this->record; + $icons = []; + + if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) { + $toolTip = BackendUtility::getRecordToolTip($row, $table); + $icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>'; + $icons[] = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']); + } + $icons[] = $this->renderLanguageFlag($this->backendLayout->getDrawingConfiguration()->getSiteLanguage((int)$row['sys_language_uid'])); + + if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) { + $icons[] = '<a href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">' + . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</a>'; + } + + $_params = ['tt_content', $row['uid'], &$row]; + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) { + $icons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this); + } + return implode(' ', $icons); + } + + public function getRecord(): array + { + return $this->record; + } + + public function getColumn(): GridColumn + { + return $this->column; + } + + public function isDisabled(): bool + { + $table = 'tt_content'; + $row = $this->getRecord(); + $enableCols = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']; + return $enableCols['disabled'] && $row[$enableCols['disabled']] + || $enableCols['starttime'] && $row[$enableCols['starttime']] > $GLOBALS['EXEC_TIME'] + || $enableCols['endtime'] && $row[$enableCols['endtime']] && $row[$enableCols['endtime']] < $GLOBALS['EXEC_TIME']; + } + + public function hasTranslation(): bool + { + $contentElements = $this->column->getRecords(); + $id = $this->backendLayout->getDrawingConfiguration()->getPageId(); + $language = $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer(); + // If in default language, you may always create new entries + // Also, you may override this strict behavior via user TS Config + // If you do so, you're on your own and cannot rely on any support by the TYPO3 core. + $allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false); + if ($language === 0 || $allowInconsistentLanguageHandling) { + return false; + } + + return $this->backendLayout->getContentFetcher()->getTranslationData($contentElements, $language)['hasTranslations'] ?? false; + } + + public function isDeletePlaceholder(): bool + { + return VersionState::cast($this->record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER); + } + + public function isEditable(): bool + { + $languageId = $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer(); + if ($this->getBackendUser()->isAdmin()) { + return true; + } + $pageRecord = $this->backendLayout->getDrawingConfiguration()->getPageRecord(); + return !$pageRecord['editlock'] + && $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT) + && ($languageId === null || $this->getBackendUser()->checkLanguageAccess($languageId)); + } + + public function isDragAndDropAllowed(): bool + { + $pageRecord = $this->backendLayout->getDrawingConfiguration()->getPageRecord(); + return (int)$this->record['l18n_parent'] === 0 && + ( + $this->getBackendUser()->isAdmin() + || ((int)$this->record['editlock'] === 0 && (int)$pageRecord['editlock'] === 0) + && $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT) + && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $this->record['CType'], $GLOBALS['TYPO3_CONF_VARS']['BE']['explicitADmode']) + ) + ; + } + + public function getNewContentAfterLinkTitle(): string + { + return $this->getLanguageService()->getLL('newContentElement'); + } + + public function getNewContentAfterTitle(): string + { + return $this->getLanguageService()->getLL('content'); + } + + public function getNewContentAfterUrl(): string + { + $pageId = $this->backendLayout->getDrawingConfiguration()->getPageId(); + $urlParameters = [ + 'id' => $pageId, + 'sys_language_uid' => $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer(), + 'colPos' => $this->column->getColumnNumber(), + 'uid_pid' => -$this->record['uid'], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ]; + $routeName = BackendUtility::getPagesTSconfig($pageId)['mod.']['newContentElementWizard.']['override'] + ?? 'new_content_element_wizard'; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + return (string)$uriBuilder->buildUriFromRoute($routeName, $urlParameters); + } + + public function getVisibilityToggleUrl(): string + { + $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; + if ($this->record[$hiddenField]) { + $value = 0; + } else { + $value = 1; + } + $params = '&data[tt_content][' . ($this->record['_ORIG_uid'] ?: $this->record['uid']) + . '][' . $hiddenField . ']=' . $value; + return BackendUtility::getLinkToDataHandlerAction($params) . '#element-tt_content-' . $this->record['uid']; + } + + public function getVisibilityToggleTitle(): string + { + $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; + return $this->getLanguageService()->getLL($this->record[$hiddenField] ? 'unhide' : 'hide'); + } + + public function getVisibilityToggleIconName(): string + { + $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; + return $this->record[$hiddenField] ? 'unhide' : 'hide'; + } + + public function isVisibilityToggling(): bool + { + $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; + return $hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField] + && ( + !$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude'] + || $this->getBackendUser()->check('non_exclude_fields', 'tt_content:' . $hiddenField) + ) + ; + } + + public function getEditUrl(): string + { + $urlParameters = [ + 'edit' => [ + 'tt_content' => [ + $this->record['uid'] => 'edit', + ] + ], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') . '#element-tt_content-' . $this->record['uid'], + ]; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + return (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $this->record['uid']; + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridRow.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridRow.php new file mode 100644 index 0000000000000000000000000000000000000000..9a456c7fb22ff6ff350c3eff4b1ce2e75d08254e --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridRow.php @@ -0,0 +1,46 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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! + */ + +/** + * 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. + */ +class GridRow extends AbstractGridObject +{ + /** + * @var GridColumn[] + */ + protected $columns = []; + + public function addColumn(GridColumn $column): void + { + $this->columns[$column->getColumnNumber()] = $column; + } + + /** + * @return GridColumn[] + */ + public function getColumns(): iterable + { + return $this->columns; + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/LanguageColumn.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/LanguageColumn.php new file mode 100644 index 0000000000000000000000000000000000000000..4e0196813a4de17c23b2bdf7ab2a572b1135852b --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/LanguageColumn.php @@ -0,0 +1,260 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; + +/* + * 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 TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Versioning\VersionState; + +/** + * 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. + */ +class LanguageColumn extends AbstractGridObject +{ + /** + * @var SiteLanguage + */ + protected $siteLanguage; + + /** + * @var array + */ + protected $localizedPageRecord = []; + + /** + * @var array + */ + protected $defaultLanguageElements = []; + + /** + * @var array + */ + protected $flatContentOfLanguage = []; + + /** + * @var GridColumn|null + */ + protected $grid; + + public function __construct(BackendLayout $backendLayout, SiteLanguage $language, array $defaultLanguageElements) + { + parent::__construct($backendLayout); + $this->siteLanguage = $language; + $this->defaultLanguageElements = $defaultLanguageElements; + if ($this->siteLanguage->getLanguageId() > 0) { + $pageLocalizationRecord = BackendUtility::getRecordLocalization( + 'pages', + $backendLayout->getDrawingConfiguration()->getPageId(), + $language->getLanguageId() + ); + if (is_array($pageLocalizationRecord)) { + $pageLocalizationRecord = reset($pageLocalizationRecord); + } + BackendUtility::workspaceOL('pages', $pageLocalizationRecord); + $this->localizedPageRecord = $pageLocalizationRecord; + } else { + $this->localizedPageRecord = $backendLayout->getDrawingConfiguration()->getPageRecord(); + } + + $contentFetcher = $backendLayout->getContentFetcher(); + $contentRecords = $contentFetcher->getContentRecordsPerColumn(null, $language->getLanguageId()); + if (!empty($contentRecords)) { + $this->flatContentOfLanguage = array_merge(...$contentRecords); + } else { + $this->flatContentOfLanguage = []; + } + } + + public function getLocalizedPageRecord(): ?array + { + return $this->localizedPageRecord ?: null; + } + + public function getSiteLanguage(): SiteLanguage + { + return $this->siteLanguage; + } + + public function getGrid(): Grid + { + if (empty($this->grid)) { + $this->grid = $this->backendLayout->getGrid(); + } + return $this->grid; + } + + public function getLanguageModeLabelClass(): string + { + $contentRecordsPerColumn = $this->backendLayout->getContentFetcher()->getFlatContentRecords(); + $translationData = $this->backendLayout->getContentFetcher()->getTranslationData($contentRecordsPerColumn, $this->siteLanguage->getLanguageId()); + return $translationData['mode'] === 'mixed' ? 'danger' : 'info'; + } + + public function getLanguageMode(): string + { + switch ($this->backendLayout->getLanguageModeIdentifier()) { + case 'mixed': + $languageMode = $this->getLanguageService()->getLL('languageModeMixed'); + break; + case 'connected': + $languageMode = $this->getLanguageService()->getLL('languageModeConnected'); + break; + case 'free': + $languageMode = $this->getLanguageService()->getLL('languageModeFree'); + break; + default: + $languageMode = ''; + } + return $languageMode; + } + + public function getPageIcon(): string + { + return BackendUtility::wrapClickMenuOnIcon( + $this->iconFactory->getIconForRecord('pages', $this->localizedPageRecord, Icon::SIZE_SMALL)->render(), + 'pages', + $this->localizedPageRecord['uid'] + ); + } + + public function getAllowTranslate(): bool + { + if ($this->siteLanguage->getLanguageId() === 0) { + return false; + } + + $localizationTsConfig = BackendUtility::getPagesTSconfig($this->backendLayout->getDrawingConfiguration()->getPageId())['mod.']['web_layout.']['localization.'] ?? []; + $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true); + if (!$allowTranslate) { + return false; + } + + $translationData = $this->backendLayout->getContentFetcher()->getTranslationData($this->flatContentOfLanguage, $this->siteLanguage->getLanguageId()); + if (!empty($translationData)) { + if (isset($translationData['hasStandAloneContent'])) { + return false; + } + } + + $defaultLanguageUids = array_flip(array_column($this->defaultLanguageElements, 'uid')); + $translatedLanguageUids = array_column($this->flatContentOfLanguage, 'l10n_source'); + if (empty($translatedLanguageUids)) { + return true; + } + + foreach ($translatedLanguageUids as $translatedUid) { + unset($defaultLanguageUids[$translatedUid]); + } + + return !empty($defaultLanguageUids); + } + + public function getTranslationData(): array + { + $contentFetcher = $this->backendLayout->getContentFetcher(); + return $contentFetcher->getTranslationData($this->defaultLanguageElements, $this->siteLanguage->getLanguageId()); + } + + public function getAllowTranslateCopy(): bool + { + $localizationTsConfig = BackendUtility::getPagesTSconfig($this->backendLayout->getDrawingConfiguration()->getPageId())['mod.']['web_layout.']['localization.'] ?? []; + $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true); + if (!empty($translationData)) { + if (isset($translationData['hasStandAloneContent'])) { + return false; + } + if (isset($translationData['hasTranslations'])) { + $allowCopy = $allowCopy && !$translationData['hasTranslations']; + } + } + return $allowCopy; + } + + public function getTranslatePageTitle(): string + { + return $this->getLanguageService()->getLL('newPageContent_translate'); + } + + public function getAllowEditPage(): bool + { + return $this->getBackendUser()->check('tables_modify', 'pages'); + } + + public function getPageEditTitle(): string + { + return $this->getLanguageService()->getLL('edit'); + } + + public function getPageEditUrl(): string + { + $urlParameters = [ + 'edit' => [ + 'pages' => [ + $this->localizedPageRecord['uid'] => 'edit' + ] + ], + // Disallow manual adjustment of the language field for pages + 'overrideVals' => [ + 'pages' => [ + 'sys_language_uid' => $this->siteLanguage->getLanguageId() + ] + ], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ]; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + return (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + } + + public function getAllowViewPage(): bool + { + return !VersionState::cast($this->backendLayout->getDrawingConfiguration()->getPageRecord()['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER); + } + + public function getViewPageLinkTitle(): string + { + return $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'); + } + + public function getViewPageOnClick(): string + { + $pageId = $this->backendLayout->getDrawingConfiguration()->getPageId(); + return BackendUtility::viewOnClick( + $pageId, + '', + BackendUtility::BEgetRootLine($pageId), + '', + '', + '&L=' . $this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer() + ); + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/RecordRememberer.php b/typo3/sysext/backend/Classes/View/BackendLayout/RecordRememberer.php new file mode 100644 index 0000000000000000000000000000000000000000..fb1d4fe2241bf62283b2a05113418d48d357af67 --- /dev/null +++ b/typo3/sysext/backend/Classes/View/BackendLayout/RecordRememberer.php @@ -0,0 +1,34 @@ +<?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 TYPO3\CMS\Core\SingletonInterface; + +class RecordRememberer implements SingletonInterface +{ + protected $rememberedUids = []; + + public function rememberRecordUid(int $uid): void + { + $this->rememberedUids[$uid] = $uid; + } + + public function isRemembered(int $uid): bool + { + return isset($this->rememberedUids[$uid]); + } +} diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php index a2abc67dfe11c67893903d9aba6a3e2aa5269f80..b238d77725b082c6fcf829465b41cd0c51b65329 100644 --- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php +++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php @@ -16,7 +16,6 @@ namespace TYPO3\CMS\Backend\View; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -353,42 +352,7 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId); } - if (!empty($backendLayout)) { - /** @var TypoScriptParser $parser */ - $parser = GeneralUtility::makeInstance(TypoScriptParser::class); - /** @var \TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher $conditionMatcher */ - $conditionMatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class); - $parser->parse(TypoScriptParser::checkIncludeLines($backendLayout->getConfiguration()), $conditionMatcher); - - $backendLayoutData = []; - $backendLayoutData['config'] = $backendLayout->getConfiguration(); - $backendLayoutData['__config'] = $parser->setup; - $backendLayoutData['__items'] = []; - $backendLayoutData['__colPosList'] = []; - - // create items and colPosList - if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) { - foreach ($backendLayoutData['__config']['backend_layout.']['rows.'] as $row) { - if (!empty($row['columns.'])) { - foreach ($row['columns.'] as $column) { - if (!isset($column['colPos'])) { - continue; - } - $backendLayoutData['__items'][] = [ - $this->getColumnName($column), - $column['colPos'], - null - ]; - $backendLayoutData['__colPosList'][] = $column['colPos']; - } - } - } - } - - $this->selectedBackendLayout[$pageId] = $backendLayoutData; - } - - return $backendLayoutData; + return $backendLayout->getConfigurationArray(); } /** @@ -471,21 +435,4 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface { return $GLOBALS['LANG']; } - - /** - * Get column name from colPos item structure - * - * @param array $column - * @return string - */ - protected function getColumnName($column) - { - $columnName = $column['name']; - - if (GeneralUtility::isFirstPartOfStr($columnName, 'LLL:')) { - $columnName = $this->getLanguageService()->sL($columnName); - } - - return $columnName; - } } diff --git a/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php new file mode 100644 index 0000000000000000000000000000000000000000..dace807aab1442a304985a8df8fda7b0ab2ceaaf --- /dev/null +++ b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php @@ -0,0 +1,202 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\Drawing; + +/* + * 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 Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Backend\Clipboard\Clipboard; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; +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\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Page\PageRenderer; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext; +use TYPO3\CMS\Extbase\Mvc\Request; +use TYPO3\CMS\Extbase\Object\ObjectManager; +use TYPO3\CMS\Fluid\View\TemplateView; + +/** + * 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. + * + * - Initializes the clipboard used in the page layout + * - Inserts an encoded paste icon as JS which is made visible when clipboard elements are registered + */ +class BackendLayoutRenderer +{ + use LoggerAwareTrait; + + /** + * @var IconFactory + */ + protected $iconFactory; + + /** + * @var BackendLayout + */ + protected $backendLayout; + + /** + * @var Clipboard + */ + protected $clipboard; + + /** + * @var TemplateView + */ + protected $view; + + public function __construct(BackendLayout $backendLayout) + { + $this->backendLayout = $backendLayout; + $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); + $this->initializeClipboard(); + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + $controllerContext = $objectManager->get(ControllerContext::class); + $request = $objectManager->get(Request::class); + $controllerContext->setRequest($request); + $this->view = GeneralUtility::makeInstance(TemplateView::class); + $this->view->getRenderingContext()->setControllerContext($controllerContext); + $this->view->getRenderingContext()->getTemplatePaths()->fillDefaultsByPackageName('backend'); + $this->view->getRenderingContext()->setControllerName('PageLayout'); + $this->view->assign('backendLayout', $backendLayout); + } + + /** + * @param bool $renderUnused If true, renders the bottom column with unused records + * @return string + */ + public function drawContent(bool $renderUnused = true): string + { + $this->view->assign('hideRestrictedColumns', (bool)(BackendUtility::getPagesTSconfig($this->backendLayout->getDrawingConfiguration()->getPageId())['mod.']['web_layout.']['hideRestrictedCols'] ?? false)); + if (!$this->backendLayout->getDrawingConfiguration()->getLanguageMode()) { + $this->view->assign('grid', $this->backendLayout->getGrid()); + } + $this->view->assign('newContentTitle', $this->getLanguageService()->getLL('newContentElement')); + $this->view->assign('newContentTitleShort', $this->getLanguageService()->getLL('content')); + + $rendered = $this->view->render('PageLayout'); + if ($renderUnused) { + $unusedBackendLayout = clone $this->backendLayout; + $unusedBackendLayout->getDrawingConfiguration()->setLanguageColumnsPointer($this->backendLayout->getDrawingConfiguration()->getLanguageColumnsPointer()); + $unusedRecords = $this->backendLayout->getContentFetcher()->getUnusedRecords(); + + if (!empty($unusedRecords)) { + $unusedElementsMessage = GeneralUtility::makeInstance( + FlashMessage::class, + $this->getLanguageService()->getLL('staleUnusedElementsWarning'), + $this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'), + FlashMessage::WARNING + ); + $service = GeneralUtility::makeInstance(FlashMessageService::class); + $queue = $service->getMessageQueueByIdentifier(); + $queue->addMessage($unusedElementsMessage); + + $unusedGrid = GeneralUtility::makeInstance(Grid::class, $unusedBackendLayout); + $unusedRow = GeneralUtility::makeInstance(GridRow::class, $unusedBackendLayout); + $unusedColumn = GeneralUtility::makeInstance(GridColumn::class, $unusedBackendLayout, ['colPos' => 99, 'name' => 'unused'], $unusedRecords); + + $unusedGrid->addRow($unusedRow); + $unusedRow->addColumn($unusedColumn); + + $this->view->assign('unusedGrid', $unusedGrid); + $rendered .= $this->view->render('UnusedRecords'); + } + } + return $rendered; + } + + /** + * Initializes the clipboard for generating paste links + * + * @see \TYPO3\CMS\Backend\Controller\ContextMenuController::clipboardAction() + * @see \TYPO3\CMS\Filelist\Controller\FileListController::indexAction() + */ + protected function initializeClipboard(): void + { + $this->clipboard = GeneralUtility::makeInstance(Clipboard::class); + $this->clipboard->initializeClipboard(); + $this->clipboard->lockToNormal(); + $this->clipboard->cleanCurrent(); + $this->clipboard->endClipboard(); + + $elFromTable = $this->clipboard->elFromTable('tt_content'); + if (!empty($elFromTable) && $this->backendLayout->getDrawingConfiguration()->isPageEditable()) { + $pasteItem = (int)substr(key($elFromTable), 11); + $pasteRecord = BackendUtility::getRecord('tt_content', (int)$pasteItem); + $pasteTitle = (string)($pasteRecord['header'] ?: $pasteItem); + $copyMode = $this->clipboard->clipData['normal']['mode'] ? '-' . $this->clipboard->clipData['normal']['mode'] : ''; + $addExtOnReadyCode = ' + top.pasteIntoLinkTemplate = ' + . $this->drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-into', 'pasteIntoColumn') + . '; + top.pasteAfterLinkTemplate = ' + . $this->drawPasteIcon($pasteItem, $pasteTitle, $copyMode, 't3js-paste-after', 'pasteAfterRecord') + . ';'; + } else { + $addExtOnReadyCode = ' + top.pasteIntoLinkTemplate = \'\'; + top.pasteAfterLinkTemplate = \'\';'; + } + GeneralUtility::makeInstance(PageRenderer::class)->addJsInlineCode('pasteLinkTemplates', $addExtOnReadyCode); + } + + /** + * Draw a paste icon either for pasting into a column or for pasting after a record + * + * @param int $pasteItem ID of the item in the clipboard + * @param string $pasteTitle Title for the JS modal + * @param string $copyMode copy or cut + * @param string $cssClass CSS class to determine if pasting is done into column or after record + * @param string $title title attribute of the generated link + * + * @return string Generated HTML code with link and icon + */ + private function drawPasteIcon(int $pasteItem, string $pasteTitle, string $copyMode, string $cssClass, string $title): string + { + $pasteIcon = json_encode( + ' <a data-content="' . htmlspecialchars((string)$pasteItem) . '"' + . ' data-title="' . htmlspecialchars($pasteTitle) . '"' + . ' data-severity="warning"' + . ' class="t3js-paste t3js-paste' . htmlspecialchars($copyMode) . ' ' . htmlspecialchars($cssClass) . ' btn btn-default btn-sm"' + . ' title="' . htmlspecialchars($this->getLanguageService()->getLL($title)) . '">' + . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)->render() + . '</a>' + ); + return $pasteIcon; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Classes/View/Drawing/DrawingConfiguration.php b/typo3/sysext/backend/Classes/View/Drawing/DrawingConfiguration.php new file mode 100644 index 0000000000000000000000000000000000000000..896a0d40be15b7b0e6ee1b5bef12ac85e5f9348d --- /dev/null +++ b/typo3/sysext/backend/Classes/View/Drawing/DrawingConfiguration.php @@ -0,0 +1,342 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Backend\View\Drawing; + +/* + * 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 TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction; +use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Site\Entity\NullSite; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Type\Bitmask\Permission; +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. + */ +class DrawingConfiguration +{ + /** + * @var bool + */ + protected $defaultLanguageBinding = true; + + /** + * @var bool + */ + protected $languageMode = false; + + /** + * @var array + */ + protected $languageColumns = []; + + /** + * @var int + */ + protected $languageColumnsPointer = 0; + + /** + * @var bool + */ + protected $showHidden = true; + + /** + * @var array + */ + protected $activeColumns = [1, 0, 2, 3]; + + /** + * @var array + */ + protected $contentTypeLabels = []; + + /** + * @var array + */ + protected $itemLabels = []; + + /** + * @var int + */ + protected $pageId = 0; + + /** + * @var array + */ + protected $pageRecord = []; + + /** + * @var SiteLanguage[] + */ + protected $siteLanguages = []; + + /** + * @var bool + */ + protected $showNewContentWizard = true; + + public function getDefaultLanguageBinding(): bool + { + return $this->defaultLanguageBinding; + } + + public function setDefaultLanguageBinding(bool $defaultLanguageBinding): void + { + $this->defaultLanguageBinding = $defaultLanguageBinding; + } + + public function getLanguageMode(): bool + { + return $this->languageMode; + } + + public function setLanguageMode(bool $languageMode): void + { + $this->languageMode = $languageMode; + } + + public function getLanguageColumns(): array + { + if ($this->languageColumnsPointer) { + return [0 => 0, $this->languageColumnsPointer => $this->languageColumnsPointer]; + } + return $this->languageColumns; + } + + public function setLanguageColumns(array $languageColumns): void + { + $this->languageColumns = $languageColumns; + } + + public function getLanguageColumnsPointer(): int + { + return $this->languageColumnsPointer; + } + + public function setLanguageColumnsPointer(int $languageColumnsPointer): void + { + $this->languageColumnsPointer = $languageColumnsPointer; + } + + 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 setActiveColumns(array $activeColumns): void + { + $this->activeColumns = $activeColumns; + } + + public function getContentTypeLabels(): array + { + if (empty($this->contentTypeLabels)) { + foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $val) { + $this->contentTypeLabels[$val[1]] = $this->getLanguageService()->sL($val[0]); + } + } + return $this->contentTypeLabels; + } + + public function getItemLabels(): array + { + if (empty($this->itemLabels)) { + foreach ($GLOBALS['TCA']['tt_content']['columns'] as $name => $val) { + $this->itemLabels[$name] = $this->getLanguageService()->sL($val['label']); + } + } + return $this->itemLabels; + } + + public function getPageId(): int + { + return $this->pageId; + } + + public function setPageId(int $pageId): void + { + $this->pageId = $pageId; + $this->pageRecord = BackendUtility::getRecordWSOL('pages', $pageId); + } + + public function getPageRecord(): array + { + return $this->pageRecord; + } + + /** + * @return SiteLanguage[] + */ + public function getSiteLanguages(): array + { + if (empty($this->setSiteLanguages)) { + try { + $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($this->pageId); + } catch (SiteNotFoundException $e) { + $site = new NullSite(); + } + $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUser(), false, $this->pageId); + } + return $this->siteLanguages; + } + + public function getSiteLanguage(int $languageUid): ?SiteLanguage + { + return $this->getSiteLanguages()[$languageUid] ?? null; + } + + public function getShowNewContentWizard(): bool + { + return $this->showNewContentWizard; + } + + public function setShowNewContentWizard(bool $showNewContentWizard): void + { + $this->showNewContentWizard = $showNewContentWizard; + } + + public function getLocalizedPageTitle(): string + { + if ($this->getLanguageColumnsPointer()) { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); + $localizedPage = $queryBuilder + ->select('*') + ->from('pages') + ->where( + $queryBuilder->expr()->eq( + $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], + $queryBuilder->createNamedParameter($this->getPageId(), \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + $GLOBALS['TCA']['pages']['ctrl']['languageField'], + $queryBuilder->createNamedParameter($this->getLanguageColumnsPointer(), \PDO::PARAM_INT) + ) + ) + ->setMaxResults(1) + ->execute() + ->fetch(); + BackendUtility::workspaceOL('pages', $localizedPage); + return $localizedPage['title']; + } + return $this->getPageRecord()['title']; + } + + public function isPageEditable(): bool + { + if ($this->getBackendUser()->isAdmin()) { + return true; + } + $pageRecord = $this->getPageRecord(); + return !$pageRecord['editlock'] && $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_EDIT); + } + + public function getNewLanguageOptions(): array + { + if (!$this->getBackendUser()->check('tables_modify', 'pages')) { + return ''; + } + $id = $this->getPageId(); + + // First, select all languages that are available for the current user + $availableTranslations = []; + foreach ($this->getSiteLanguages() as $language) { + if ($language->getLanguageId() === 0) { + continue; + } + $availableTranslations[$language->getLanguageId()] = $language->getTitle(); + } + + // Then, subtract the languages which are already on the page: + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); + $queryBuilder->getRestrictions()->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) + ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class)); + $queryBuilder->select('uid', $GLOBALS['TCA']['pages']['ctrl']['languageField']) + ->from('pages') + ->where( + $queryBuilder->expr()->eq( + $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], + $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT) + ) + ); + $statement = $queryBuilder->execute(); + while ($row = $statement->fetch()) { + unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]); + } + // If any languages are left, make selector: + $options = []; + if (!empty($availableTranslations)) { + $options[] = $this->getLanguageService()->getLL('new_language'); + foreach ($availableTranslations as $languageUid => $languageTitle) { + // Build localize command URL to DataHandler (tce_db) + // which redirects to FormEngine (record_edit) + // which, when finished editing should return back to the current page (returnUrl) + $parameters = [ + 'justLocalized' => 'pages:' . $id . ':' . $languageUid, + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ]; + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', $parameters); + $targetUrl = BackendUtility::getLinkToDataHandlerAction( + '&cmd[pages][' . $id . '][localize]=' . $languageUid, + $redirectUrl + ); + + $options[$targetUrl] = $languageTitle; + } + } + return $options; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php index 355bf54ab03f1c512b278fdf93f86d7de889b911..aca6a02b3e34b0c975c2209179b902e888f26876 100644 --- a/typo3/sysext/backend/Classes/View/PageLayoutView.php +++ b/typo3/sysext/backend/Classes/View/PageLayoutView.php @@ -52,6 +52,7 @@ use TYPO3\CMS\Fluid\View\StandaloneView; /** * Child class for the Web > Page module * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API. + * @deprecated Will be removed in TYPO3 11 */ class PageLayoutView implements LoggerAwareInterface { @@ -200,10 +201,6 @@ class PageLayoutView implements LoggerAwareInterface $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); $this->localizationController = GeneralUtility::makeInstance(LocalizationController::class); - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); - $pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf'); - $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip'); - $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Localization'); $this->eventDispatcher = $eventDispatcher; } diff --git a/typo3/sysext/backend/Classes/ViewHelpers/LanguageColumnViewHelper.php b/typo3/sysext/backend/Classes/ViewHelpers/LanguageColumnViewHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..17dd371fd43cd5815f8023609b7afa2026001cba --- /dev/null +++ b/typo3/sysext/backend/Classes/ViewHelpers/LanguageColumnViewHelper.php @@ -0,0 +1,34 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Backend\ViewHelpers; + +/* + * 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 TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumn; +use TYPO3\CMS\Backend\View\BackendLayout\Grid\LanguageColumn; +use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; + +class LanguageColumnViewHelper extends AbstractViewHelper +{ + public function initializeArguments() + { + $this->registerArgument('languageColumn', LanguageColumn::class, 'Language column object which is context for column', true); + $this->registerArgument('columnNumber', 'int', 'Number (colPos) of column within LanguageColumn to be returned', true); + } + + public function render(): GridColumn + { + return $this->arguments['languageColumn']->getGrid()->getColumns()[$this->arguments['columnNumber']]; + } +} diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid.html new file mode 100644 index 0000000000000000000000000000000000000000..f28b0846ef023d792289d7472ee647570cc65485 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid.html @@ -0,0 +1,11 @@ +<div class="t3-grid-container"> + <table border="0" cellspacing="0" cellpadding="0" width="100%" class="t3-page-columns t3-grid-table t3js-page-columns"> + <f:for each="{grid.rows}" as="row"> + <tr> + <f:for each="{row.columns}" as="column"> + <f:render partial="PageLayout/Grid/Column" arguments="{_all}" /> + </f:for> + </tr> + </f:for> + </table> +</div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid/Column.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid/Column.html new file mode 100644 index 0000000000000000000000000000000000000000..f2b104b19ae7a43c9f58c01a550d16e45e69a1d3 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Grid/Column.html @@ -0,0 +1,65 @@ +<td valign="top" colspan="{column.colSpan}" rowspan="{column.rowSpan}" + data-colpos="{column.columnNumber}" data-language-uid="{column.backendLayout.drawingConfiguration.languageColumnsPointer}" + class="t3js-page-lang-column-{column.backendLayout.drawingConfiguration.languageColumnsPointer} t3js-page-column t3-grid-cell t3-page-column t3-page-column-{column.columnNumber} + {f:if(condition: column.active, else: 't3-grid-cell-unassigned')} + t3-gridCell-width{column.colSpan} + t3-gridCell-height{column.rowSpan}"> + <div class="t3-page-column-header"> + <f:if condition="{column.active}"> + <f:then> + <div class="t3-page-column-header-icons"> + <f:if condition="{column.editUrl}"> + <a href="{column.editUrl}" title="{column.editLinkTitle}"><core:icon identifier="actions-document-open" /></a> + </f:if> + </div> + {column.title} + </f:then> + <f:else if="{column.unassigned}"> + {column.title} (<f:format.raw>{column.titleUnassigned}</f:format.raw>) + </f:else> + <f:else if="!{hideRestrictedColumns}"> + <f:format.raw>{column.titleInaccessible}</f:format.raw> + </f:else> + <f:else> + <f:format.raw>{column.titleInaccessible}</f:format.raw> + </f:else> + </f:if> + </div> + <f:if condition="{column.contentEditable} && {grid.allowNewContent}"> + <div class="t3-page-ce t3js-page-ce" data-page="{column.backendLayout.drawingConfiguration.pageId}" id="{column.uniqueId}"> + <div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-{column.columnNumber}-page-{column.backendLayout.drawingConfiguration.pageId}-{column.uniqueId}"> + <a href="{column.newContentUrl}" title="{newContentTitle}" data-title="{newContentTitle}" + class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard"> + <core:icon identifier="actions-add" /> + {newContentTitleShort} + </a> + </div> + <div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div> + </div> + </f:if> + <f:if condition="{column.unassigned}"> + <div class="t3-page-ce"> + <div class="t3-page-ce-header">Empty Colpos</div> + <div class="t3-page-ce-body"> + <div class="t3-page-ce-body-inner"> + <div class="row"> + <div class="col-xs-12"> + This column has no "colPos". This is only for display Purposes. + </div> + </div> + </div> + </div> + </div> + </f:if> + <f:if condition="{column.items}"> + <div data-colpos="{column.columnNumber}" data-language-uid="{column.backendLayout.drawingConfiguration.languageColumnsPointer}" + class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-{column.backendLayout.drawingConfiguration.languageColumnsPointer} t3-page-ce-wrapper + {f:if(condition: column.items, else: 't3-page-ce-empty')}"> + <f:for each="{column.items}" as="item"> + <f:if condition="{item.deletePlaceholder} == 0"> + <f:render partial="PageLayout/Record" arguments="{item: item, grid: grid}" /> + </f:if> + </f:for> + </div> + </f:if> +</td> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html new file mode 100644 index 0000000000000000000000000000000000000000..d7cbb286e89074670dd3f0d7198044a4264313bf --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/LanguageColumns.html @@ -0,0 +1,69 @@ +<f:if condition="{backendLayout.drawingConfiguration.newLanguageOptions}"> + <div class="form-inline form-inline-spaced"> + <div class="form-group"> + <select class="form-control input-sm" name="createNewLanguage" onchange="window.location.href=this.options[this.selectedIndex].value">' + <f:for each="{backendLayout.drawingConfiguration.newLanguageOptions}" as="languageName" key="url"> + <option value="{url}">{languageName}</option> + </f:for> + </select> + </div> + </div> +</f:if> +<div class="t3-grid-container"> + <table cellpadding="0" cellspacing="0" class="t3-page-columns t3-grid-table t3js-page-columns"> + <tr> + <f:for each="{backendLayout.languageColumns}" as="languageColumn"> + <td valign="top" class="t3-page-column t3-page-column-lang-name" data-language-uid="{languageColumn.siteLanguage.languageId}"> + <h2>{languageColumn.siteLanguage.title}</h2> + <f:if condition="{languageColumn.languageMode}"> + <span class="label label-{languageColumn.languageModeLabelClass}">{languageColumn.languageMode}</span> + </f:if> + </td> + </f:for> + </tr> + <tr> + <f:for each="{backendLayout.languageColumns}" as="languageColumn"> + <td class="t3-page-column t3-page-lang-label nowrap"> + <div class="btn-group"> + <f:if condition="{languageColumn.allowViewPage}"> + <a href="#" class="btn btn-default btn-sm" onclick="{languageColumn.viewPageOnClick}" title="{languageColumn.viewPageLinkTitle}"> + <core:icon identifier="actions-view" /> + </a> + </f:if> + <f:if condition="{languageColumn.allowEditPage}"> + <a href="{languageColumn.pageEditUrl}" class="btn btn-default btn-sm" title="{languageColumn.pageEditTitle}"> + <core:icon identifier="actions-open" /> + </a> + </f:if> + <f:if condition="{languageColumn.allowTranslate}"> + <a href="#" class="btn btn-default btn-sm t3js-localize disabled" + title="{languageColumn.translatePageTitle}" + data-page="{languageColumn.localizedPageRecord.title}" + data-has-elements="{languageColumn.translationData.hasTranslations as integer}" + data-allow-copy="{languageColumn.allowTranslateCopy as integer}" + data-allow-translate="{languageColumn.allowTranslate as integer}" + data-table="tt_content" + data-page-id="{backendLayout.drawingConfiguration.pageId}" + data-language-id="{languageColumn.siteLanguage.languageId}" + data-language-name="{languageColumn.siteLanguage.title}"> + <core:icon identifier="actions-localize" /> + {languageColumn.translatePageTitle} + </a> + </f:if> + </div> + {languageColumn.pageIcon -> f:format.raw()} + {languageColumn.localizedPageRecord.title} + </td> + </f:for> + </tr> + <f:for each="{backendLayout.drawingConfiguration.activeColumns}" as="columnNumber"> + <tr> + <f:for each="{backendLayout.languageColumns}" as="languageColumn"> + <f:variable name="grid" value="{languageColumn.grid}" /> + <f:variable name="column" value="{be:languageColumn(languageColumn: languageColumn, columnNumber: columnNumber)}" /> + <f:render partial="PageLayout/Grid/Column" arguments="{_all}" /> + </f:for> + </tr> + </f:for> + </table> +</div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record.html new file mode 100644 index 0000000000000000000000000000000000000000..3f13d95723b9ac8663150b8ab01ddd9c66a3f9ed --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record.html @@ -0,0 +1,27 @@ +{f:if(condition: '{item.disabled} && {item.backendLayout.drawingConfiguration.showHidden} == 0', then: 'display: none;') -> f:variable(name: 'style')} +<div class="t3-page-ce {item.wrapperClassName} t3js-page-ce t3js-page-ce-sortable" id="element-tt_content-{item.record.uid}" data-table="tt_content" data-uid="{item.record.uid}" style="{style}"> + <div class="t3-page-ce-dragitem" id="{item.uniqueId}"> + <f:render partial="PageLayout/Record/{item.record.CType}/Header" optional="1"> + <f:render partial="PageLayout/Record/Header" arguments="{item: item}" /> + </f:render> + <div class="t3-page-ce-body"> + <div class="t3-page-ce-body-inner"> + <div class="{f:if(condition: item.versioned, then: 'ver-element')}"> + <f:render partial="PageLayout/Record/{item.record.CType}/Preview" arguments="{_all}" optional="1"> + {item.preview -> f:format.raw()} + </f:render> + </div> + </div> + <f:render partial="PageLayout/Record/{item.record.CType}/Footer" optional="1"> + <f:render partial="PageLayout/Record/Footer" arguments="{item: item}" /> + </f:render> + </div> + </div> + <f:if condition="{item.column.contentEditable} && {grid.allowNewContent}"> + <a href="{item.newContentAfterUrl}" title="{item.newContentAfterLinkTitle}" data-title="{item.newContentAfterLinkTitle}" class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard"> + <core:icon identifier="actions-add" /> + {item.newContentAfterTitle} + </a> + </f:if> + <div class="t3-page-ce-dropzone-available t3js-page-ce-dropzone-available"></div> +</div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html new file mode 100644 index 0000000000000000000000000000000000000000..3f7d3c38841f3221e950791c5f37e604b111fbc9 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Footer.html @@ -0,0 +1,7 @@ +<div class="t3-page-ce-footer"> + <div class="t3-page-ce-info"> + <f:for each="{item.footerInfo}" as="infoLine" iteration="iteration"> + {infoLine}<f:if condition="!{iteration.isLast}"><br /></f:if> + </f:for> + </div> +</div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Header.html b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Header.html new file mode 100644 index 0000000000000000000000000000000000000000..2cf6917020e772eba93b98aa92a095dac1811e76 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Partials/PageLayout/Record/Header.html @@ -0,0 +1,31 @@ +<div class="t3-page-ce-header {f:if(condition: item.dragAndDropAllowed, then: 't3-page-ce-header-draggable t3js-page-ce-draghandle')}"> + <div class="t3-page-ce-header-icons-left"> + {item.icons -> f:format.raw()} + </div> + <div class="t3-page-ce-header-icons-right"> + <f:if condition="{item.editable}"> + <div class="btn-toolbar"> + <div class="btn-group btn-group-sm"> + <a href="{item.editUrl}" class="btn btn-default"> + <core:icon identifier="actions-open" /> + </a> + <f:if condition="{item.visibilityToggling}"> + <a class="btn btn-default" href="{item.visibilityToggleUrl}" title="{item.visibilityToggleTitle}"> + <core:icon identifier="actions-edit-{item.visibilityToggleIconName}" /> + </a> + </f:if> + <f:if condition="{item.delible}"> + <a class="btn btn-default t3js-modal-trigger" href="{item.deleteUrl}" + data-severity="warning" + data-title="{item.deleteConfirmText}" + data-content="{item.deleteConfirmText}" + data-button-close-text="{item.deleteCancelText}" + title="{item.deleteTitle}"> + <core:icon identifier="actions-edit-delete" size="small" /> + </a> + </f:if> + </div> + </div> + </f:if> + </div> +</div> diff --git a/typo3/sysext/backend/Resources/Private/Templates/PageLayout/PageLayout.html b/typo3/sysext/backend/Resources/Private/Templates/PageLayout/PageLayout.html new file mode 100644 index 0000000000000000000000000000000000000000..ac32e59a40a2b7b25fe7bdf2e89890b984fe00e5 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/PageLayout/PageLayout.html @@ -0,0 +1,18 @@ +{namespace be=TYPO3\CMS\Backend\ViewHelpers} + +<f:be.pageRenderer includeRequireJsModules="{ + 0: 'TYPO3/CMS/Backend/Tooltip', + 1: 'TYPO3/CMS/Backend/Localization', + 2: 'TYPO3/CMS/Backend/LayoutModule/DragDrop', + 3: 'TYPO3/CMS/Backend/Modal', + 4: 'TYPO3/CMS/Backend/LayoutModule/Paste' +}" /> + +<f:if condition="{backendLayout.drawingConfiguration.languageMode}"> + <f:then> + <f:render partial="PageLayout/LanguageColumns" arguments="{_all}" /> + </f:then> + <f:else> + <f:render partial="PageLayout/Grid" arguments="{_all}" /> + </f:else> +</f:if> diff --git a/typo3/sysext/backend/Resources/Private/Templates/PageLayout/UnusedRecords.html b/typo3/sysext/backend/Resources/Private/Templates/PageLayout/UnusedRecords.html new file mode 100644 index 0000000000000000000000000000000000000000..62c91436d71fb156078364d9cef1fd067bc0f33a --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/PageLayout/UnusedRecords.html @@ -0,0 +1 @@ +<f:render partial="PageLayout/Grid" arguments="{grid: unusedGrid, backendLayout: backendLayout}" /> diff --git a/typo3/sysext/backend/Tests/Unit/View/BackendLayout/BackendLayoutTest.php b/typo3/sysext/backend/Tests/Unit/View/BackendLayout/BackendLayoutTest.php index cbdd3d14b91ecd2208d94d399f31cbb24ae68aa0..2cab9d5ec1cd98072050c01705d0121084bf11e1 100644 --- a/typo3/sysext/backend/Tests/Unit/View/BackendLayout/BackendLayoutTest.php +++ b/typo3/sysext/backend/Tests/Unit/View/BackendLayout/BackendLayoutTest.php @@ -14,6 +14,7 @@ namespace TYPO3\CMS\Backend\Tests\Unit\View\BackendLayout; * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout; use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -22,6 +23,8 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase; */ class BackendLayoutTest extends UnitTestCase { + protected $resetSingletonInstances = true; + /** * @test */ @@ -43,7 +46,7 @@ class BackendLayoutTest extends UnitTestCase $identifier = StringUtility::getUniqueId('identifier'); $title = StringUtility::getUniqueId('title'); $configuration = StringUtility::getUniqueId('configuration'); - $backendLayout = new \TYPO3\CMS\Backend\View\BackendLayout\BackendLayout($identifier, $title, $configuration); + $backendLayout = $this->getMockBuilder(BackendLayout::class)->onlyMethods(['parseConfigurationStringAndSetConfigurationArray'])->setConstructorArgs([$identifier, $title, $configuration])->getMock(); self::assertEquals($identifier, $backendLayout->getIdentifier()); self::assertEquals($title, $backendLayout->getTitle()); diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index 5bc9ffd4bdafdd0ae0063b34b0fd498edad54beb..7f91e1bccf183a3f3363b0728ac411349ca57228 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -72,6 +72,7 @@ return [ 'fileCreateMask' => '0664', 'folderCreateMask' => '2775', 'features' => [ + 'fluidBasedPageModule' => true, 'form.legacyUploadMimeTypes' => true, 'redirects.hitCount' => false, 'unifiedPageTranslationHandling' => false, @@ -1074,6 +1075,7 @@ return [ ], 'BE' => [ // Backend Configuration. + 'fluidPageModule' => true, 'languageDebug' => false, 'fileadminDir' => 'fileadmin/', 'lockRootPath' => '', diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml index 8b07f1efcad26339516130424e42ee51472b25de..98b779e7090f26ebafe18b11e38ce60df7ef0a69 100644 --- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml +++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml @@ -205,6 +205,9 @@ SYS: type: container description: 'New features of TYPO3 that are activated on new installations but upgrading installations can still use the old behaviour' items: + fluidBasedPageModule: + type: bool + description: 'Use the rewritten page layout backend module which is based on Fluid. Can be toggled off to use the legacy PageLayoutView on installations which require the hooks etc. which are associated with PageLayoutView' form.legacyUploadMimeTypes: type: bool description: 'If on, some mime types are predefined for the "FileUpload" and "ImageUpload" elements of the "form" extension which always allows file uploads of these types, no matter the specific form element definition.' diff --git a/typo3/sysext/core/Configuration/FactoryConfiguration.php b/typo3/sysext/core/Configuration/FactoryConfiguration.php index 5ed31d31445ffd6a9cd58b33978b7e296b8cdcf4..6d3e79376e9033140229da9cd08d4b21c0c15f77 100644 --- a/typo3/sysext/core/Configuration/FactoryConfiguration.php +++ b/typo3/sysext/core/Configuration/FactoryConfiguration.php @@ -20,6 +20,7 @@ return [ 'SYS' => [ 'sitename' => 'New TYPO3 site', 'features' => [ + 'fluidBasedPageModule' => true, 'unifiedPageTranslationHandling' => true, 'rearrangedRedirectMiddlewares' => true, 'felogin.extbase' => true, diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst new file mode 100644 index 0000000000000000000000000000000000000000..140c4d24f473d70c4c1db75fcde5bf601c3de7c2 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90348-PageLayoutViewClassInternalIsDeprecated.rst @@ -0,0 +1,32 @@ +.. include:: ../../Includes.txt + +=================================================================== +Deprecation: #90348 - PageLayoutView class (internal) is deprecated +=================================================================== + +See :issue:`90348` + +Description +=========== + +The :php:`PageLayoutView` class, which is considered internal API, has been deprecated in favor of the new Fluid-based alternative which renders the "page" BE module. + + +Impact +====== + +Implementations which depend on :php:`PageLayoutView` should prepare to use the alternative implementation (by overlaying and overriding Fluid templates of EXT:backend). + + +Affected Installations +====================== + +Any site which uses PSR-14 events or backend content rendering hooks associated with :php:`PageLayoutView` such as :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter']`. + + +Migration +========= + +Fluid templates can be extended or replaced to render custom header, footer or preview of a given :php:`CType`, see feature description for feature 90348. + +.. index:: Backend, Fluid, NotScanned, ext:backend diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90348-NewFluid-basedReplacementForPageLayoutView.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90348-NewFluid-basedReplacementForPageLayoutView.rst new file mode 100644 index 0000000000000000000000000000000000000000..a756f18c7049f293f7e7d6ecf2969e419ffeb284 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90348-NewFluid-basedReplacementForPageLayoutView.rst @@ -0,0 +1,83 @@ +.. include:: ../../Includes.txt + +================================================================ +Feature: #90348 - New Fluid-based replacement for PageLayoutView +================================================================ + +See :issue:`90348` + +Description +=========== + +A completely rewritten replacement for PageLayoutView has been added. This replacement allows third parties to override and extend any part of the "page" module's output by overriding Fluid templates. + +Although it is visually identical to the old :php:`PageLayoutView`'s output, the new alternative has a number of benefits: + +* The grid defined in a BackendLayout is now represented as objects which are assigned to Fluid templates and can be iterated to render rows, columns and records. +* Custom BackendLayout implementations can now manipulate every part of the configuration that determines how the page module is rendered - or completely replace the logic that draws the "columns" and "languages" views of the page BE module. +* Custom BackendLayout implementations can also provide custom classes for LanguageColumn, Grid, GridRow, GridColumn and GridColumnItem instances that are assigned to and used by Fluid templates to render the page layout. +* Headers, footers and previews for content types can be created in Fluid in a way that groups each of these component templates by the content type (CType) value of content records. +* Any part of the page layout can now be rendered elsewhere by creating instances of any of the "grid" objects and assigning them to Fluid templates. +* The "grid" structure of BackendLayouts can be manipulated as objects, adding and removing rows and columns on-the-fly. + +The new Fluid-based implementation is enabled by the global setting :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['fluidBasedPageModule']` which can be changed from the install tool or from extensions. The setting is enabled by default, meaning that the Fluid-based implementation is used as default method in this and future TYPO3 versions. +The feature flag can be managed either by setting it through code (for example, in :php:`ext_localconf.php` of an extension) or you can set it through the "Settings" admin module's' "Feature Toggles" view. + +New Fluid templates: + +* `EXT:backend/Resources/Private/Templates/PageLayout/PageLayout.html` +* `EXT:backend/Resources/Private/Templates/PageLayout/UnusedRecords.html` +* `EXT:backend/Resources/Private/Partials/PageLayout/Grid.html` +* `EXT:backend/Resources/Private/Partials/PageLayout/Grid/Column.html` +* `EXT:backend/Resources/Private/Partials/PageLayout/Record.html` +* `EXT:backend/Resources/Private/Partials/PageLayout/Record/Header.html` +* `EXT:backend/Resources/Private/Partials/PageLayout/Record/Footer.html` + +These Fluid templates can be overridden or extended by TS: + +* :typoscript:`module.tx_backend.view.templateRootPaths.100 = EXT:myext/Resources/Private/Templates/` +* :typoscript:`module.tx_backend.view.partialRootPaths.100 = EXT:myext/Resources/Private/Partials/` + +Depending on which type or types of templates you wish to override. + +In addition, custom header/footer/preview templates can be added by extending the :typoscript:`partialRootPaths` and placing for example a template file in: + +* `EXT:myext/Resources/Private/Partials/PageLayout/Record/my_contenttype/Header` +* `EXT:myext/Resources/Private/Partials/PageLayout/Record/my_contenttype/Footer` +* `EXT:myext/Resources/Private/Partials/PageLayout/Record/my_contenttype/Preview` + +If no such templates exist the default partials (listed above) are used. Note that the folder name `my_contenttype` should use the CType value associated with the content type for which you wish to provide a custom header, footer or preview template. + +Within these last three types of templates the following variables are available: + +* :html:`{item}` which represents a single record. +* :html:`{backendLayout}` which represents the :php:`BackendLayout` instance that defined the grid which was rendered. +* :html:`{grid}` which represents the :php:`Grid` instance that was produced by the :php:`BackendLayout` (also accessible through :html:`{backendLayout.grid}`, provided as extracted variable for easier and more performance-efficient access) + +Properties on :html:`{item}` include: + +* :html:`{item.record}` (the database row of the content element) +* :html:`{item.column}` (the :php:`GridColumn` instance within which the item resides) +* :html:`{item.delible}` +* :html:`{item.translations}` (bool, whether or not the item is translated) +* :html:`{item.dragAndDropAllowed}` (bool, whether or not the item can be dragged and dropped) +* :html:`{item.footerInfo}` (array) + +Properties on :html:`{backendLayout}` include: + +* :html:`{backendLayout.configurationArray}` (array, the low level definition of rows/columns within the :php:`BackendLayout` - array form of the pageTSconfig that defines the grid) +* :html:`{backendLayout.iconPath}` +* :html:`{backendLayout.description}` +* :html:`{backendLayout.identifier}` +* :html:`{backendLayout.title}` +* :html:`{backendLayout.drawingConfiguration}` (the instance of :php:`DrawingConfiguration` which holds properties like active language, site languages and TCA labels for content types and content record fields) +* :html:`{backendLayout.grid}` (the instance of the :php:`Grid` that represents the backend layout rows/columns as PHP objects) + + +Impact +====== + +* A new setting :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['fluidPageModule']` is introduced, enabled by default, which allows switching to the legacy :php:`PageLayoutView`. +* By default, a new set of objects and extended methods on :php:`BackendLayout` now provide a completely Fluid-based implementation of the "page" BE module. + +.. index:: Backend, Fluid, ext:backend