From 9202450c128b79cc802cc76273dba31f5bbac319 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Mon, 19 Sep 2022 16:21:28 +0200 Subject: [PATCH] [!!!][FEATURE] Add PSR-14 Events for customized Page Module rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three legacy hooks * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] are replaced by new PSR-14 Events: * TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent * TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent * TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent Additionally, the hooks * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'] * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] are removed as the same functionality can be achieved with the existing PreviewRenderer functionality since TYPO3 v10. The previous "main class" PageLayoutView is now removed (was marked as internal) along with the interfaces for the removed hooks * TYPO3\CMS\Backend\View\PageLayoutViewDrawFooterHookInterface * TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface Resolves: #98375 Releases: main Change-Id: Iac4a76dce934de31c9749076d8054ae83ac45edb Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75778 Tested-by: core-ci <typo3@b13.com> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Tested-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> --- Build/phpstan/phpstan-baseline.neon | 5 - .../StandardContentPreviewRenderer.php | 38 +- .../View/BackendLayout/ContentFetcher.php | 50 +- .../BackendLayout/Grid/GridColumnItem.php | 23 +- .../Event/IsContentUsedOnPageLayoutEvent.php | 58 + .../ModifyDatabaseQueryForContentEvent.php | 53 + .../PageContentPreviewRenderingEvent.php | 66 + .../backend/Classes/View/PageLayoutView.php | 1938 ----------------- .../PageLayoutViewDrawFooterHookInterface.php | 32 - .../PageLayoutViewDrawItemHookInterface.php | 34 - .../LanguageSelectorScenarioDefault.csv | 3 - ...anguageSelectorScenarioTranslationDone.csv | 4 - .../Functional/View/PageLayoutViewTest.php | 132 -- ...reaking-98375-RemovedHooksInPageModule.rst | 60 + ...Feature-98375-PSR-14EventsInPageModule.rst | 40 + .../Php/ArrayDimensionMatcher.php | 30 + .../ExtensionScanner/Php/ClassNameMatcher.php | 18 + 17 files changed, 348 insertions(+), 2236 deletions(-) create mode 100644 typo3/sysext/backend/Classes/View/Event/IsContentUsedOnPageLayoutEvent.php create mode 100644 typo3/sysext/backend/Classes/View/Event/ModifyDatabaseQueryForContentEvent.php create mode 100644 typo3/sysext/backend/Classes/View/Event/PageContentPreviewRenderingEvent.php delete mode 100644 typo3/sysext/backend/Classes/View/PageLayoutView.php delete mode 100644 typo3/sysext/backend/Classes/View/PageLayoutViewDrawFooterHookInterface.php delete mode 100644 typo3/sysext/backend/Classes/View/PageLayoutViewDrawItemHookInterface.php delete mode 100644 typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioDefault.csv delete mode 100644 typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioTranslationDone.csv delete mode 100644 typo3/sysext/backend/Tests/Functional/View/PageLayoutViewTest.php create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98375-RemovedHooksInPageModule.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-98375-PSR-14EventsInPageModule.rst diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index e355f93d3eea..4919376d7ff8 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -165,11 +165,6 @@ parameters: count: 1 path: ../../typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php - - - message: "#^Variable \\$localizationButtons in empty\\(\\) always exists and is not falsy\\.$#" - count: 1 - path: ../../typo3/sysext/backend/Classes/View/PageLayoutView.php - - message: "#^Call to an undefined method TYPO3Fluid\\\\Fluid\\\\Core\\\\Rendering\\\\RenderingContextInterface\\:\\:getRequest\\(\\)\\.$#" count: 1 diff --git a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php index 7200cfa45580..2a7f8dd5ea77 100644 --- a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php +++ b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php @@ -22,8 +22,6 @@ use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; -use TYPO3\CMS\Backend\View\PageLayoutView; -use TYPO3\CMS\Backend\View\PageLayoutViewDrawFooterHookInterface; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Imaging\Icon; @@ -145,22 +143,7 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger } break; case 'list': - $hookOut = ''; - if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'])) { - $pageLayoutView = PageLayoutView::createFromPageLayoutContext($item->getContext()); - $_params = ['pObj' => &$pageLayoutView, 'row' => $record]; - foreach ( - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info'][$record['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, $pageLayoutView); - } - } - - if ((string)$hookOut !== '') { - $out .= $hookOut; - } elseif (!empty($record['list_type'])) { + if (!empty($record['list_type'])) { $label = BackendUtility::getLabelFromItemListMerged($record['pid'], 'tt_content', 'list_type', $record['list_type']); if (!empty($label)) { $out .= $this->linkEditContent('<strong>' . htmlspecialchars($languageService->sL($label)) . '</strong>', $record); @@ -209,7 +192,6 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger */ public function renderPageModulePreviewFooter(GridColumnItem $item): string { - $content = ''; $info = []; $record = $item->getRecord(); $this->getProcessedValue($item, 'starttime,endtime,fe_group,space_before_class,space_after_class', $info); @@ -218,24 +200,10 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger $info[] = htmlspecialchars($record[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]); } - // Call drawFooter hooks - if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'])) { - $pageLayoutView = PageLayoutView::createFromPageLayoutContext($item->getContext()); - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) { - $hookObject = GeneralUtility::makeInstance($className); - if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) { - throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1582574541); - } - $hookObject->preProcess($pageLayoutView, $info, $record); - } - $item->setRecord($record); - } - if (!empty($info)) { - $content = implode('<br>', $info); + return implode('<br>', $info); } - - return $content; + return ''; } public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php b/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php index 66d0664ad355..0a23e37a972a 100644 --- a/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php +++ b/typo3/sysext/backend/Classes/View/BackendLayout/ContentFetcher.php @@ -17,9 +17,11 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\View\BackendLayout; +use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent; +use TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent; use TYPO3\CMS\Backend\View\PageLayoutContext; -use TYPO3\CMS\Backend\View\PageLayoutView; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -57,10 +59,13 @@ class ContentFetcher */ protected $fetchedContentRecords = []; + protected EventDispatcherInterface $eventDispatcher; + public function __construct(PageLayoutContext $pageLayoutContext) { $this->context = $pageLayoutContext; $this->fetchedContentRecords = $this->getRuntimeCache()->get('ContentFetcher_fetchedContentRecords') ?: []; + $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); } /** @@ -114,28 +119,22 @@ class ContentFetcher } /** - * A hook allows to decide whether a custom type has children which were rendered or should not be rendered. + * Allows to decide via an Event whether a custom type has children which were rendered or should not be rendered. * * @return iterable */ public function getUnusedRecords(): iterable { $unrendered = []; - $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] ?? []; - $pageLayoutView = PageLayoutView::createFromPageLayoutContext($this->context); - - $knownColumnPositionNumbers = $this->context->getBackendLayout()->getColumnPositionNumbers(); $rememberer = GeneralUtility::makeInstance(RecordRememberer::class); $languageId = $this->context->getDrawingConfiguration()->getSelectedLanguageId(); foreach ($this->getContentRecordsPerColumn(null, $languageId) as $contentRecordsInColumn) { foreach ($contentRecordsInColumn as $contentRecord) { $used = $rememberer->isRemembered((int)$contentRecord['uid']); // A hook mentioned that this record is used somewhere, so this is in fact "rendered" already - foreach ($hookArray as $hookFunction) { - $_params = ['columns' => $knownColumnPositionNumbers, 'record' => $contentRecord, 'used' => $used]; - $used = GeneralUtility::callUserFunction($hookFunction, $_params, $pageLayoutView); - } - if (!$used) { + $event = new IsContentUsedOnPageLayoutEvent($contentRecord, $used, $this->context); + $event = $this->eventDispatcher->dispatch($event); + if (!$event->isRecordUsed()) { $unrendered[] = $contentRecord; } } @@ -211,7 +210,6 @@ class ContentFetcher protected function getQueryBuilder(): QueryBuilder { - $fields = ['*']; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tt_content'); $queryBuilder->getRestrictions() @@ -219,7 +217,7 @@ class ContentFetcher ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$GLOBALS['BE_USER']->workspace)); $queryBuilder - ->select(...$fields) + ->select('*') ->from('tt_content'); $queryBuilder->andWhere( @@ -229,34 +227,14 @@ class ContentFetcher ) ); - $additionalConstraints = []; - $parameters = [ - 'table' => 'tt_content', - 'fields' => $fields, - 'groupBy' => null, - 'orderBy' => null, - ]; - $sortBy = (string)($GLOBALS['TCA']['tt_content']['ctrl']['sortby'] ?: $GLOBALS['TCA']['tt_content']['ctrl']['default_sortby']); foreach (QueryHelper::parseOrderBy($sortBy) as $orderBy) { $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]); } - 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->context->getPageId(), - $additionalConstraints, - $fields, - $queryBuilder - ); - } - } - - return $queryBuilder; + $event = new ModifyDatabaseQueryForContentEvent($queryBuilder, 'tt_content', $this->context->getPageId()); + $event = $this->eventDispatcher->dispatch($event); + return $event->getQueryBuilder(); } protected function getResult($result): array diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php index 9d9c4cc44266..3c7b0483cd2c 100644 --- a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php +++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumnItem.php @@ -17,12 +17,12 @@ declare(strict_types=1); namespace TYPO3\CMS\Backend\View\BackendLayout\Grid; +use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent; use TYPO3\CMS\Backend\View\PageLayoutContext; -use TYPO3\CMS\Backend\View\PageLayoutView; -use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface; use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; @@ -81,22 +81,11 @@ class GridColumnItem extends AbstractGridObject ); $previewHeader = $previewRenderer->renderPageModulePreviewHeader($this); - $drawItem = true; - $previewContent = ''; - // Hook: Render an own preview of a record - if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'])) { - $pageLayoutView = PageLayoutView::createFromPageLayoutContext($this->getContext()); - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) { - $hookObject = GeneralUtility::makeInstance($className); - if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) { - throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1582574553); - } - $hookObject->preProcess($pageLayoutView, $drawItem, $previewHeader, $previewContent, $record); - } - $this->setRecord($record); - } + $event = new PageContentPreviewRenderingEvent('tt_content', $this->record, $this->context); + GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event); - if ($drawItem) { + $previewContent = $event->getPreviewContent(); + if ($previewContent === null) { $previewContent = $previewRenderer->renderPageModulePreviewContent($this); } diff --git a/typo3/sysext/backend/Classes/View/Event/IsContentUsedOnPageLayoutEvent.php b/typo3/sysext/backend/Classes/View/Event/IsContentUsedOnPageLayoutEvent.php new file mode 100644 index 000000000000..b5d7a0a2f409 --- /dev/null +++ b/typo3/sysext/backend/Classes/View/Event/IsContentUsedOnPageLayoutEvent.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\View\Event; + +use TYPO3\CMS\Backend\View\PageLayoutContext; + +/** + * Use this Event to identify whether a content element is used. + */ +final class IsContentUsedOnPageLayoutEvent +{ + public function __construct( + private readonly array $record, + private bool $used, + private PageLayoutContext $context + ) { + } + + public function getRecord(): array + { + return $this->record; + } + + public function isRecordUsed(): bool + { + return $this->used; + } + + public function setUsed(bool $isUsed): void + { + $this->used = $isUsed; + } + + public function getKnownColumnPositionNumbers(): array + { + return $this->context->getBackendLayout()->getColumnPositionNumbers(); + } + + public function getPageLayoutContext(): PageLayoutContext + { + return $this->context; + } +} diff --git a/typo3/sysext/backend/Classes/View/Event/ModifyDatabaseQueryForContentEvent.php b/typo3/sysext/backend/Classes/View/Event/ModifyDatabaseQueryForContentEvent.php new file mode 100644 index 000000000000..5b361adf835e --- /dev/null +++ b/typo3/sysext/backend/Classes/View/Event/ModifyDatabaseQueryForContentEvent.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\View\Event; + +use TYPO3\CMS\Core\Database\Query\QueryBuilder; + +/** + * Use this Event to alter the database query when loading content for a page. + */ +final class ModifyDatabaseQueryForContentEvent +{ + public function __construct( + private QueryBuilder $queryBuilder, + private string $table, + private int $pageId, + ) { + } + + public function getQueryBuilder(): QueryBuilder + { + return $this->queryBuilder; + } + + public function setQueryBuilder(QueryBuilder $queryBuilder): void + { + $this->queryBuilder = $queryBuilder; + } + + public function getTable(): string + { + return $this->table; + } + + public function getPageId(): int + { + return $this->pageId; + } +} diff --git a/typo3/sysext/backend/Classes/View/Event/PageContentPreviewRenderingEvent.php b/typo3/sysext/backend/Classes/View/Event/PageContentPreviewRenderingEvent.php new file mode 100644 index 000000000000..2588f266219e --- /dev/null +++ b/typo3/sysext/backend/Classes/View/Event/PageContentPreviewRenderingEvent.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\View\Event; + +use Psr\EventDispatcher\StoppableEventInterface; +use TYPO3\CMS\Backend\View\PageLayoutContext; + +/** + * Use this Event to have a custom preview for a content type in the Page Module + */ +final class PageContentPreviewRenderingEvent implements StoppableEventInterface +{ + private ?string $content = null; + + public function __construct( + private string $table, + private array $record, + private PageLayoutContext $context + ) { + } + + public function getTable(): string + { + return $this->table; + } + + public function getRecord(): array + { + return $this->record; + } + + public function getPageLayoutContext(): PageLayoutContext + { + return $this->context; + } + + public function getPreviewContent(): ?string + { + return $this->content; + } + + public function setPreviewContent(string $content): void + { + $this->content = $content; + } + + public function isPropagationStopped(): bool + { + return $this->content !== null; + } +} diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php deleted file mode 100644 index 8d56228f94b8..000000000000 --- a/typo3/sysext/backend/Classes/View/PageLayoutView.php +++ /dev/null @@ -1,1938 +0,0 @@ -<?php - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Backend\View; - -use Doctrine\DBAL\Result; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; -use TYPO3\CMS\Backend\Controller\Page\LocalizationController; -use TYPO3\CMS\Backend\Routing\PreviewUriBuilder; -use TYPO3\CMS\Backend\Routing\UriBuilder; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Core\Environment; -use TYPO3\CMS\Core\Database\Connection; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Database\Query\QueryHelper; -use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; -use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; -use TYPO3\CMS\Core\Database\ReferenceIndex; -use TYPO3\CMS\Core\Exception\SiteNotFoundException; -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\JavaScriptModuleInstruction; -use TYPO3\CMS\Core\Page\PageRenderer; -use TYPO3\CMS\Core\Service\FlexFormService; -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\Type\ContextualFeedbackSeverity; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\StringUtility; -use TYPO3\CMS\Core\Versioning\VersionState; -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 -{ - use LoggerAwareTrait; - - /** - * If TRUE, elements will have edit icons (probably this is whether the user has permission to edit the page content). Set externally. - * - * @var bool - */ - public $doEdit = true; - - /** - * If set TRUE, the language mode of tt_content elements will be rendered with hard binding between - * default language content elements and their translations! - * - * @var bool - */ - public $defLangBinding = false; - - /** - * External, static: Configuration of tt_content element display: - * - * @var array - */ - public $tt_contentConfig = [ - 'languageCols' => 0, - 'languageMode' => 0, - 'languageColsPointer' => 0, - // Displays hidden records as well - 'showHidden' => 1, - // Which language - 'sys_language_uid' => 0, - 'cols' => '1,0,2,3', - // Which columns can be accessed by current BE user - 'activeCols' => '1,0,2,3', - ]; - - /** - * Used to move content up / down - * @var array - */ - public $tt_contentData = [ - 'prev' => [], - 'next' => [], - ]; - - /** - * Used to store labels for CTypes for tt_content elements - * - * @var array - */ - public $CType_labels = []; - - /** - * Used to store labels for the various fields in tt_content elements - * - * @var array - */ - public $itemLabels = []; - - /** - * Page id - * - * @var int - */ - public $id; - - /** - * Loaded with page record with version overlay if any. - * - * @var string[] - */ - public $pageRecord = []; - - /** - * Contains site languages for this page ID - * - * @var SiteLanguage[] - */ - protected $siteLanguages = []; - - /** - * Current ids page record - * - * @var array - */ - protected $pageinfo; - - /** - * Caches the amount of content elements as a matrix - * - * @var array - * @internal - */ - protected $contentElementCache = []; - - /** - * @var IconFactory - */ - protected $iconFactory; - - /** - * Stores whether a certain language has translations in it - * - * @var array - */ - protected $languageHasTranslationsCache = []; - - /** - * @var LocalizationController - */ - protected $localizationController; - - /** - * Cache the number of references to a record - * - * @var array - */ - protected $referenceCount = []; - - /** - * @var UriBuilder - */ - protected $uriBuilder; - - public function __construct() - { - $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); - $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $this->localizationController = GeneralUtility::makeInstance(LocalizationController::class); - } - - /** - * @param PageLayoutContext $context - * @return PageLayoutView - * @internal - */ - public static function createFromPageLayoutContext(PageLayoutContext $context): PageLayoutView - { - $drawingConfiguration = $context->getDrawingConfiguration(); - $languageId = $drawingConfiguration->getSelectedLanguageId(); - $pageLayoutView = GeneralUtility::makeInstance(self::class); - $pageLayoutView->id = $context->getPageId(); - $pageLayoutView->pageinfo = BackendUtility::readPageAccess($pageLayoutView->id, '') ?: []; - $pageLayoutView->pageRecord = $context->getPageRecord(); - $pageLayoutView->defLangBinding = $drawingConfiguration->getDefaultLanguageBinding(); - $pageLayoutView->tt_contentConfig['cols'] = implode(',', $drawingConfiguration->getActiveColumns()); - $pageLayoutView->tt_contentConfig['activeCols'] = implode(',', $drawingConfiguration->getActiveColumns()); - $pageLayoutView->tt_contentConfig['showHidden'] = $drawingConfiguration->getShowHidden(); - $pageLayoutView->tt_contentConfig['sys_language_uid'] = $languageId; - if ($drawingConfiguration->getLanguageMode()) { - $pageLayoutView->tt_contentConfig['languageMode'] = 1; - $pageLayoutView->tt_contentConfig['languageCols'] = $drawingConfiguration->getLanguageColumns(); - $pageLayoutView->tt_contentConfig['languageColsPointer'] = $languageId; - } - $pageLayoutView->doEdit = $pageLayoutView->isContentEditable($languageId); - $pageLayoutView->CType_labels = $context->getContentTypeLabels(); - $pageLayoutView->itemLabels = $context->getItemLabels(); - return $pageLayoutView; - } - - protected function initialize() - { - $this->resolveSiteLanguages($this->id); - $this->pageRecord = BackendUtility::getRecordWSOL('pages', $this->id); - $this->pageinfo = BackendUtility::readPageAccess($this->id, '') ?: []; - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); - - $pageActionsInstruction = JavaScriptModuleInstruction::create('@typo3/backend/page-actions.js'); - if ($this->isPageEditable()) { - $languageOverlayId = 0; - $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, (int)$this->tt_contentConfig['sys_language_uid']); - if (is_array($pageLocalizationRecord)) { - $pageLocalizationRecord = reset($pageLocalizationRecord); - } - if (!empty($pageLocalizationRecord['uid'])) { - $languageOverlayId = $pageLocalizationRecord['uid']; - } - $pageActionsInstruction - ->invoke('setPageId', (int)$this->id) - ->invoke('setLanguageOverlayId', $languageOverlayId); - } - $pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($pageActionsInstruction); - // Get labels for CTypes and tt_content element fields in general: - $this->CType_labels = []; - foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $val) { - $this->CType_labels[$val[1]] = $this->getLanguageService()->sL($val[0]); - } - - $this->itemLabels = []; - foreach ($GLOBALS['TCA']['tt_content']['columns'] as $name => $val) { - $this->itemLabels[$name] = $this->getLanguageService()->sL($val['label']); - } - } - - /** - * Build a list of language IDs that should be rendered in this view - * @return int[] - */ - protected function getSelectedLanguages(): array - { - $langList = (string)($this->tt_contentConfig['sys_language_uid'] ?? ''); - if ($this->tt_contentConfig['languageMode']) { - if ($this->tt_contentConfig['languageColsPointer']) { - $langList = '0,' . $this->tt_contentConfig['languageColsPointer']; - } else { - $langList = implode(',', array_keys($this->tt_contentConfig['languageCols'])); - } - } - return GeneralUtility::intExplode(',', $langList); - } - - /** - * Renders Content Elements from the tt_content table from page id - * - * @param int $id Page id - * @return string HTML for the listing - */ - public function getTable_tt_content($id) - { - $this->id = (int)$id; - $this->initialize(); - $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable('tt_content') - ->getExpressionBuilder(); - - $languageColumn = []; - $out = ''; - $tcaItems = GeneralUtility::makeInstance(BackendLayoutView::class)->getColPosListItemsParsed($this->id); - $languageIds = $this->getSelectedLanguages(); - $defaultLanguageElementsByColumn = []; - $defLangBinding = []; - // For each languages... - // If not languageMode, then we'll only be through this once. - foreach ($languageIds as $lP) { - if (!isset($this->contentElementCache[$lP])) { - $this->contentElementCache[$lP] = []; - } - - if (count($languageIds) === 1 || $lP === 0) { - $showLanguage = $expressionBuilder->in('sys_language_uid', [$lP, -1]); - } else { - $showLanguage = $expressionBuilder->eq('sys_language_uid', $lP); - } - $content = []; - $head = []; - - $backendLayout = $this->getBackendLayoutView()->getSelectedBackendLayout($this->id); - $columns = $backendLayout['__colPosList']; - // Select content records per column - $contentRecordsPerColumn = $this->getContentRecordsPerColumn('tt_content', $id, $columns, $showLanguage); - $cList = array_keys($contentRecordsPerColumn); - // For each column, render the content into a variable: - foreach ($cList as $columnId) { - if (!isset($this->contentElementCache[$lP])) { - $this->contentElementCache[$lP] = []; - } - - if (!$lP) { - $defaultLanguageElementsByColumn[$columnId] = []; - } - - // Start wrapping div - $content[$columnId] .= '<div data-colpos="' . $columnId . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper'; - if (empty($contentRecordsPerColumn[$columnId])) { - $content[$columnId] .= ' t3-page-ce-empty'; - } - $content[$columnId] .= '">'; - // Add new content at the top most position - $link = ''; - if ($this->isContentEditable() - && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP)) - ) { - $url = (string)$this->uriBuilder->buildUriFromRoute('new_content_element_wizard', [ - 'id' => $id, - 'sys_language_uid' => $lP, - 'colPos' => $columnId, - 'uid_pid' => $id, - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri(), - ]); - $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement')); - $link = '<a href="' . htmlspecialchars($url) . '" ' - . 'title="' . $title . '"' - . 'data-title="' . $title . '"' - . 'class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard disabled">' - . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() - . ' ' - . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>'; - } - if ($this->getBackendUser()->checkLanguageAccess($lP) && $columnId !== 'unused') { - $content[$columnId] .= ' - <div class="t3-page-ce t3js-page-ce" data-page="' . (int)$id . '" id="' . StringUtility::getUniqueId() . '"> - <div class="t3-page-ce-actions t3js-page-new-ce" id="colpos-' . $columnId . '-page-' . $id . '-' . StringUtility::getUniqueId() . '">' - . $link - . '</div> - <div class="t3-page-ce-dropzone t3js-page-ce-dropzone-available"></div> - </div> - '; - } - $editUidList = ''; - if (!isset($contentRecordsPerColumn[$columnId]) || !is_array($contentRecordsPerColumn[$columnId])) { - $message = GeneralUtility::makeInstance( - FlashMessage::class, - $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:error.invalidBackendLayout'), - '', - ContextualFeedbackSeverity::WARNING - ); - $service = GeneralUtility::makeInstance(FlashMessageService::class); - $queue = $service->getMessageQueueByIdentifier(); - $queue->addMessage($message); - } else { - $rowArr = $contentRecordsPerColumn[$columnId]; - $this->generateTtContentDataArray($rowArr); - - foreach ((array)$rowArr as $rKey => $row) { - $this->contentElementCache[$lP][$columnId][$row['uid']] = $row; - if ($this->tt_contentConfig['languageMode']) { - $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId]; - } - if (is_array($row) && !VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { - $singleElementHTML = '<div class="t3-page-ce-element t3-page-ce-dragitem t3js-page-ce-dragitem" id="' . StringUtility::getUniqueId() . '">'; - if (!$lP && ($this->defLangBinding || $row['sys_language_uid'] != -1)) { - $defaultLanguageElementsByColumn[$columnId][] = ($row['_ORIG_uid'] ?? $row['uid']); - } - $editUidList .= $row['uid'] . ','; - $disableMoveAndNewButtons = $this->defLangBinding && $lP > 0 && $this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP); - $singleElementHTML .= $this->tt_content_drawHeader( - $row, - 0, - $disableMoveAndNewButtons, - true, - $this->hasContentModificationAndAccessPermissions() - ); - $singleElementHTML .= '<div class="t3-page-ce-body">' . $this->tt_content_drawItem($row) . '</div>' . $this->tt_content_drawFooter($row); - $isDisabled = $this->isDisabled('tt_content', $row); - $statusHidden = $isDisabled ? ' t3-page-ce-hidden t3js-hidden-record' : ''; - $displayNone = !$this->tt_contentConfig['showHidden'] && $isDisabled ? ' style="display: none;"' : ''; - $highlightHeader = ''; - if ($this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid']) && (int)$row['l18n_parent'] === 0) { - $highlightHeader = ' t3-page-ce-danger'; - } elseif ($columnId === 'unused') { - $highlightHeader = ' t3-page-ce-warning'; - } - $singleElementHTML = '<div class="t3-page-ce' . $highlightHeader . ' t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-' - . $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '" data-language-uid="' - . $row['sys_language_uid'] . '"' . $displayNone . '>' . $singleElementHTML . '</div>'; - - $singleElementHTML .= '<div class="t3-page-ce" data-colpos="' . $columnId . '">'; - $singleElementHTML .= '<div class="t3-page-ce-actions t3js-page-new-ce" id="colpos-' . $columnId . '-page-' . $id . - '-' . StringUtility::getUniqueId() . '">'; - // Add icon "new content element below" - if (!$disableMoveAndNewButtons - && $this->isContentEditable($lP) - && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP)) - && $columnId !== 'unused' - ) { - // New content element - $url = (string)$this->uriBuilder->buildUriFromRoute('new_content_element_wizard', [ - 'id' => $row['pid'], - 'sys_language_uid' => $row['sys_language_uid'], - 'colPos' => $row['colPos'], - 'uid_pid' => -$row['uid'], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri(), - ]); - $title = htmlspecialchars($this->getLanguageService()->getLL('newContentElement')); - $singleElementHTML .= '<a href="' . htmlspecialchars($url) . '" ' - . 'title="' . $title . '"' - . 'data-title="' . $title . '"' - . 'class="btn btn-default btn-sm t3js-toggle-new-content-element-wizard disabled">' - . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() - . ' ' - . htmlspecialchars($this->getLanguageService()->getLL('content')) . '</a>'; - } - $singleElementHTML .= '</div></div><div class="t3-page-ce-dropzone t3js-page-ce-dropzone-available"></div></div>'; - if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) { - $defLangBinding[$columnId][$lP][$row[$lP ? 'l18n_parent' : 'uid'] ?: $row['uid']] = $singleElementHTML; - } else { - $content[$columnId] .= $singleElementHTML; - } - } else { - unset($rowArr[$rKey]); - } - } - $content[$columnId] .= '</div>'; - if ($columnId === 'unused') { - if (empty($unusedElementsMessage)) { - $unusedElementsMessage = GeneralUtility::makeInstance( - FlashMessage::class, - $this->getLanguageService()->getLL('staleUnusedElementsWarning'), - $this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'), - ContextualFeedbackSeverity::WARNING - ); - $service = GeneralUtility::makeInstance(FlashMessageService::class); - $queue = $service->getMessageQueueByIdentifier(); - $queue->addMessage($unusedElementsMessage); - } - $colTitle = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:unusedColPos'); - $editParam = ''; - } else { - $colTitle = ''; - foreach ($tcaItems as $item) { - if ($item[1] == $columnId) { - $colTitle = $this->getLanguageService()->sL($item[0]); - } - } - if (empty($colTitle)) { - $colTitle = BackendUtility::getProcessedValue('tt_content', 'colPos', (string)$columnId) ?? ''; - } - $editParam = $this->doEdit && !empty($rowArr) - ? '&edit[tt_content][' . $editUidList . ']=edit&recTitle=' . rawurlencode(BackendUtility::getRecordTitle('pages', $this->pageRecord, true)) - : ''; - } - $head[$columnId] .= $this->tt_content_drawColHeader($colTitle, $editParam); - } - } - // For each column, fit the rendered content into a table cell: - $out = ''; - if ($this->tt_contentConfig['languageMode']) { - // in language mode process the content elements, but only fill $languageColumn. output will be generated later - $sortedLanguageColumn = []; - foreach ($cList as $columnId) { - if (GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnId) || $columnId === 'unused') { - $languageColumn[$columnId][$lP] = $head[$columnId] . $content[$columnId]; - - // We sort $languageColumn again according to $cList as it may contain data already from above. - $sortedLanguageColumn[$columnId] = $languageColumn[$columnId]; - } - } - if (!empty($languageColumn['unused'])) { - $sortedLanguageColumn['unused'] = $languageColumn['unused']; - } - $languageColumn = $sortedLanguageColumn; - } else { - // GRID VIEW: - $grid = '<div class="t3-grid-container"><table class="t3-page-columns t3-grid-table t3js-page-columns">'; - // Add colgroups - $colCount = (int)$backendLayout['__config']['backend_layout.']['colCount']; - $rowCount = (int)$backendLayout['__config']['backend_layout.']['rowCount']; - $colSpan = 0; - $rowSpan = 0; - $grid .= '<colgroup>'; - for ($i = 0; $i < $colCount; $i++) { - $grid .= '<col />'; - } - $grid .= '</colgroup>'; - - // Check how to handle restricted columns - $hideRestrictedCols = (bool)(BackendUtility::getPagesTSconfig($id)['mod.']['web_layout.']['hideRestrictedCols'] ?? false); - - // Cycle through rows - for ($row = 1; $row <= $rowCount; $row++) { - $rowConfig = $backendLayout['__config']['backend_layout.']['rows.'][$row . '.']; - if (!isset($rowConfig)) { - continue; - } - $grid .= '<tr>'; - for ($col = 1; $col <= $colCount; $col++) { - $columnConfig = $rowConfig['columns.'][$col . '.']; - if (!isset($columnConfig)) { - continue; - } - // Which tt_content colPos should be displayed inside this cell - $columnKey = (int)$columnConfig['colPos']; - // Render the grid cell - $colSpan = (int)$columnConfig['colspan']; - $rowSpan = (int)$columnConfig['rowspan']; - $grid .= '<td valign="top"' . - ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') . - ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') . - ' data-colpos="' . (int)$columnConfig['colPos'] . '" data-language-uid="' . $lP . '" class="t3js-page-lang-column-' . $lP . ' t3js-page-column t3-grid-cell t3-page-column t3-page-column-' . $columnKey . - ((!isset($columnConfig['colPos']) || $columnConfig['colPos'] === '') ? ' t3-grid-cell-unassigned' : '') . - ((isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && !$head[$columnKey]) || !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ? ($hideRestrictedCols ? ' t3-grid-cell-restricted t3-grid-cell-hidden' : ' t3-grid-cell-restricted') : '') . - ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . - ($rowSpan > 0 ? ' t3-gridCell-height' . $rowSpan : '') . '">'; - - // Draw the pre-generated header with edit and new buttons if a colPos is assigned. - // If not, a new header without any buttons will be generated. - if (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && $head[$columnKey] - && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) - ) { - $grid .= $head[$columnKey]; - $grid .= $content[$columnKey]; - } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' - && GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) - ) { - if (!$hideRestrictedCols) { - $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('noAccess')); - } - } elseif (isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' - && !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) - ) { - if (!$hideRestrictedCols) { - $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) . - ' (' . $this->getLanguageService()->getLL('noAccess') . ')'); - } - } elseif (isset($columnConfig['name']) && $columnConfig['name'] !== '') { - $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->sL($columnConfig['name']) - . ' (' . $this->getLanguageService()->getLL('notAssigned') . ')'); - } else { - $grid .= $this->tt_content_drawColHeader($this->getLanguageService()->getLL('notAssigned')); - } - - $grid .= '</td>'; - } - $grid .= '</tr>'; - } - if (!empty($content['unused'])) { - $grid .= '<tr>'; - // Which tt_content colPos should be displayed inside this cell - $columnKey = 'unused'; - // Render the grid cell - $colSpan = (int)$backendLayout['__config']['backend_layout.']['colCount']; - $grid .= '<td valign="top"' . - ($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') . - ($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') . - ' data-colpos="unused" data-language-uid="' . $lP . '" class="t3js-page-lang-column-' . $lP . ' t3js-page-column t3-grid-cell t3-page-column t3-page-column-' . $columnKey . - ($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') . '">'; - - // Draw the pre-generated header with edit and new buttons if a colPos is assigned. - // If not, a new header without any buttons will be generated. - $grid .= $head[$columnKey] . $content[$columnKey]; - $grid .= '</td></tr>'; - } - $out .= $grid . '</table></div>'; - } - } - // If language mode, then make another presentation: - // Notice that THIS presentation will override the value of $out! - // But it needs the code above to execute since $languageColumn is filled with content we need! - if ($this->tt_contentConfig['languageMode']) { - return $this->generateLanguageView($languageIds, $defaultLanguageElementsByColumn, $languageColumn, $defLangBinding); - } - return $out; - } - - /** - * Shows the content elements of the selected languages in each column. - * @param array $languageIds languages to render - * @param array $defaultLanguageElementsByColumn - * @param array $languageColumn - * @param array $defLangBinding - * @return string the compiled content - */ - protected function generateLanguageView( - array $languageIds, - array $defaultLanguageElementsByColumn, - array $languageColumn, - array $defLangBinding - ): string { - // Get language selector: - $languageSelector = $this->languageSelector($this->id); - // Reset out - we will make new content here: - $out = ''; - // Traverse languages found on the page and build up the table displaying them side by side: - $cCont = []; - $sCont = []; - foreach ($languageIds as $languageId) { - $languageMode = ''; - $labelClass = 'info'; - // Header: - $languageId = (int)$languageId; - // Determine language mode - if ($languageId > 0 && isset($this->languageHasTranslationsCache[$languageId]['mode'])) { - switch ($this->languageHasTranslationsCache[$languageId]['mode']) { - case 'mixed': - $languageMode = $this->getLanguageService()->getLL('languageModeMixed'); - $labelClass = 'danger'; - break; - case 'connected': - $languageMode = $this->getLanguageService()->getLL('languageModeConnected'); - break; - case 'free': - $languageMode = $this->getLanguageService()->getLL('languageModeFree'); - break; - default: - // we'll let opcode optimize this intentionally empty case - } - } - $columnAttributes = [ - 'valign' => 'top', - 'class' => 't3-page-column t3-page-column-lang-name', - 'data-language-uid' => (string)$languageId, - 'data-language-title' => $this->siteLanguages[$languageId]->getTitle(), - 'data-flag-identifier' => $this->siteLanguages[$languageId]->getFlagIdentifier(), - ]; - - $cCont[$languageId] = ' - <td ' . GeneralUtility::implodeAttributes($columnAttributes, true) . '> - <h2>' . htmlspecialchars($this->tt_contentConfig['languageCols'][$languageId]) . '</h2> - ' . ($languageMode !== '' ? '<span class="badge badge-' . $labelClass . '">' . $languageMode . '</span>' : '') . ' - </td>'; - - $editLink = ''; - $recordIcon = ''; - $viewLink = ''; - // "View page" icon is added: - if (!VersionState::cast($this->pageinfo['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { - $attributes = PreviewUriBuilder::create($this->id) - ->withRootLine(BackendUtility::BEgetRootLine($this->id)) - ->withLanguage($languageId) - ->serializeDispatcherAttributes(); - $viewLink = '<a href="#" class="btn btn-default btn-sm" ' . $attributes . ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) . '">' . $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render() . '</a>'; - } - // Language overlay page header: - if ($languageId) { - $pageLocalizationRecord = BackendUtility::getRecordLocalization('pages', $this->id, $languageId); - if (is_array($pageLocalizationRecord)) { - $pageLocalizationRecord = reset($pageLocalizationRecord); - } - BackendUtility::workspaceOL('pages', $pageLocalizationRecord); - $recordIcon = BackendUtility::wrapClickMenuOnIcon( - $this->iconFactory->getIconForRecord('pages', $pageLocalizationRecord, Icon::SIZE_SMALL)->render(), - 'pages', - $pageLocalizationRecord['uid'] - ); - $urlParameters = [ - 'edit' => [ - 'pages' => [ - $pageLocalizationRecord['uid'] => 'edit', - ], - ], - // Disallow manual adjustment of the language field for pages - 'overrideVals' => [ - 'pages' => [ - 'sys_language_uid' => $languageId, - ], - ], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri(), - ]; - $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); - if ($this->getBackendUser()->check('tables_modify', 'pages')) { - $editLink = '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"' - . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' - . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'; - } - - $defaultLanguageElements = []; - array_walk($defaultLanguageElementsByColumn, static function (array $columnContent) use (&$defaultLanguageElements) { - $defaultLanguageElements = array_merge($defaultLanguageElements, $columnContent); - }); - - $localizationButtons = []; - $localizationButtons[] = $this->newLanguageButton( - $this->getNonTranslatedTTcontentUids($defaultLanguageElements, $this->id, $languageId), - $languageId - ); - - $languageLabel = - '<div class="btn-group">' - . $viewLink - . $editLink - . (!empty($localizationButtons) ? implode(LF, $localizationButtons) : '') - . '</div>' - . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($pageLocalizationRecord['title'], 20)) - ; - } else { - if ($this->getBackendUser()->checkLanguageAccess(0)) { - $recordIcon = BackendUtility::wrapClickMenuOnIcon( - $this->iconFactory->getIconForRecord('pages', $this->pageRecord, Icon::SIZE_SMALL)->render(), - 'pages', - $this->id - ); - $urlParameters = [ - 'edit' => [ - 'pages' => [ - $this->id => 'edit', - ], - ], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri(), - ]; - $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); - if ($this->getBackendUser()->check('tables_modify', 'pages')) { - $editLink = '<a href="' . htmlspecialchars($url) . '" class="btn btn-default btn-sm"' - . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' - . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'; - } - } - - $languageLabel = - '<div class="btn-group">' - . $viewLink - . $editLink - . '</div>' - . ' ' . $recordIcon . ' ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($this->pageRecord['title'], 20)); - } - $sCont[$languageId] = ' - <td class="t3-page-column t3-page-lang-label nowrap">' . $languageLabel . '</td>'; - } - // Add headers: - $out .= '<tr>' . implode('', $cCont) . '</tr>'; - $out .= '<tr>' . implode('', $sCont) . '</tr>'; - unset($cCont, $sCont); - - // Traverse previously built content for the columns: - foreach ($languageColumn as $cKey => $cCont) { - $out .= '<tr>'; - foreach ($cCont as $languageId => $columnContent) { - $out .= '<td valign="top" data-colpos="' . $cKey . '" class="t3-grid-cell t3-page-column t3js-page-column t3js-page-lang-column t3js-page-lang-column-' . $languageId . '">' . $columnContent . '</td>'; - } - $out .= '</tr>'; - if ($this->defLangBinding && !empty($defLangBinding[$cKey])) { - $maxItemsCount = max(array_map('count', $defLangBinding[$cKey])); - for ($i = 0; $i < $maxItemsCount; $i++) { - $defUid = $defaultLanguageElementsByColumn[$cKey][$i] ?? 0; - $cCont = []; - foreach ($languageIds as $languageId) { - if ($languageId > 0 - && is_array($defLangBinding[$cKey][$languageId]) - && !$this->checkIfTranslationsExistInLanguage($defaultLanguageElementsByColumn[$cKey], $languageId) - && count($defLangBinding[$cKey][$languageId]) > $i - ) { - $slice = array_slice($defLangBinding[$cKey][$languageId], $i, 1); - $element = $slice[0] ?? ''; - } else { - $element = $defLangBinding[$cKey][$languageId][$defUid] ?? ''; - } - $cCont[] = $element; - } - $out .= ' - <tr> - <td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">' . implode('</td> - <td valign="top" class="t3-grid-cell" data-colpos="' . $cKey . '">', $cCont) . '</td> - </tr>'; - } - } - } - // Finally, wrap it all in a table and add the language selector on top of it: - return $languageSelector . ' - <div class="t3-grid-container"> - <table class="t3-page-columns t3-grid-table t3js-page-columns"> - ' . $out . ' - </table> - </div>'; - } - - /** - * Gets content records per column. - * This is required for correct workspace overlays. - * - * @param string $table Name of table storing content records, by default tt_content - * @param int $id Page Id to be used (not used at all, but part of the API, see $this->pidSelect) - * @param array $columns colPos values to be considered to be shown - * @param string $additionalWhereClause Additional where clause for database select - * @return array Associative array for each column (colPos) - */ - protected function getContentRecordsPerColumn($table, $id, array $columns, $additionalWhereClause = '') - { - $contentRecordsPerColumn = array_fill_keys($columns, []); - $columns = array_flip($columns); - $queryBuilder = $this->getQueryBuilder( - $table, - $id, - [ - $additionalWhereClause, - ] - ); - - // Traverse any selected elements and render their display code: - $result = $queryBuilder->executeQuery(); - $results = $this->getResult($result); - $unused = []; - $hookArray = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used'] ?? []; - foreach ($results as $record) { - $used = isset($columns[$record['colPos']]); - foreach ($hookArray as $_funcRef) { - $_params = ['columns' => $columns, 'record' => $record, 'used' => $used]; - $used = GeneralUtility::callUserFunction($_funcRef, $_params, $this); - } - if ($used) { - $columnValue = (string)$record['colPos']; - $contentRecordsPerColumn[$columnValue][] = $record; - } else { - $unused[] = $record; - } - } - if (!empty($unused)) { - $contentRecordsPerColumn['unused'] = $unused; - } - return $contentRecordsPerColumn; - } - - /** - * Draw header for a content element column: - * - * @param string $colName Column name - * @param string $editParams Edit params (Syntax: &edit[...] for FormEngine) - * @return string HTML table - */ - public function tt_content_drawColHeader($colName, $editParams = '') - { - $icons = ''; - // Edit whole of column: - if ($editParams && $this->hasContentModificationAndAccessPermissions() && $this->getBackendUser()->checkLanguageAccess(0)) { - $link = $this->uriBuilder->buildUriFromRoute('record_edit') . $editParams . '&returnUrl=' . rawurlencode($GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri()); - $icons = '<a href="' . htmlspecialchars($link) . '" title="' - . htmlspecialchars($this->getLanguageService()->getLL('editColumn')) . '">' - . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>'; - $icons = '<div class="t3-page-column-header-icons">' . $icons . '</div>'; - } - return '<div class="t3-page-column-header"> - ' . $icons . ' - <div class="t3-page-column-header-label">' . htmlspecialchars($colName) . '</div> - </div>'; - } - - /** - * Draw the footer for a single tt_content element - * - * @param array $row Record array - * @return string HTML of the footer - * @throws \UnexpectedValueException - */ - protected function tt_content_drawFooter(array $row) - { - $content = ''; - // Get processed values: - $info = []; - $this->getProcessedValue('tt_content', 'starttime,endtime,fe_group,space_before_class,space_after_class', $row, $info); - - // Content element annotation - if (!empty($GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']) && !empty($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']])) { - $info[] = htmlspecialchars($row[$GLOBALS['TCA']['tt_content']['ctrl']['descriptionColumn']]); - } - - // Call drawFooter hooks - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter'] ?? [] as $className) { - $hookObject = GeneralUtility::makeInstance($className); - if (!$hookObject instanceof PageLayoutViewDrawFooterHookInterface) { - throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawFooterHookInterface::class, 1404378171); - } - $hookObject->preProcess($this, $info, $row); - } - - // Display info from records fields: - if (!empty($info)) { - $content = '<div class="t3-page-ce-info">' . implode('<br>', $info) . '</div>'; - } - if (!empty($content)) { - $content = '<div class="t3-page-ce-footer">' . $content . '</div>'; - } - return $content; - } - - /** - * Draw the header for a single tt_content element - * - * @param array $row Record array - * @param int $space Amount of pixel space above the header. UNUSED - * @param bool $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown. - * @param bool $langMode If set, we are in language mode and flags will be shown for languages - * @param bool $dragDropEnabled If set the move button must be hidden - * @return string HTML table with the record header. - */ - public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = false, $langMode = false, $dragDropEnabled = false) - { - $backendUser = $this->getBackendUser(); - $out = ''; - // Render control panel for the element - if ($backendUser->recordEditAccessInternals('tt_content', $row) && $this->isContentEditable($row['sys_language_uid'])) { - // Edit content element: - $urlParameters = [ - 'edit' => [ - 'tt_content' => [ - $row['uid'] => 'edit', - ], - ], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() . '#element-tt_content-' . $row['uid'], - ]; - $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-tt_content-' . $row['uid']; - - $out .= '<a class="btn btn-default" href="' . htmlspecialchars($url) - . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) - . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>'; - // Hide element: - $hiddenField = $GLOBALS['TCA']['tt_content']['ctrl']['enablecolumns']['disabled']; - if ($hiddenField && $GLOBALS['TCA']['tt_content']['columns'][$hiddenField] - && (!$GLOBALS['TCA']['tt_content']['columns'][$hiddenField]['exclude'] - || $backendUser->check('non_exclude_fields', 'tt_content:' . $hiddenField)) - ) { - if ($row[$hiddenField]) { - $value = 0; - $label = 'unHide'; - } else { - $value = 1; - $label = 'hide'; - } - $params = '&data[tt_content][' . ($row['_ORIG_uid'] ?: $row['uid']) - . '][' . $hiddenField . ']=' . $value; - $out .= '<a class="btn btn-default" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) - . '#element-tt_content-' . $row['uid'] . '" title="' . htmlspecialchars($this->getLanguageService()->getLL($label)) . '">' - . $this->iconFactory->getIcon('actions-edit-' . strtolower($label), Icon::SIZE_SMALL)->render() . '</a>'; - } - // Delete - $disableDelete = (bool)\trim( - $backendUser->getTSConfig()['options.']['disableDelete.']['tt_content'] - ?? $backendUser->getTSConfig()['options.']['disableDelete'] - ?? '0' - ); - if (!$disableDelete) { - $params = '&cmd[tt_content][' . $row['uid'] . '][delete]=1'; - - $recordInfo = $row['header'] ?? ''; - if ($this->getBackendUser()->shallDisplayDebugInformation()) { - $recordInfo .= ' [' . 'tt_content' . ':' . $row['uid'] . ']'; - } - - $refCountMsg = BackendUtility::referenceCount( - 'tt_content', - $row['uid'], - ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'), - (string)$this->getReferenceCount('tt_content', $row['uid']) - ) . BackendUtility::translationCount( - 'tt_content', - $row['uid'], - ' ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord') - ); - - $out .= '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) . '"' - . ' data-severity="warning"' - . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')) . '"' - . ' data-bs-content="' . htmlspecialchars(sprintf($this->getLanguageService()->getLL('deleteWarning'), trim($recordInfo)) . $refCountMsg) . '" ' - . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) . '"' - . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('deleteItem')) . '">' - . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>'; - if ($this->hasContentModificationAndAccessPermissions()) { - $out = '<div class="btn-group btn-group-sm" role="group">' . $out . '</div>'; - } else { - $out = ''; - } - } - if (!$disableMoveAndNewButtons) { - $moveButtonContent = ''; - $displayMoveButtons = false; - // Move element up: - if ($this->tt_contentData['prev'][$row['uid']]) { - $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']]; - $moveButtonContent .= '<a class="btn btn-default" href="' - . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) - . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveUp')) . '">' - . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</a>'; - if (!$dragDropEnabled) { - $displayMoveButtons = true; - } - } else { - $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'; - } - // Move element down: - if ($this->tt_contentData['next'][$row['uid']]) { - $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']]; - $moveButtonContent .= '<a class="btn btn-default" href="' - . htmlspecialchars(BackendUtility::getLinkToDataHandlerAction($params)) - . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('moveDown')) . '">' - . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</a>'; - if (!$dragDropEnabled) { - $displayMoveButtons = true; - } - } else { - $moveButtonContent .= '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'; - } - if ($displayMoveButtons) { - $out .= '<div class="btn-group btn-group-sm" role="group">' . $moveButtonContent . '</div>'; - } - } - } - $allowDragAndDrop = $this->isDragAndDropAllowed($row); - $additionalIcons = []; - $additionalIcons[] = $this->getIcon('tt_content', $row) . ' '; - if ($langMode && isset($this->siteLanguages[(int)$row['sys_language_uid']])) { - $additionalIcons[] = $this->renderLanguageFlag($this->siteLanguages[(int)$row['sys_language_uid']]); - } - // Get record locking status: - if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) { - $additionalIcons[] = '<a href="#" data-bs-toggle="tooltip" title="' . htmlspecialchars($lockInfo['msg']) . '">' - . $this->iconFactory->getIcon('status-user-backend', Icon::SIZE_SMALL, 'overlay-edit')->render() . '</a>'; - } - // Call stats information hook - $_params = ['tt_content', $row['uid'], &$row]; - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) { - $additionalIcons[] = GeneralUtility::callUserFunction($_funcRef, $_params, $this); - } - - $contentType = $this->CType_labels[$row['CType']]; - if (!isset($contentType)) { - $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']); - } - - // Wrap the whole header - // NOTE: end-tag for <div class="t3-page-ce-body"> is in getTable_tt_content() - return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '"> - <div class="t3-page-ce-header-left">' . implode('', $additionalIcons) . '</div> - <div class="t3-page-ce-header-title">' . $contentType . '</div> - <div class="t3-page-ce-header-right">' . ($out ? '<div class="btn-toolbar">' . $out . '</div>' : '') . '</div> - </div> - <div class="t3-page-ce-body">'; - } - - /** - * Gets the number of records referencing the record with the UID $uid in - * the table $tableName. - * - * @param string $tableName - * @param int $uid - * @return int The number of references to record $uid in table - */ - protected function getReferenceCount(string $tableName, int $uid): int - { - if (!isset($this->referenceCount[$tableName][$uid])) { - $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class); - $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords($tableName, $uid); - $this->referenceCount[$tableName][$uid] = $numberOfReferences; - } - return $this->referenceCount[$tableName][$uid]; - } - - /** - * Determine whether Drag & Drop should be allowed - * - * @param array $row - * @return bool - */ - protected function isDragAndDropAllowed(array $row) - { - if ((int)$row['l18n_parent'] === 0 && - ( - $this->getBackendUser()->isAdmin() - || ((int)$row['editlock'] === 0 && (int)$this->pageinfo['editlock'] === 0) - && $this->hasContentModificationAndAccessPermissions() - && $this->getBackendUser()->checkAuthMode('tt_content', 'CType', $row['CType']) - ) - ) { - return true; - } - return false; - } - - /** - * Draws the preview content for a content element - * - * @param array $row Content element - * @return string HTML - * @throws \UnexpectedValueException - */ - public function tt_content_drawItem($row) - { - $out = ''; - $outHeader = $this->renderContentElementHeader($row); - $drawItem = true; - // Hook: Render an own preview of a record - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem'] ?? [] as $className) { - $hookObject = GeneralUtility::makeInstance($className); - if (!$hookObject instanceof PageLayoutViewDrawItemHookInterface) { - throw new \UnexpectedValueException($className . ' must implement interface ' . PageLayoutViewDrawItemHookInterface::class, 1218547409); - } - $hookObject->preProcess($this, $drawItem, $outHeader, $out, $row); - } - - // If the previous hook did not render something, - // then check if a Fluid-based preview template was defined for this CType - // and render it via Fluid. Possible option: - // mod.web_layout.tt_content.preview.media = EXT:site_mysite/Resources/Private/Templates/Preview/Media.html - if ($drawItem) { - $fluidPreview = $this->renderContentElementPreviewFromFluidTemplate($row); - if ($fluidPreview !== null) { - $out .= $fluidPreview; - $drawItem = false; - } - } - - // Draw preview of the item depending on its CType (if not disabled by previous hook) - if ($drawItem) { - $out .= $this->renderContentElementPreview($row); - } - $out = $outHeader . $out; - - return $out; - } - - public function renderContentElementHeader(array $row): string - { - $outHeader = ''; - // Make header: - 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($this->itemLabels['date'] . ' ' . BackendUtility::date($row['date'])) . '<br />' - : ''; - $outHeader .= '<strong>' . $this->linkEditContent($this->renderText($row['header']), $row) - . $hiddenHeaderNote . '</strong><br />'; - } - return $outHeader; - } - - public function renderContentElementPreviewFromFluidTemplate(array $row): ?string - { - $tsConfig = BackendUtility::getPagesTSconfig($row['pid'])['mod.']['web_layout.']['tt_content.']['preview.'] ?? []; - $fluidTemplateFile = ''; - - if ($row['CType'] === 'list' && !empty($row['list_type']) - && !empty($tsConfig['list.'][$row['list_type']]) - ) { - $fluidTemplateFile = $tsConfig['list.'][$row['list_type']]; - } elseif (!empty($tsConfig[$row['CType']])) { - $fluidTemplateFile = $tsConfig[$row['CType']]; - } - - if ($fluidTemplateFile) { - $fluidTemplateFile = GeneralUtility::getFileAbsFileName($fluidTemplateFile); - if ($fluidTemplateFile) { - try { - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setTemplatePathAndFilename($fluidTemplateFile); - $view->assignMultiple($row); - if (!empty($row['pi_flexform'])) { - $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); - $view->assign('pi_flexform_transformed', $flexFormService->convertFlexFormContentToArray($row['pi_flexform'])); - } - return $view->render(); - } catch (\Exception $e) { - $this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [ - 'uid' => $row['uid'], - 'file' => $fluidTemplateFile, - $e->getMessage(), - ]); - - if ($this->getBackendUser()->shallDisplayDebugInformation()) { - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->assign('error', [ - 'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()), - 'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFile), - ]); - $view->setTemplateSource('<f:be.infobox title="{error.title}" state="2">{error.message}</f:be.infobox>'); - return $view->render(); - } - } - } - } - return null; - } - - /** - * Renders the preview part of a content element - * @param array $row given tt_content database record - * @return string - */ - public function renderContentElementPreview(array $row): string - { - $previewHtml = ''; - switch ($row['CType']) { - case 'header': - if ($row['subheader']) { - $previewHtml = $this->linkEditContent($this->renderText($row['subheader']), $row) . '<br>'; - } - break; - case 'bullets': - case 'table': - if ($row['bodytext']) { - $previewHtml = $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br>'; - } - break; - case 'uploads': - if ($row['media']) { - $previewHtml = $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)); - } - } - $previewHtml = implode('<br>', $shortcutContent) . '<br>'; - } - break; - case 'list': - $hookOut = ''; - $_params = ['pObj' => &$this, 'row' => $row]; - 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 !== '') { - $previewHtml = $hookOut; - } elseif (!empty($row['list_type'])) { - $label = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'list_type', $row['list_type']); - if (!empty($label)) { - $previewHtml = $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']); - $previewHtml = '<span class="badge badge-warning">' . htmlspecialchars($message) . '</span>'; - } - } else { - $previewHtml = '<strong>' . $this->getLanguageService()->getLL('noPluginSelected') . '</strong>'; - } - $previewHtml .= htmlspecialchars($this->getLanguageService()->sL( - BackendUtility::getLabelFromItemlist('tt_content', 'pages', $row['pages']) - )) . '<br />'; - break; - default: - $contentType = $this->CType_labels[$row['CType']]; - if (!isset($contentType)) { - $contentType = BackendUtility::getLabelFromItemListMerged($row['pid'], 'tt_content', 'CType', $row['CType']); - } - - if ($contentType) { - $previewHtml = ''; - if ($row['bodytext']) { - $previewHtml .= $this->linkEditContent($this->renderText($row['bodytext']), $row) . '<br />'; - } - if ($row['image']) { - $previewHtml .= $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'] - ); - $previewHtml = '<span class="badge badge-warning">' . htmlspecialchars($message) . '</span>'; - } - } - return $previewHtml; - } - - /** - * Filters out all tt_content uids which are already translated so only non-translated uids is left. - * Selects across columns, but within in the same PID. Columns are expect to be the same - * for translations and original but this may be a conceptual error (?) - * - * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language - * @param int $id The page UID from which to fetch untranslated records (unused, remains in place for compatibility) - * @param int $lP Sys language UID - * @return array Modified $defLanguageCount - */ - public function getNonTranslatedTTcontentUids($defaultLanguageUids, $id, $lP) - { - if ($lP && !empty($defaultLanguageUids)) { - // Select all translations here: - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('tt_content'); - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, 0)); - $queryBuilder - ->select('*') - ->from('tt_content') - ->where( - $queryBuilder->expr()->eq( - 'sys_language_uid', - $queryBuilder->createNamedParameter($lP, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->in( - 'l18n_parent', - $queryBuilder->createNamedParameter($defaultLanguageUids, Connection::PARAM_INT_ARRAY) - ) - ); - - $result = $queryBuilder->executeQuery(); - - // Flip uids: - $defaultLanguageUids = array_flip($defaultLanguageUids); - // Traverse any selected elements and unset original UID if any: - while ($row = $result->fetchAssociative()) { - BackendUtility::workspaceOL('tt_content', $row); - unset($defaultLanguageUids[$row['l18n_parent']]); - } - // Flip again: - $defaultLanguageUids = array_keys($defaultLanguageUids); - } - return $defaultLanguageUids; - } - - /** - * Creates button which is used to create copies of records.. - * - * @param array $defaultLanguageUids Numeric array with uids of tt_content elements in the default language - * @param int $lP Sys language UID - * @return string "Copy languages" button, if available. - */ - public function newLanguageButton($defaultLanguageUids, $lP) - { - $lP = (int)$lP; - if (!$this->doEdit || !$lP || !$this->hasContentModificationAndAccessPermissions()) { - return ''; - } - $theNewButton = ''; - - $localizationTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['localization.'] ?? []; - $allowCopy = (bool)($localizationTsConfig['enableCopy'] ?? true); - $allowTranslate = (bool)($localizationTsConfig['enableTranslate'] ?? true); - if (!empty($this->languageHasTranslationsCache[$lP])) { - if (isset($this->languageHasTranslationsCache[$lP]['hasStandAloneContent'])) { - $allowTranslate = false; - } - if (isset($this->languageHasTranslationsCache[$lP]['hasTranslations'])) { - $allowCopy = $allowCopy && !$this->languageHasTranslationsCache[$lP]['hasTranslations']; - } - } - - if (isset($this->contentElementCache[$lP]) && is_array($this->contentElementCache[$lP])) { - foreach ($this->contentElementCache[$lP] as $column => $records) { - foreach ($records as $record) { - $key = array_search($record['l10n_source'], $defaultLanguageUids); - if ($key !== false) { - unset($defaultLanguageUids[$key]); - } - } - } - } - - if (!empty($defaultLanguageUids)) { - $theNewButton = - '<a' - . ' href="#"' - . ' class="btn btn-default btn-sm t3js-localize disabled"' - . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) . '"' - . ' data-page="' . htmlspecialchars($this->getLocalizedPageTitle()) . '"' - . ' data-has-elements="' . (int)!empty($this->contentElementCache[$lP]) . '"' - . ' data-allow-copy="' . (int)$allowCopy . '"' - . ' data-allow-translate="' . (int)$allowTranslate . '"' - . ' data-table="tt_content"' - . ' data-page-id="' . (int)GeneralUtility::_GP('id') . '"' - . ' data-language-id="' . $lP . '"' - . ' data-language-name="' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '"' - . '>' - . $this->iconFactory->getIcon('actions-localize', Icon::SIZE_SMALL)->render() - . ' ' . htmlspecialchars($this->getLanguageService()->getLL('newPageContent_translate')) - . '</a>'; - } - - return $theNewButton; - } - - /** - * 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 ($this->doEdit) $str is return with link around. Otherwise just $str. - * @see getTable_tt_content() - */ - public function linkEditContent($str, $row) - { - if ($this->doEdit - && $this->hasContentModificationAndAccessPermissions() - && $this->getBackendUser()->recordEditAccessInternals('tt_content', $row) - ) { - $urlParameters = [ - 'edit' => [ - 'tt_content' => [ - $row['uid'] => 'edit', - ], - ], - 'returnUrl' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri() . '#element-tt_content-' . $row['uid'], - ]; - $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); - return '<a href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('edit')) . '">' . $str . '</a>'; - } - return $str; - } - - /** - * Make selector box for creating new translation in a language - * Displays only languages which are not yet present for the current page and - * that are not disabled with page TS. - * - * @param int $id Page id for which to create a new translation record of pages - * @return string HTML <select> element (if there were items for the box anyways...) - * @see getTable_tt_content() - */ - public function languageSelector($id) - { - if (!$this->getBackendUser()->check('tables_modify', 'pages')) { - return ''; - } - $id = (int)$id; - - // First, select all languages that are available for the current user - $availableTranslations = []; - foreach ($this->siteLanguages 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(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace)); - $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->executeQuery(); - while ($row = $statement->fetchAssociative()) { - unset($availableTranslations[(int)$row[$GLOBALS['TCA']['pages']['ctrl']['languageField']]]); - } - // If any languages are left, make selector: - if (!empty($availableTranslations)) { - $output = '<option value="">' . htmlspecialchars($this->getLanguageService()->getLL('new_language')) . '</option>'; - 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' => $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getRequestUri(), - ]; - $redirectUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $parameters); - $targetUrl = BackendUtility::getLinkToDataHandlerAction( - '&cmd[pages][' . $id . '][localize]=' . $languageUid, - $redirectUrl - ); - - $output .= '<option value="' . htmlspecialchars($targetUrl) . '">' . htmlspecialchars($languageTitle) . '</option>'; - } - - return '<div class="row row-cols-auto align-items-end g-3 mb-3">' - . '<div class="col">' - . '<select class="form-select" name="createNewLanguage" data-global-event="change" data-action-navigate="$value">' - . $output - . '</select></div></div>'; - } - return ''; - } - - /** - * Traverse the result pointer given, adding each record to array and setting some internal values at the same time. - * - * @param Result $result DBAL Result - * @return array The selected rows returned in this array. - */ - public function getResult($result): array - { - $output = []; - // Traverse the result: - while ($row = $result->fetchAssociative()) { - BackendUtility::workspaceOL('tt_content', $row, -99, true); - if ($row) { - // Add the row to the array: - $output[] = $row; - } - } - $this->generateTtContentDataArray($output); - // Return selected records - return $output; - } - - /******************************** - * - * Various helper functions - * - ********************************/ - - /** - * Generates the data for previous and next elements which is needed for movements. - * - * @param array $rowArray - */ - protected function generateTtContentDataArray(array $rowArray) - { - if (empty($this->tt_contentData)) { - $this->tt_contentData = [ - 'next' => [], - 'prev' => [], - ]; - } - foreach ($rowArray as $key => $value) { - // Create information for next and previous content elements - if (isset($rowArray[$key - 1])) { - if (isset($rowArray[$key - 2])) { - $this->tt_contentData['prev'][$value['uid']] = -$rowArray[$key - 2]['uid']; - } else { - $this->tt_contentData['prev'][$value['uid']] = $value['pid']; - } - $this->tt_contentData['next'][$rowArray[$key - 1]['uid']] = -$value['uid']; - } - } - } - - /** - * 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) - { - $input = strip_tags($input); - $input = GeneralUtility::fixed_lgd_cs($input, 1500); - return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false)); - } - - /** - * Creates the icon image tag for record from table and wraps it in a link which will trigger the click menu. - * - * @param string $table Table name - * @param array $row Record array - * @return string HTML for the icon - */ - public function getIcon($table, $row) - { - // Initialization - $toolTip = BackendUtility::getRecordToolTip($row, $table); - $icon = '<span ' . $toolTip . '>' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>'; - // The icon with link - if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) { - $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']); - } - return $icon; - } - - /** - * Creates processed values for all field names in $fieldList based on values from $row array. - * The result is 'returned' through $info which is passed as a reference - * - * @param string $table Table name - * @param string $fieldList Comma separated list of fields. - * @param array $row Record from which to take values for processing. - * @param array $info Array to which the processed values are added. - */ - public function getProcessedValue($table, $fieldList, array $row, array &$info) - { - // Splitting values from $fieldList - $fieldArr = explode(',', $fieldList); - // Traverse fields from $fieldList - foreach ($fieldArr as $field) { - if ($row[$field]) { - $info[] = '<strong>' . htmlspecialchars($this->itemLabels[$field]) . '</strong> ' - . htmlspecialchars(BackendUtility::getProcessedValue($table, $field, $row[$field]) ?? ''); - } - } - } - - /** - * Returns TRUE, if the record given as parameters is NOT visible based on hidden/starttime/endtime (if available) - * - * @param string $table Tablename of table to test - * @param array $row Record row. - * @return bool Returns TRUE, if disabled. - */ - public function isDisabled($table, $row) - { - $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']; - } - - /***************************************** - * - * External renderings - * - *****************************************/ - - /** - * 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. - */ - public function getThumbCodeUnlinked($row, $table, $field) - { - return BackendUtility::thumbCode($row, $table, $field, '', '', null, 0, '', '', false); - } - - /** - * Checks whether translated Content Elements exist in the desired language - * If so, deny creating new ones via the UI - * - * @param array $contentElements - * @param int $language - * @return bool - */ - protected function checkIfTranslationsExistInLanguage(array $contentElements, int $language) - { - // 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 - // We jump out here since we don't need to do the expensive loop operations - $allowInconsistentLanguageHandling = (bool)(BackendUtility::getPagesTSconfig($this->id)['mod.']['web_layout.']['allowInconsistentLanguageHandling'] ?? false); - if ($language === 0 || $allowInconsistentLanguageHandling) { - return false; - } - /** - * Build up caches - */ - if (!isset($this->languageHasTranslationsCache[$language])) { - foreach ($contentElements as $columns) { - foreach ($columns 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 whether we have a mix of both - if (isset($this->languageHasTranslationsCache[$language]['hasStandAloneContent']) - && $this->languageHasTranslationsCache[$language]['hasTranslations'] - ) { - $this->languageHasTranslationsCache[$language]['mode'] = 'mixed'; - $siteLanguage = $this->siteLanguages[$language]; - $message = GeneralUtility::makeInstance( - FlashMessage::class, - $this->getLanguageService()->getLL('staleTranslationWarning'), - sprintf($this->getLanguageService()->getLL('staleTranslationWarningTitle'), $siteLanguage->getTitle()), - ContextualFeedbackSeverity::WARNING - ); - $service = GeneralUtility::makeInstance(FlashMessageService::class); - $queue = $service->getMessageQueueByIdentifier(); - $queue->addMessage($message); - } - } - - return $this->languageHasTranslationsCache[$language]['hasTranslations']; - } - - /** - * @return BackendLayoutView - */ - protected function getBackendLayoutView() - { - return GeneralUtility::makeInstance(BackendLayoutView::class); - } - - protected function getBackendUser(): BackendUserAuthentication - { - return $GLOBALS['BE_USER']; - } - - /** - * Create thumbnail code for record/field - * - * @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. - */ - public function thumbCode($row, $table, $field) - { - return BackendUtility::thumbCode($row, $table, $field); - } - - /** - * Returns a QueryBuilder configured to select $fields from $table where the pid is restricted. - * - * @param string $table Table name - * @param int $pageId Page id Only used to build the search constraints, getPageIdConstraint() used for restrictions - * @param string[] $additionalConstraints Additional part for where clause - * @param string[] $fields Field list to select, * for all - * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder - */ - public function getQueryBuilder( - string $table, - int $pageId, - array $additionalConstraints = [], - array $fields = ['*'] - ): QueryBuilder { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable($table); - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace)); - $queryBuilder - ->select(...$fields) - ->from($table); - - if (!empty($additionalConstraints)) { - $queryBuilder->andWhere(...$additionalConstraints); - } - - $queryBuilder = $this->prepareQueryBuilder($table, $pageId, $fields, $additionalConstraints, $queryBuilder); - - return $queryBuilder; - } - - /** - * Return the modified QueryBuilder object ($queryBuilder) which will be - * used to select the records from a table $table with pid = $this->pidList - * - * @param string $table Table name - * @param int $pageId Page id Only used to build the search constraints, $this->pidList is used for restrictions - * @param string[] $fieldList List of fields to select from the table - * @param string[] $additionalConstraints Additional part for where clause - * @param QueryBuilder $queryBuilder - * @return QueryBuilder - */ - protected function prepareQueryBuilder( - string $table, - int $pageId, - array $fieldList, - array $additionalConstraints, - QueryBuilder $queryBuilder - ): QueryBuilder { - $parameters = [ - 'table' => $table, - 'fields' => $fieldList, - 'groupBy' => null, - 'orderBy' => null, - ]; - - $sortBy = (string)($GLOBALS['TCA'][$table]['ctrl']['sortby'] ?: $GLOBALS['TCA'][$table]['ctrl']['default_sortby']); - foreach (QueryHelper::parseOrderBy($sortBy) as $orderBy) { - $queryBuilder->addOrderBy($orderBy[0], $orderBy[1]); - } - - // Build the query constraints - $queryBuilder->andWhere( - $queryBuilder->expr()->eq( - $table . '.pid', - $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) - ) - ); - - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery'] ?? [] as $className) { - $hookObject = GeneralUtility::makeInstance($className); - if (method_exists($hookObject, 'modifyQuery')) { - $hookObject->modifyQuery( - $parameters, - $table, - $pageId, - $additionalConstraints, - $fieldList, - $queryBuilder - ); - } - } - - return $queryBuilder; - } - - /** - * 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 . '" class="t3js-flag">' . $icon . '</span> <span class="t3js-language-title">' . $title . '</span>'; - } - return $title; - } - - /** - * Fetch the site language objects for the given $pageId and store it in $this->siteLanguages - * - * @param int $pageId - * @throws SiteNotFoundException - */ - protected function resolveSiteLanguages(int $pageId) - { - try { - $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId); - } catch (SiteNotFoundException $e) { - $site = new NullSite(); - } - $this->siteLanguages = $site->getAvailableLanguages($this->getBackendUser(), true, $pageId); - } - - /** - * @return string $title - */ - protected function getLocalizedPageTitle(): string - { - if (($this->tt_contentConfig['sys_language_uid'] ?? 0) > 0) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('pages'); - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) - ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace)); - $localizedPage = $queryBuilder - ->select('*') - ->from('pages') - ->where( - $queryBuilder->expr()->eq( - $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], - $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - $GLOBALS['TCA']['pages']['ctrl']['languageField'], - $queryBuilder->createNamedParameter($this->tt_contentConfig['sys_language_uid'], \PDO::PARAM_INT) - ) - ) - ->setMaxResults(1) - ->executeQuery() - ->fetchAssociative(); - BackendUtility::workspaceOL('pages', $localizedPage); - return $localizedPage['title']; - } - return $this->pageinfo['title']; - } - - /** - * Check if page can be edited by current user - * - * @return bool - */ - protected function isPageEditable() - { - if ($this->getBackendUser()->isAdmin()) { - return true; - } - return !$this->pageinfo['editlock'] && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::PAGE_EDIT); - } - - /** - * Check if content can be edited by current user - * - * @param int|null $languageId - * @return bool - */ - protected function isContentEditable(?int $languageId = null) - { - if ($this->getBackendUser()->isAdmin()) { - return true; - } - return !$this->pageinfo['editlock'] - && $this->hasContentModificationAndAccessPermissions() - && ($languageId === null || $this->getBackendUser()->checkLanguageAccess($languageId)); - } - - /** - * Check if current user has modification and access permissions for content set - * - * @return bool - */ - protected function hasContentModificationAndAccessPermissions(): bool - { - return $this->getBackendUser()->check('tables_modify', 'tt_content') - && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT); - } - - protected function getLanguageService(): LanguageService - { - return $GLOBALS['LANG']; - } -} diff --git a/typo3/sysext/backend/Classes/View/PageLayoutViewDrawFooterHookInterface.php b/typo3/sysext/backend/Classes/View/PageLayoutViewDrawFooterHookInterface.php deleted file mode 100644 index fee6bf094cea..000000000000 --- a/typo3/sysext/backend/Classes/View/PageLayoutViewDrawFooterHookInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Backend\View; - -/** - * Interface for classes which hook into PageLayoutView and do additional - * tt_content_drawFooter processing. - */ -interface PageLayoutViewDrawFooterHookInterface -{ - /** - * Preprocesses the preview footer rendering of a content element. - * - * @param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject Calling parent object - * @param array $info Processed values - * @param array $row Record row of tt_content - */ - public function preProcess(PageLayoutView &$parentObject, &$info, array &$row); -} diff --git a/typo3/sysext/backend/Classes/View/PageLayoutViewDrawItemHookInterface.php b/typo3/sysext/backend/Classes/View/PageLayoutViewDrawItemHookInterface.php deleted file mode 100644 index e2681c7a927b..000000000000 --- a/typo3/sysext/backend/Classes/View/PageLayoutViewDrawItemHookInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Backend\View; - -/** - * Interface for classes which hook into PageLayoutView and do additional - * tt_content_drawItem processing. - */ -interface PageLayoutViewDrawItemHookInterface -{ - /** - * Preprocesses the preview rendering of a content element. - * - * @param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject Calling parent object - * @param bool $drawItem Whether to draw the item using the default functionalities - * @param string $headerContent Header content - * @param string $itemContent Item content - * @param array $row Record row of tt_content - */ - public function preProcess(PageLayoutView &$parentObject, &$drawItem, &$headerContent, &$itemContent, array &$row); -} diff --git a/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioDefault.csv b/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioDefault.csv deleted file mode 100644 index 82efb2e608ab..000000000000 --- a/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioDefault.csv +++ /dev/null @@ -1,3 +0,0 @@ -"pages",,,,,,,,, -,uid,pid,sys_language_uid,deleted,l10n_parent,t3ver_state,t3ver_wsid,title,l10n_source -,17,0,0,0,0,0,0,Home,0 diff --git a/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioTranslationDone.csv b/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioTranslationDone.csv deleted file mode 100644 index 534241175d94..000000000000 --- a/typo3/sysext/backend/Tests/Functional/View/Fixtures/LanguageSelectorScenarioTranslationDone.csv +++ /dev/null @@ -1,4 +0,0 @@ -"pages",,,,,,,,, -,uid,pid,sys_language_uid,deleted,l10n_parent,t3ver_state,t3ver_wsid,title,l10n_source -,17,0,0,0,0,0,0,Home,0 -,2,0,3,0,17,0,0,"[Translate to polish] Home",17 diff --git a/typo3/sysext/backend/Tests/Functional/View/PageLayoutViewTest.php b/typo3/sysext/backend/Tests/Functional/View/PageLayoutViewTest.php deleted file mode 100644 index ba1a5e02d524..000000000000 --- a/typo3/sysext/backend/Tests/Functional/View/PageLayoutViewTest.php +++ /dev/null @@ -1,132 +0,0 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Backend\Tests\Functional\View; - -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Psr\EventDispatcher\EventDispatcherInterface; -use TYPO3\CMS\Backend\View\PageLayoutView; -use TYPO3\CMS\Core\Core\Bootstrap; -use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; -use TYPO3\CMS\Core\Http\NormalizedParams; -use TYPO3\CMS\Core\Http\ServerRequest; -use TYPO3\CMS\Core\Site\Entity\Site; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\TestingFramework\Core\AccessibleObjectInterface; -use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; - -class PageLayoutViewTest extends FunctionalTestCase -{ - use ProphecyTrait; - - /** - * @var PageLayoutView|AccessibleObjectInterface - */ - private $subject; - - /** - * Sets up this test case. - */ - protected function setUp(): void - { - parent::setUp(); - - $eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $eventDispatcher->dispatch(Argument::cetera())->willReturnArgument(0); - - $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); - $this->setUpBackendUser(1); - Bootstrap::initializeLanguageObject(); - - $site = new Site('test', 1, [ - 'identifier' => 'test', - 'rootPageId' => 1, - 'base' => '/', - 'languages' => [ - [ - 'languageId' => 0, - 'locale' => '', - 'base' => '/', - 'title' => 'default', - ], - [ - 'languageId' => 1, - 'locale' => '', - 'base' => '/', - 'title' => 'german', - ], - [ - 'languageId' => 2, - 'locale' => '', - 'base' => '/', - 'title' => 'french', - ], - [ - 'languageId' => 3, - 'locale' => '', - 'base' => '/', - 'title' => 'polish', - ], - ], - ]); - $this->subject = $this->getAccessibleMock(PageLayoutView::class, ['dummy'], [$eventDispatcher->reveal()]); - $this->subject->_set('siteLanguages', $site->getLanguages()); - - $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://www.example.com/')) - ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE) - ->withAttribute('normalizedParams', new NormalizedParams([], [], '', '')); - } - - /** - * @test - */ - public function languageSelectorShowsAllAvailableLanguagesForTranslation(): void - { - $this->importCSVDataSet(__DIR__ . '/Fixtures/LanguageSelectorScenarioDefault.csv'); - - $result = $this->subject->languageSelector(17); - - $matches = []; - - preg_match_all('/<option value=.+<\/option>/', $result, $matches); - $resultingOptions = GeneralUtility::trimExplode('</option>', $matches[0][0], true); - self::assertCount(4, $resultingOptions); - // first entry is the empty option - self::assertStringEndsWith('german', $resultingOptions[1]); - self::assertStringEndsWith('french', $resultingOptions[2]); - self::assertStringEndsWith('polish', $resultingOptions[3]); - } - - /** - * @test - */ - public function languageSelectorDoesNotOfferLanguageIfTranslationHasBeenDoneAlready(): void - { - $this->importCSVDataSet(__DIR__ . '/Fixtures/LanguageSelectorScenarioTranslationDone.csv'); - $result = $this->subject->languageSelector(17); - - $matches = []; - - preg_match_all('/<option value=.+<\/option>/', $result, $matches); - $resultingOptions = GeneralUtility::trimExplode('</option>', $matches[0][0], true); - self::assertCount(3, $resultingOptions); - // first entry is the empty option - self::assertStringEndsWith('german', $resultingOptions[1]); - self::assertStringEndsWith('french', $resultingOptions[2]); - } -} diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98375-RemovedHooksInPageModule.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98375-RemovedHooksInPageModule.rst new file mode 100644 index 000000000000..d2e79f2054e0 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98375-RemovedHooksInPageModule.rst @@ -0,0 +1,60 @@ +.. include:: /Includes.rst.txt + +.. _breaking-98375-1663598608: + +=============================================== +Breaking: #98375 - Removed hooks in Page Module +=============================================== + +See :issue:`98375` + +Description +=========== + +Since TYPO3 v10, TYPO3 Backend's Page Module is based on Fluid and custom +rendering functionality. The internal class "PageLayoutView" is now removed, +along with its interfaces and hooks. + +The following hooks are removed with a PSR-14 equivalent: + +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']` + +The following hooks have been removed without substitution: +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawFooter']` + +Existing patterns such as the PreviewRenderer concept can be used instead of +the latter hooks. + +Impact +====== + +Registering one of the hooks above in TYPO3 v12+ has no effect anymore. + + +Affected installations +====================== + +TYPO3 installations with modifications to the page module in third-party +extensions via one of the hooks. + + +Migration +========= + +Use :php:`TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent` as a +drop-in alternative for :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used']` + +Use :php:`TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent` +as a drop-in replacement for :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery']` + +Use :php:`TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent` as a +drop-in replacement for :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']` + +For extension authors that use these hooks can register a new Event Listener +and keep the hook registration to stay compatible with TYPO3 v11 and TYPO3 v12 +at the same time. + +.. index:: Backend, PHP-API, FullyScanned, ext:backend diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-98375-PSR-14EventsInPageModule.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-98375-PSR-14EventsInPageModule.rst new file mode 100644 index 000000000000..c9d5cd3837d3 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-98375-PSR-14EventsInPageModule.rst @@ -0,0 +1,40 @@ +.. include:: /Includes.rst.txt + +.. _feature-98375-1663598746: + +============================================== +Feature: #98375 - PSR-14 Events in Page Module +============================================== + +See :issue:`98375` + +Description +=========== + +Three new PSR-14 Events have been added to TYPO3's Page Module to modify +the preparation and rendering of content elements: + +* :php:`TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent` +* :php:`TYPO3\CMS\Backend\View\Event\ModifyDatabaseQueryForContentEvent` +* :php:`TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent` + +They are drop-in replacement to the removed hooks: + +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][PageLayoutView::class]['modifyQuery']` +* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']` + + +Impact +====== + +Use :php:`IsContentUsedOnPageLayoutEvent` to identify if a content has been used +in a column that isn't on a Backend Layout. + +Use :php:`ModifyDatabaseQueryForContentEvent` to filter out certain content elements +from being shown in the Page Module. + +Use :php:`PageContentPreviewRenderingEvent` to ship an alternative rendering for a +specific content type. + +.. index:: Backend, PHP-API, ext:backend diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php index f16a1c582518..e01af912d52d 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php @@ -871,4 +871,34 @@ return [ 'Feature-98303-PSR-14EventsForModifyingLanguageOverlays.rst', ], ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'cms/layout/class.tx_cms_layout.php\'][\'record_is_used\']' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'TYPO3\CMS\Backend\View\PageLayoutView\'][\'modifyQuery\']' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'cms/layout/class.tx_cms_layout.php\'][\'tt_content_drawItem\']' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'cms/layout/class.tx_cms_layout.php\'][\'list_type_Info\']' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'cms/layout/class.tx_cms_layout.php\'][\'tt_content_drawFooter\']' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php index 2c046a12f4d1..1825ae246207 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php @@ -2003,4 +2003,22 @@ return [ 'Breaking-98281-MakeAbstractPluginInternal.rst', ], ], + 'TYPO3\CMS\Backend\View\PageLayoutView' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + 'TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], + 'TYPO3\CMS\Backend\View\PageLayoutViewDrawFooterHookInterface' => [ + 'restFiles' => [ + 'Breaking-98375-RemovedHooksInPageModule.rst', + 'Feature-98375-PSR-14EventsInPageModule.rst', + ], + ], ]; -- GitLab