From 3d03fc5866925c73a6beceb7182316b0d815e4cc Mon Sep 17 00:00:00 2001 From: Achim Fritz Date: Fri, 2 Oct 2026 15:59:13 +0200 Subject: [PATCH] [BUGFIX] Show children of detached container translations When a translated container is detached from its default language record (connected -> free mode), its translated child elements were neither rendered inside the container nor listed as unused content in the page module, which made them inaccessible. Fixes: #732 --- Build/phpstan-baseline-13.neon | 4 ++ Classes/Listener/ContentUsedOnPage.php | 43 ++++++++++++++++++- ...en_of_detachted_contaienr_translations.csv | 11 +++++ .../Listener/ContentUsedOnPageTest.php | 19 ++++++++ Tests/Unit/Listener/ContentUsedOnPageTest.php | 13 ++++-- 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 Tests/Functional/Hooks/Fixtures/UsedRecords/children_of_detachted_contaienr_translations.csv diff --git a/Build/phpstan-baseline-13.neon b/Build/phpstan-baseline-13.neon index cd9e6827..f198e7a8 100644 --- a/Build/phpstan-baseline-13.neon +++ b/Build/phpstan-baseline-13.neon @@ -74,3 +74,7 @@ parameters: message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 path: ../Classes/Backend/Grid/ContainerGridColumnItem.php + - + message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Backend\\\\Domain\\\\Repository\\\\Localization\\\\LocalizationRepository\\:\\:getRecordTranslation\\(\\)\\.$#" + count: 1 + path: ../Classes/Listener/ContentUsedOnPage.php \ No newline at end of file diff --git a/Classes/Listener/ContentUsedOnPage.php b/Classes/Listener/ContentUsedOnPage.php index 6692da7a..4f3847a8 100644 --- a/Classes/Listener/ContentUsedOnPage.php +++ b/Classes/Listener/ContentUsedOnPage.php @@ -15,14 +15,20 @@ use B13\Container\Domain\Factory\Exception; use B13\Container\Domain\Factory\PageView\Backend\ContainerFactory; use B13\Container\Tca\Registry; +use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository; +use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent; use TYPO3\CMS\Core\Attribute\AsEventListener; +use TYPO3\CMS\Core\Information\Typo3Version; #[AsEventListener(identifier: 'tx-container-content-used-on-page')] class ContentUsedOnPage { - public function __construct(protected ContainerFactory $containerFactory, protected Registry $tcaRegistry) - { + public function __construct( + protected ContainerFactory $containerFactory, + protected Registry $tcaRegistry, + protected LocalizationRepository $localizationRepository + ) { } public function __invoke(IsContentUsedOnPageLayoutEvent $event): void @@ -31,6 +37,14 @@ public function __invoke(IsContentUsedOnPageLayoutEvent $event): void if ($record['tx_container_parent'] > 0) { try { $container = $this->containerFactory->buildContainer((int)$record['tx_container_parent']); + if (($record['sys_language_uid'] ?? 0) > 0 && ($record['l18n_parent'] ?? 0) > 0) { + // child is connected, assure container is connected too + if ($this->assureContainerIsConnected($record) === false) { + $event->setUsed(false); + return; + } + } + $columns = $this->tcaRegistry->getAvailableColumns($container->getCType()); foreach ($columns as $column) { if ($column['colPos'] === (int)$record['colPos']) { @@ -48,4 +62,29 @@ public function __invoke(IsContentUsedOnPageLayoutEvent $event): void } } } + + protected function assureContainerIsConnected(array $record): bool + { + if ((new Typo3Version())->getMajorVersion() < 14) { + $translations = BackendUtility::getRecordLocalization( + 'tt_content', + (int)$record['tx_container_parent'], + (int)$record['sys_language_uid'] + ); + + if ($translations === false || empty($translations[0]) || (int)($translations[0]['l18n_parent'] ?? 0) !== (int)$record['tx_container_parent']) { + return false; + } + } else { + $translation = $this->localizationRepository->getRecordTranslation( + 'tt_content', + (int)$record['tx_container_parent'], + (int)$record['sys_language_uid'] + ); + if ($translation === null || (int)$translation->getRawRecord()->get('l18n_parent') !== (int)$record['tx_container_parent']) { + return false; + } + } + return true; + } } diff --git a/Tests/Functional/Hooks/Fixtures/UsedRecords/children_of_detachted_contaienr_translations.csv b/Tests/Functional/Hooks/Fixtures/UsedRecords/children_of_detachted_contaienr_translations.csv new file mode 100644 index 00000000..6ba71893 --- /dev/null +++ b/Tests/Functional/Hooks/Fixtures/UsedRecords/children_of_detachted_contaienr_translations.csv @@ -0,0 +1,11 @@ +"tt_content" +,"uid","pid","CType","header","sys_language_uid","l18n_parent","l10n_source","tx_container_parent" +,"1","1","b13-2cols-with-header-container","My Container","0","0","0","0" +,"2","1","textmedia","Child Element","0","0","0","1" +,"5","1","b13-2cols-with-header-container","[Translate to de:] My Container","1","0","1","0" +,"6","1","textmedia","[Translate to de:] Child Element","1","2","2","1" +"pages" +,"uid","pid","sys_language_uid","l10n_parent" +,"1","0","0","0" +,"2","0","1","1" + diff --git a/Tests/Functional/Listener/ContentUsedOnPageTest.php b/Tests/Functional/Listener/ContentUsedOnPageTest.php index 74dadcfc..0f988a26 100644 --- a/Tests/Functional/Listener/ContentUsedOnPageTest.php +++ b/Tests/Functional/Listener/ContentUsedOnPageTest.php @@ -28,6 +28,13 @@ class ContentUsedOnPageTest extends FunctionalTestCase 'typo3conf/ext/container_example', ]; + protected function setUp(): void + { + parent::setUp(); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $GLOBALS['BE_USER'] = $GLOBALS['BE_USER'] = $this->setUpBackendUser(1); + } + #[Test] public function addContainerChildrenReturnsTrueIfChildrenInContainer(): void { @@ -88,6 +95,18 @@ public function addContainerChildrenReturnsTrueForLocalizedContent(): void self::assertTrue($event->isRecordUsed()); } + #[Test] + public function childrenOfDetachedContainerTranslationsReturnsTrue(): void + { + $this->importCSVDataSet(__DIR__ . '/../Hooks/Fixtures/UsedRecords/children_of_detachted_contaienr_translations.csv'); + $pageLayoutContext = $this->getMockBuilder(PageLayoutContext::class)->disableOriginalConstructor()->getMock(); + $record = $this->fetchOneRecordByUid(6); + $event = new IsContentUsedOnPageLayoutEvent($record, false, $pageLayoutContext); + $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class); + $listener($event); + self::assertFalse($event->isRecordUsed()); + } + protected function fetchOneRecordByUid(int $uid): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); diff --git a/Tests/Unit/Listener/ContentUsedOnPageTest.php b/Tests/Unit/Listener/ContentUsedOnPageTest.php index e96ab0bc..3c94b8e6 100644 --- a/Tests/Unit/Listener/ContentUsedOnPageTest.php +++ b/Tests/Unit/Listener/ContentUsedOnPageTest.php @@ -17,6 +17,7 @@ use B13\Container\Listener\ContentUsedOnPage; use B13\Container\Tca\Registry; use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository; use TYPO3\CMS\Backend\View\Event\IsContentUsedOnPageLayoutEvent; use TYPO3\CMS\Backend\View\PageLayoutContext; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -32,8 +33,9 @@ public function addContainerChildrenReturnsUsedOfParamsIfTxContainerParentIsZero $containerFactory = $this->getMockBuilder(ContainerFactory::class)->disableOriginalConstructor()->getMock(); $registry = $this->getMockBuilder(Registry::class)->disableOriginalConstructor()->getMock(); $pageLayoutContext = $this->getMockBuilder(PageLayoutContext::class)->disableOriginalConstructor()->getMock(); + $localizationRepository = $this->getMockBuilder(LocalizationRepository::class)->disableOriginalConstructor()->getMock(); $event = new IsContentUsedOnPageLayoutEvent(['tx_container_parent' => 0], true, $pageLayoutContext); - $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $registry); + $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $registry, $localizationRepository); $listener($event); self::assertTrue($event->isRecordUsed()); $event = new IsContentUsedOnPageLayoutEvent(['tx_container_parent' => 0], false, $pageLayoutContext); @@ -45,6 +47,7 @@ public function addContainerChildrenReturnsUsedOfParamsIfTxContainerParentIsZero public function addContainerChildrenReturnsTrueIfChildrenInContainerColPos(): void { $pageLayoutContext = $this->getMockBuilder(PageLayoutContext::class)->disableOriginalConstructor()->getMock(); + $localizationRepository = $this->getMockBuilder(LocalizationRepository::class)->disableOriginalConstructor()->getMock(); $containerFactory = $this->getMockBuilder(ContainerFactory::class) ->disableOriginalConstructor() ->onlyMethods(['buildContainer']) @@ -60,7 +63,7 @@ public function addContainerChildrenReturnsTrueIfChildrenInContainerColPos(): vo $tcaRegistry->expects(self::once())->method('getAvailableColumns')->with('myCType')->willReturn([['colPos' => 2]]); $event = new IsContentUsedOnPageLayoutEvent(['tx_container_parent' => 1, 'colPos' => 2, 'uid' => 3, 'sys_language_uid' => 0], false, $pageLayoutContext); - $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry); + $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry, $localizationRepository); $listener($event); self::assertTrue($event->isRecordUsed()); } @@ -69,6 +72,7 @@ public function addContainerChildrenReturnsTrueIfChildrenInContainerColPos(): vo public function addContainerChildrenReturnsFalseIfChildrenIsNotInContainerColPos(): void { $pageLayoutContext = $this->getMockBuilder(PageLayoutContext::class)->disableOriginalConstructor()->getMock(); + $localizationRepository = $this->getMockBuilder(LocalizationRepository::class)->disableOriginalConstructor()->getMock(); $containerFactory = $this->getMockBuilder(ContainerFactory::class) ->disableOriginalConstructor() ->onlyMethods(['buildContainer']) @@ -84,7 +88,7 @@ public function addContainerChildrenReturnsFalseIfChildrenIsNotInContainerColPos $tcaRegistry->expects(self::once())->method('getAvailableColumns')->with('myCType')->willReturn([['colPos' => 2]]); $event = new IsContentUsedOnPageLayoutEvent(['tx_container_parent' => 1, 'colPos' => 2, 'uid' => 3, 'sys_language_uid' => 0], false, $pageLayoutContext); - $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry); + $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry, $localizationRepository); $listener($event); self::assertFalse($event->isRecordUsed()); } @@ -93,6 +97,7 @@ public function addContainerChildrenReturnsFalseIfChildrenIsNotInContainerColPos public function addContainerChildrenReturnsFalseIfChildrenIsNotInRegisterdGrid(): void { $pageLayoutContext = $this->getMockBuilder(PageLayoutContext::class)->disableOriginalConstructor()->getMock(); + $localizationRepository = $this->getMockBuilder(LocalizationRepository::class)->disableOriginalConstructor()->getMock(); $containerFactory = $this->getMockBuilder(ContainerFactory::class) ->disableOriginalConstructor() ->onlyMethods(['buildContainer']) @@ -107,7 +112,7 @@ public function addContainerChildrenReturnsFalseIfChildrenIsNotInRegisterdGrid() $tcaRegistry->expects(self::once())->method('getAvailableColumns')->with('myCType')->willReturn([['colPos' => 3]]); $event = new IsContentUsedOnPageLayoutEvent(['tx_container_parent' => 1, 'colPos' => 2, 'uid' => 3], false, $pageLayoutContext); - $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry); + $listener = GeneralUtility::makeInstance(ContentUsedOnPage::class, $containerFactory, $tcaRegistry, $localizationRepository); $listener($event); self::assertFalse($event->isRecordUsed()); }