diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index e355f93d3eeaa95c642aa4cff1570369bb310f0c..4919376d7ff80ecada2f354e49c3cb8de69f74ec 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 7200cfa45580f5021044c3c60d64f2b20d852060..2a7f8dd5ea77f6751688877df152369ed5387602 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 66d0664ad3554ea14bc14dd4dc3f231a64d0341f..0a23e37a972a98c6d3623902d4cde1ca8fba660a 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 9d9c4cc442661778b01cb0a5b9412bc7893327d1..3c7b0483cd2c404f01ac1c0e81b6ee235675ea19 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 0000000000000000000000000000000000000000..b5d7a0a2f40952fefebc84a862ced09d78b4195b --- /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 0000000000000000000000000000000000000000..5b361adf835e1e9fbf666e6ba9a0be19e0b1aabd --- /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 0000000000000000000000000000000000000000..2588f266219ed3be489809f725c6d4a89979e0b8 --- /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 8d56228f94b8672e9003c78d58dafe1cfa4edf77..0000000000000000000000000000000000000000 --- 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 fee6bf094cea7feffcea73133688c05c9a65e162..0000000000000000000000000000000000000000 --- 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 e2681c7a927b81e9cf1090b41f738afc97c223ef..0000000000000000000000000000000000000000 --- 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 82efb2e608ab5ce0f3e5e6a6a65e2d98add562a2..0000000000000000000000000000000000000000 --- 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 534241175d94460f9022a0d97772013eb088f45a..0000000000000000000000000000000000000000 --- 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 ba1a5e02d5244809be7d23c7fd1a670e79515c5d..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..d2e79f2054e09f3bfc353f064f677567fb1f6adf --- /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 0000000000000000000000000000000000000000..c9d5cd3837d3fcf9d6eda7e418e94750b258d38e --- /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 f16a1c582518aa3a0973fe8490bc3758be058a69..e01af912d52dd419e4cabd5af78351d82f0e319f 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 2c046a12f4d10025e6f2234a0ee6a8f9a942f345..1825ae2462075d6c65df9948a503697684f2f979 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', + ], + ], ];