├── Classes ├── Backend │ ├── ContextMenu │ │ └── RecordContextMenuItemProvider.php │ ├── Grid │ │ ├── ContainerGridColumn.php │ │ └── ContainerGridColumnItem.php │ ├── Preview │ │ ├── ContainerPreviewRenderer.php │ │ └── GridRenderer.php │ └── Service │ │ └── NewContentUrlBuilder.php ├── Command │ ├── DeleteChildrenWithNonExistingParentCommand.php │ ├── DeleteChildrenWithWrongPidCommand.php │ ├── FixContainerParentForConnectedModeCommand.php │ ├── FixLanguageModeCommand.php │ ├── IntegrityCommand.php │ ├── SortingCommand.php │ └── SortingInPageCommand.php ├── ContentDefender │ ├── ContainerColumnConfigurationService.php │ ├── Hooks │ │ └── ColumnConfigurationManipulationHook.php │ └── Xclasses │ │ ├── CommandMapHook.php │ │ └── DatamapHook.php ├── DataProcessing │ ├── ContainerDataProcessingFailedException.php │ └── ContainerProcessor.php ├── Domain │ ├── Factory │ │ ├── ContainerFactory.php │ │ ├── Database.php │ │ ├── Exception.php │ │ ├── FrontendContainerFactory.php │ │ └── PageView │ │ │ └── Backend │ │ │ ├── ContainerFactory.php │ │ │ └── ContentStorage.php │ ├── Model │ │ └── Container.php │ └── Service │ │ └── ContainerService.php ├── Events │ ├── BeforeContainerConfigurationIsAppliedEvent.php │ └── BeforeContainerPreviewIsRenderedEvent.php ├── Exception.php ├── Hooks │ ├── Datahandler │ │ ├── CommandMapAfterFinishHook.php │ │ ├── CommandMapBeforeStartHook.php │ │ ├── CommandMapPostProcessingHook.php │ │ ├── Database.php │ │ ├── DatahandlerProcess.php │ │ ├── DatamapBeforeStartHook.php │ │ ├── DatamapPreProcessFieldArrayHook.php │ │ └── DeleteHook.php │ ├── UsedRecords.php │ └── WizardItems.php ├── Integrity │ ├── Database.php │ ├── Error │ │ ├── ChildInTranslatedContainerError.php │ │ ├── ErrorInterface.php │ │ ├── NonExistingParentWarning.php │ │ ├── UnusedColPosWarning.php │ │ ├── WrongL18nParentError.php │ │ ├── WrongLanguageWarning.php │ │ ├── WrongParentError.php │ │ └── WrongPidError.php │ ├── Integrity.php │ ├── IntegrityFix.php │ ├── Sorting.php │ └── SortingInPage.php ├── Listener │ ├── BootCompleted.php │ ├── ContentUsedOnPage.php │ ├── LegacyPageTsConfig.php │ ├── ModifyNewContentElementWizardItems.php │ ├── PageContentPreviewRendering.php │ ├── PageTsConfig.php │ └── RecordSummaryForLocalization.php ├── Service │ └── RecordLocalizeSummaryModifier.php ├── Tca │ ├── ContainerConfiguration.php │ ├── ItemProcFunc.php │ └── Registry.php ├── Updates │ ├── ContainerDeleteChildrenWithWrongPid.php │ └── ContainerMigrateSorting.php └── Xclasses │ └── LocalizationController.php ├── Configuration ├── Icons.php ├── JavaScriptModules.php ├── Services.yaml └── TCA │ └── Overrides │ └── tt_content.php ├── LICENSE ├── README.md ├── Resources ├── Private │ ├── Language │ │ └── locallang.xlf │ ├── Partials │ │ └── PageLayout │ │ │ ├── Grid │ │ │ ├── Column.html │ │ │ └── ColumnHeader.html │ │ │ └── Record.html │ ├── Partials11 │ │ └── PageLayout │ │ │ ├── Grid │ │ │ └── Column.html │ │ │ └── Record.html │ ├── Partials12 │ │ └── PageLayout │ │ │ ├── Grid │ │ │ ├── Column.html │ │ │ └── ColumnHeader.html │ │ │ └── Record.html │ └── Templates │ │ ├── Container.html │ │ └── Grid.html └── Public │ ├── Icons │ ├── Extension.svg │ ├── container-1col.svg │ ├── container-2col-left.svg │ ├── container-2col-right.svg │ ├── container-2col.svg │ ├── container-3col.svg │ └── container-4col.svg │ └── JavaScript │ ├── Overrides │ ├── drag-drop.js │ └── paste.js │ └── Overrides12 │ ├── drag-drop.js │ └── paste.js ├── composer.json ├── ext_emconf.php ├── ext_localconf.php └── ext_tables.sql /Classes/Backend/ContextMenu/RecordContextMenuItemProvider.php: -------------------------------------------------------------------------------- 1 | table === 'tt_content' 24 | && isset($this->record['tx_container_parent']) && $this->record['tx_container_parent'] > 0) { 25 | $languageField = method_exists($this, 'getLanguageField') ? $this->getLanguageField() : $GLOBALS['TCA']['tt_content']['ctrl']['languageField']; 26 | $urlParameters = [ 27 | 'id' => $this->record['pid'], 28 | 'sys_language_uid' => $this->record[$languageField] ?? null, 29 | 'colPos' => $this->record['colPos'], 30 | 'uid_pid' => -$this->record['uid'], 31 | 'tx_container_parent' => $this->record['tx_container_parent'], 32 | ]; 33 | $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 34 | $url = (string)$uriBuilder->buildUriFromRoute('new_content_element_wizard', $urlParameters); 35 | if (isset($attributes['data-new-wizard-url'])) { 36 | $attributes['data-new-wizard-url'] = $url; 37 | } 38 | } 39 | 40 | return $attributes; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Backend/Grid/ContainerGridColumn.php: -------------------------------------------------------------------------------- 1 | container = $container; 37 | $this->newContentUrl = $newContentUrl; 38 | $this->skipNewContentElementWizard = $skipNewContentElementWizard; 39 | } 40 | 41 | public function getContainerUid(): int 42 | { 43 | return $this->container->getUidOfLiveWorkspace(); 44 | } 45 | 46 | public function getNewContentElementWizardShouldBeSkipped(): bool 47 | { 48 | return $this->skipNewContentElementWizard; 49 | } 50 | 51 | public function getTitle(): string 52 | { 53 | return (string)$this->getLanguageService()->sL($this->getColumnName()); 54 | } 55 | 56 | public function getAllowNewContent(): bool 57 | { 58 | if ($this->container->getLanguage() > 0 && $this->container->isConnectedMode()) { 59 | return false; 60 | } 61 | return $this->newContentUrl !== null; 62 | } 63 | 64 | public function isActive(): bool 65 | { 66 | // yes we are active 67 | return true; 68 | } 69 | 70 | public function getNewContentUrl(): string 71 | { 72 | if ($this->newContentUrl === null) { 73 | return ''; 74 | } 75 | return $this->newContentUrl; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/Backend/Grid/ContainerGridColumnItem.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | $this->newContentUrl = $newContentUrl; 29 | } 30 | 31 | public function getAllowNewContent(): bool 32 | { 33 | if ($this->container->getLanguage() > 0 && $this->container->isConnectedMode()) { 34 | return false; 35 | } 36 | return true; 37 | } 38 | 39 | public function getWrapperClassName(): string 40 | { 41 | $wrapperClassNames = []; 42 | if ($this->isDisabled()) { 43 | $wrapperClassNames[] = 't3-page-ce-hidden t3js-hidden-record'; 44 | } 45 | // we do not need a "t3-page-ce-warning" class because we are build from Container 46 | return implode(' ', $wrapperClassNames); 47 | } 48 | 49 | public function getNewContentAfterUrl(): string 50 | { 51 | if ($this->newContentUrl === null) { 52 | return ''; 53 | } 54 | return $this->newContentUrl; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Classes/Backend/Preview/ContainerPreviewRenderer.php: -------------------------------------------------------------------------------- 1 | gridRenderer = $gridRenderer; 29 | $this->runtimeCache = $runtimeCache; 30 | } 31 | 32 | public function renderPageModulePreviewHeader(GridColumnItem $item): string 33 | { 34 | $this->runtimeCache->set('tx_container_current_gridColumItem', $item); 35 | return parent::renderPageModulePreviewHeader($item); 36 | } 37 | 38 | public function renderPageModulePreviewContent(GridColumnItem $item): string 39 | { 40 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 11) { 41 | return parent::renderPageModulePreviewContent($item); 42 | } 43 | $record = $item->getRecord(); 44 | $record['tx_container_grid'] = $this->gridRenderer->renderGrid($record, $item->getContext()); 45 | $item->setRecord($record); 46 | return parent::renderPageModulePreviewContent($item); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Classes/Backend/Service/NewContentUrlBuilder.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 37 | $this->containerColumnConfigurationService = $containerColumnConfigurationService; 38 | $this->containerService = $containerService; 39 | $this->uriBuilder = $uriBuilder; 40 | } 41 | 42 | public function getNewContentUrlAfterChild(PageLayoutContext $context, Container $container, int $columnNumber, int $recordUid, ?array $defVals): string 43 | { 44 | if ($defVals !== null) { 45 | return $this->getNewContentEditUrl($container, $columnNumber, -$recordUid, $defVals); 46 | } 47 | return $this->getNewContentWizardUrl($context, $container, $columnNumber, -$recordUid); 48 | } 49 | 50 | public function getNewContentUrlAtTopOfColumn(PageLayoutContext $context, Container $container, int $columnNumber, ?array $defVals): ?string 51 | { 52 | if ($this->containerColumnConfigurationService->isMaxitemsReached($container, $columnNumber)) { 53 | return null; 54 | } 55 | $newContentElementAtTopTarget = $this->containerService->getNewContentElementAtTopTargetInColumn($container, $columnNumber); 56 | if ($defVals !== null) { 57 | return $this->getNewContentEditUrl($container, $columnNumber, $newContentElementAtTopTarget, $defVals); 58 | } 59 | return $this->getNewContentWizardUrl($context, $container, $columnNumber, $newContentElementAtTopTarget); 60 | } 61 | 62 | protected function getNewContentEditUrl(Container $container, int $columnNumber, int $target, array $defVals): string 63 | { 64 | $ttContentDefVals = array_merge($defVals, [ 65 | 'colPos' => $columnNumber, 66 | 'sys_language_uid' => $container->getLanguage(), 67 | 'tx_container_parent' => $container->getUidOfLiveWorkspace(), 68 | ]); 69 | $urlParameters = [ 70 | 'edit' => [ 71 | 'tt_content' => [ 72 | $target => 'new', 73 | ], 74 | ], 75 | 'defVals' => [ 76 | 'tt_content' => $ttContentDefVals, 77 | ], 78 | 'returnUrl' => $this->getReturnUrl(), 79 | ]; 80 | return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); 81 | } 82 | 83 | protected function getNewContentWizardUrl(PageLayoutContext $context, Container $container, int $columnNumber, int $uidPid): string 84 | { 85 | $pageId = $context->getPageId(); 86 | $urlParameters = [ 87 | 'id' => $pageId, 88 | 'sys_language_uid' => $container->getLanguage(), 89 | 'colPos' => $columnNumber, 90 | 'tx_container_parent' => $container->getUidOfLiveWorkspace(), 91 | 'uid_pid' => $uidPid, 92 | 'returnUrl' => $this->getReturnUrl(), 93 | ]; 94 | return (string)$this->uriBuilder->buildUriFromRoute('new_content_element_wizard', $urlParameters); 95 | } 96 | 97 | protected function getServerRequest(): ?ServerRequestInterface 98 | { 99 | return $GLOBALS['TYPO3_REQUEST'] ?? null; 100 | } 101 | 102 | protected function getReturnUrl(): string 103 | { 104 | $request = $this->getServerRequest(); 105 | if ($request === null) { 106 | return ''; 107 | } 108 | return (string)$request->getAttribute('normalizedParams')->getRequestUri(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Classes/Command/DeleteChildrenWithNonExistingParentCommand.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 40 | $this->integrityFix = $integrityFix; 41 | parent::__construct($name); 42 | } 43 | 44 | public function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | Bootstrap::initializeBackendAuthentication(); 47 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 48 | $res = $this->integrity->run(); 49 | foreach ($res['warnings'] as $warning) { 50 | if ($warning instanceof NonExistingParentWarning) { 51 | $this->integrityFix->deleteChildrenWithNonExistingParent($warning); 52 | } 53 | } 54 | return 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Classes/Command/DeleteChildrenWithWrongPidCommand.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 40 | $this->integrityFix = $integrityFix; 41 | parent::__construct($name); 42 | } 43 | 44 | public function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | Bootstrap::initializeBackendAuthentication(); 47 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 48 | $res = $this->integrity->run(); 49 | foreach ($res['errors'] as $error) { 50 | if ($error instanceof WrongPidError) { 51 | $this->integrityFix->deleteChildrenWithWrongPid($error); 52 | } 53 | } 54 | return 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Classes/Command/FixContainerParentForConnectedModeCommand.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 37 | $this->integrityFix = $integrityFix; 38 | parent::__construct($name); 39 | } 40 | 41 | public function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $res = $this->integrity->run(); 44 | foreach ($res['errors'] as $error) { 45 | if ($error instanceof ChildInTranslatedContainerError) { 46 | $this->integrityFix->changeContainerParentToDefaultLanguageContainer($error); 47 | } 48 | } 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Classes/Command/FixLanguageModeCommand.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 37 | $this->integrityFix = $integrityFix; 38 | parent::__construct($name); 39 | } 40 | 41 | public function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $res = $this->integrity->run(); 44 | $errors = []; 45 | foreach ($res['errors'] as $error) { 46 | if ($error instanceof WrongL18nParentError) { 47 | $errors[] = $error; 48 | } 49 | } 50 | $this->integrityFix->languageMode($errors); 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Command/IntegrityCommand.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 31 | parent::__construct($name); 32 | } 33 | 34 | public function execute(InputInterface $input, OutputInterface $output): int 35 | { 36 | $io = new SymfonyStyle($input, $output); 37 | $res = $this->integrity->run(); 38 | if (count($res['errors']) > 0) { 39 | $errors = []; 40 | foreach ($res['errors'] as $error) { 41 | $errors[] = $error->getErrorMessage(); 42 | } 43 | $io->error('ERRORS: ' . chr(10) . implode(chr(10), $errors)); 44 | } 45 | if (count($res['warnings']) > 0) { 46 | $warnings = []; 47 | foreach ($res['warnings'] as $warning) { 48 | $warnings[] = $warning->getErrorMessage(); 49 | } 50 | $io->error('WARNINGS ("unused elements")' . chr(10) . implode(chr(10), $warnings)); 51 | } 52 | if (count($res['warnings']) === 0 && count($res['errors']) === 0) { 53 | $io->success('Good Job, no errors/warnings!'); 54 | } 55 | return 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/Command/SortingCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('pid', InputArgument::OPTIONAL, 'limit to this pid', 0); 35 | $this->addOption('apply', null, InputOption::VALUE_NONE, 'apply migration'); 36 | $this->addOption( 37 | 'enable-logging', 38 | null, 39 | InputOption::VALUE_NONE, 40 | 'enables datahandler logging, should only use for debug issues, not in production' 41 | ); 42 | } 43 | 44 | public function __construct(Sorting $sorting, ?string $name = null) 45 | { 46 | parent::__construct($name); 47 | $this->sorting = $sorting; 48 | } 49 | 50 | public function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $dryrun = $input->getOption('apply') !== true; 53 | $pid = (int)$input->getArgument('pid'); 54 | 55 | Bootstrap::initializeBackendAuthentication(); 56 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 57 | $errors = $this->sorting->run( 58 | $dryrun, 59 | $input->getOption('enable-logging'), 60 | $pid 61 | ); 62 | 63 | if ($output->isVerbose()) { 64 | $output->writeln('Checking ' . ($pid > 0 ? 'page ' . $pid : 'entire pagetree') . ($dryrun ? ' [DRYRUN]' : '')); 65 | foreach ($errors as $error) { 66 | $output->writeln($error); 67 | } 68 | if (empty($errors)) { 69 | $output->writeln('- all good, nothing to do here'); 70 | } 71 | } 72 | 73 | return self::SUCCESS; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Classes/Command/SortingInPageCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('pid', InputArgument::OPTIONAL, 'limit to this pid', 0); 35 | $this->addOption('apply', null, InputOption::VALUE_NONE, 'apply migration'); 36 | $this->addOption( 37 | 'enable-logging', 38 | null, 39 | InputOption::VALUE_NONE, 40 | 'enables datahandler logging, should only use for debug issues, not in production' 41 | ); 42 | } 43 | 44 | public function __construct(SortingInPage $sorting, ?string $name = null) 45 | { 46 | parent::__construct($name); 47 | $this->sorting = $sorting; 48 | } 49 | 50 | public function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $dryrun = $input->getOption('apply') !== true; 53 | $pid = (int)$input->getArgument('pid'); 54 | 55 | Bootstrap::initializeBackendAuthentication(); 56 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 57 | $errors = $this->sorting->run( 58 | $dryrun, 59 | $input->getOption('enable-logging'), 60 | $pid 61 | ); 62 | foreach ($errors as $error) { 63 | $output->writeln($error); 64 | } 65 | if (empty($errors)) { 66 | $output->writeln('migration finished'); 67 | } 68 | return 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Classes/ContentDefender/ContainerColumnConfigurationService.php: -------------------------------------------------------------------------------- 1 | contentDefenderContainerDataHandlerHookIsLocked = true; 42 | } 43 | 44 | public function endCmdMap(): void 45 | { 46 | $this->contentDefenderContainerDataHandlerHookIsLocked = false; 47 | } 48 | 49 | public function isContentDefenderContainerDataHandlerHookLooked(): bool 50 | { 51 | return $this->contentDefenderContainerDataHandlerHookIsLocked; 52 | } 53 | 54 | public function __construct(ContainerFactory $containerFactory, Registry $tcaRegistry) 55 | { 56 | $this->containerFactory = $containerFactory; 57 | $this->tcaRegistry = $tcaRegistry; 58 | } 59 | 60 | protected function getRecord(int $uid): ?array 61 | { 62 | return BackendUtility::getRecord('tt_content', $uid); 63 | } 64 | 65 | public function addCopyMapping(int $sourceContentId, int $containerId, int $targetColpos): void 66 | { 67 | $record = $this->getRecord($sourceContentId); 68 | $sourceColPos = (int)$record['colPos']; 69 | $sourceContainerId = (int)$record['tx_container_parent']; 70 | $this->copyMapping[$sourceContainerId . ContainerGridColumn::CONTAINER_COL_POS_DELIMITER . $sourceColPos] = [ 71 | 'containerId' => $containerId, 72 | 'sourceColPos' => $sourceColPos, 73 | 'targetColPos' => $targetColpos, 74 | ]; 75 | } 76 | 77 | public function override(array $columnConfiguration, int $containerId, int $colPos): array 78 | { 79 | try { 80 | $container = $this->containerFactory->buildContainer($containerId); 81 | $columnConfiguration = $this->getColumnConfigurationForContainer($container, $colPos); 82 | } catch (Exception $e) { 83 | // not a container 84 | } 85 | return $columnConfiguration; 86 | } 87 | 88 | protected function getColumnConfigurationForContainer(Container $container, int $colPos): array 89 | { 90 | $cType = $container->getCType(); 91 | $columnConfiguration = $this->tcaRegistry->getContentDefenderConfiguration($cType, $colPos); 92 | return $columnConfiguration; 93 | } 94 | 95 | public function isMaxitemsReachedByContainenrId(int $containerId, int $colPos, ?int $childUid = null): bool 96 | { 97 | try { 98 | $container = $this->containerFactory->buildContainer($containerId); 99 | return $this->isMaxitemsReached($container, $colPos, $childUid); 100 | } catch (Exception $e) { 101 | // not a container 102 | } 103 | return false; 104 | } 105 | 106 | public function isMaxitemsReached(Container $container, int $colPos, ?int $childUid = null): bool 107 | { 108 | $columnConfiguration = $this->getColumnConfigurationForContainer($container, $colPos); 109 | if (!isset($columnConfiguration['maxitems']) || (int)$columnConfiguration['maxitems'] === 0) { 110 | return false; 111 | } 112 | $childrenOfColumn = $container->getChildrenByColPos($colPos); 113 | $count = count($childrenOfColumn); 114 | if ($childUid !== null && $container->hasChildInColPos($colPos, $childUid)) { 115 | $count--; 116 | } 117 | return $count >= $columnConfiguration['maxitems']; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Classes/ContentDefender/Hooks/ColumnConfigurationManipulationHook.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 37 | $this->tcaRegistry = $tcaRegistry; 38 | } 39 | 40 | public function manipulateConfiguration(array $configuration, int $colPos, $recordUid): array 41 | { 42 | $parent = $this->getParentUid($recordUid); 43 | if ($parent === null) { 44 | return $configuration; 45 | } 46 | try { 47 | $container = $this->containerFactory->buildContainer($parent); 48 | } catch (Exception $e) { 49 | // not a container 50 | return $configuration; 51 | } 52 | $cType = $container->getCType(); 53 | $configuration = $this->tcaRegistry->getContentDefenderConfiguration($cType, $colPos); 54 | // maxitems needs not to be considered in this case 55 | // (new content elemment wizard, TcaCTypeItems: new record, TcaCTypeItems: edit record) 56 | // consider maxitems here leeds to errors, because relation to container gets lost in EXT:content_defender 57 | // EXT:container has already a own solution to prevent new records inside a container if maxitems is reached 58 | // "New Content" Button is not rendered inside die colPos, this is possible because EXT:container has its own templates 59 | $configuration['maxitems'] = 0; 60 | return $configuration; 61 | } 62 | 63 | private function getParentUid($recordUid): ?int 64 | { 65 | $request = $this->getServerRequest(); 66 | if ($request === null) { 67 | return null; 68 | } 69 | $queryParams = $request->getQueryParams(); 70 | if (isset($queryParams['tx_container_parent']) && $queryParams['tx_container_parent'] > 0) { 71 | // new content elemment wizard 72 | return (int)$queryParams['tx_container_parent']; 73 | } 74 | if ( 75 | isset($queryParams['defVals']['tt_content']['tx_container_parent']) && 76 | $queryParams['defVals']['tt_content']['tx_container_parent'] > 0 77 | ) { 78 | // TcaCTypeItems: new record 79 | return (int)$queryParams['defVals']['tt_content']['tx_container_parent']; 80 | } 81 | if (isset($queryParams['edit']['tt_content'][$recordUid])) { 82 | // TcaCTypeItems: edit record 83 | $record = BackendUtility::getRecord('tt_content', $recordUid, 'tx_container_parent'); 84 | if (isset($record['tx_container_parent'])) { 85 | return (int)$record['tx_container_parent']; 86 | } 87 | } 88 | return null; 89 | } 90 | 91 | protected function getServerRequest(): ?ServerRequest 92 | { 93 | return $GLOBALS['TYPO3_REQUEST'] ?? null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/ContentDefender/Xclasses/CommandMapHook.php: -------------------------------------------------------------------------------- 1 | containerColumnConfigurationService = $containerColumnConfigurationService ?? GeneralUtility::makeInstance(ContainerColumnConfigurationService::class); 38 | parent::__construct($contentRepository); 39 | } 40 | 41 | public function processCmdmap_beforeStart(DataHandler $dataHandler): void 42 | { 43 | if (isset($dataHandler->cmdmap['pages'])) 44 | { 45 | $this->containerColumnConfigurationService->startCmdMap(); 46 | } 47 | if (!empty($dataHandler->cmdmap['tt_content'])) { 48 | $this->containerColumnConfigurationService->startCmdMap(); 49 | foreach ($dataHandler->cmdmap['tt_content'] as $id => $cmds) { 50 | foreach ($cmds as $cmd => $data) { 51 | if ( 52 | ($cmd === 'copy' || $cmd === 'move') && 53 | (!empty($data['update'])) && 54 | isset($data['update']['colPos']) && 55 | $data['update']['colPos'] > 0 && 56 | isset($data['update']['tx_container_parent']) && 57 | $data['update']['tx_container_parent'] > 0 && 58 | MathUtility::canBeInterpretedAsInteger($id) 59 | ) { 60 | $this->mapping[(int)$id] = [ 61 | 'containerId' => (int)$data['update']['tx_container_parent'], 62 | 'colPos' => (int)$data['update']['colPos'], 63 | ]; 64 | $this->containerColumnConfigurationService->addCopyMapping( 65 | (int)$id, 66 | (int)$data['update']['tx_container_parent'], 67 | (int)$data['update']['colPos'] 68 | ); 69 | $useChildId = null; 70 | if ($cmd === 'move') { 71 | $useChildId = $id; 72 | } 73 | 74 | if ($this->containerColumnConfigurationService->isMaxitemsReachedByContainenrId((int)$data['update']['tx_container_parent'], (int)$data['update']['colPos'], $useChildId)) { 75 | unset($dataHandler->cmdmap['tt_content'][$id]); 76 | $recpid = null; 77 | $detailsNumber = null; 78 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() < 13) { 79 | $recpid = 0; 80 | $detailsNumber = 28; 81 | } 82 | $dataHandler->log( 83 | 'tt_content', 84 | $id, 85 | 1, 86 | $recpid, 87 | 1, 88 | 'The command couldn\'t be executed due to reached maxitems configuration', 89 | $detailsNumber 90 | ); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | parent::processCmdmap_beforeStart($dataHandler); 97 | } 98 | 99 | public function processCmdmap_postProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate, $pasteDatamap): void 100 | { 101 | $this->containerColumnConfigurationService->endCmdMap(); 102 | } 103 | 104 | protected function isRecordAllowedByRestriction(array $columnConfiguration, array $record): bool 105 | { 106 | if (isset($record['tx_container_parent']) && 107 | $record['tx_container_parent'] > 0 && 108 | (GeneralUtility::makeInstance(DatahandlerProcess::class))->isContainerInProcess((int)$record['tx_container_parent']) 109 | ) { 110 | return true; 111 | } 112 | $recordOrigUid = (int)($record['uid'] ?? 0); 113 | if (isset($this->mapping[$recordOrigUid])) { 114 | $columnConfiguration = $this->containerColumnConfigurationService->override( 115 | $columnConfiguration, 116 | $this->mapping[$recordOrigUid]['containerId'], 117 | $this->mapping[$recordOrigUid]['colPos'] 118 | ); 119 | } 120 | return parent::isRecordAllowedByRestriction($columnConfiguration, $record); 121 | } 122 | 123 | protected function isRecordAllowedByItemsCount(array $columnConfiguration, array $record): bool 124 | { 125 | if (isset($record['tx_container_parent']) && 126 | $record['tx_container_parent'] > 0 && 127 | (GeneralUtility::makeInstance(DatahandlerProcess::class))->isContainerInProcess((int)$record['tx_container_parent']) 128 | ) { 129 | return true; 130 | } 131 | if (isset($this->mapping[$record['uid']])) { 132 | return true; 133 | } 134 | return parent::isRecordAllowedByItemsCount($columnConfiguration, $record); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Classes/ContentDefender/Xclasses/DatamapHook.php: -------------------------------------------------------------------------------- 1 | containerColumnConfigurationService = $containerColumnConfigurationService ?? GeneralUtility::makeInstance(ContainerColumnConfigurationService::class); 38 | parent::__construct($contentRepository); 39 | } 40 | 41 | /** 42 | * @param DataHandler $dataHandler 43 | */ 44 | public function processDatamap_beforeStart(DataHandler $dataHandler): void 45 | { 46 | if (is_array($dataHandler->datamap['tt_content'] ?? null) && 47 | !$this->containerColumnConfigurationService->isContentDefenderContainerDataHandlerHookLooked() 48 | ) { 49 | foreach ($dataHandler->datamap['tt_content'] as $id => $values) { 50 | if ( 51 | isset($values['tx_container_parent']) && 52 | $values['tx_container_parent'] > 0 && 53 | isset($values['colPos']) && 54 | $values['colPos'] > 0 55 | ) { 56 | if (MathUtility::canBeInterpretedAsInteger($id)) { 57 | // edit 58 | continue; 59 | } 60 | $containerId = (int)$values['tx_container_parent']; 61 | 62 | if ($this->containerColumnConfigurationService->isMaxitemsReachedByContainenrId($containerId, (int)$values['colPos'])) { 63 | unset($dataHandler->datamap['tt_content'][$id]); 64 | $recpid = null; 65 | $detailsNumber = null; 66 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() < 13) { 67 | $recpid = 0; 68 | $detailsNumber = 28; 69 | } 70 | $dataHandler->log( 71 | 'tt_content', 72 | $id, 73 | 1, 74 | $recpid, 75 | 1, 76 | 'The command couldn\'t be executed due to reached maxitems configuration', 77 | $detailsNumber 78 | ); 79 | } 80 | } 81 | } 82 | parent::processDatamap_beforeStart($dataHandler); 83 | } 84 | } 85 | 86 | protected function isRecordAllowedByRestriction(array $columnConfiguration, array $record): bool 87 | { 88 | if ( 89 | isset($record['tx_container_parent']) && 90 | $record['tx_container_parent'] > 0 && 91 | (GeneralUtility::makeInstance(DatahandlerProcess::class))->isContainerInProcess((int)$record['tx_container_parent']) 92 | ) { 93 | return true; 94 | } 95 | return parent::isRecordAllowedByRestriction($columnConfiguration, $record); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Classes/DataProcessing/ContainerDataProcessingFailedException.php: -------------------------------------------------------------------------------- 1 | contentDataProcessor = $contentDataProcessor; 39 | $this->context = $context; 40 | $this->frontendContainerFactory = $frontendContainerFactory; 41 | } 42 | 43 | public function process( 44 | ContentObjectRenderer $cObj, 45 | array $contentObjectConfiguration, 46 | array $processorConfiguration, 47 | array $processedData 48 | ): array { 49 | if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) { 50 | return $processedData; 51 | } 52 | $contentId = null; 53 | if ($processorConfiguration['contentId.'] ?? false) { 54 | $contentId = (int)$cObj->stdWrap($processorConfiguration['contentId'] ?? '', $processorConfiguration['contentId.']); 55 | } elseif ($processorConfiguration['contentId'] ?? false) { 56 | $contentId = (int)$processorConfiguration['contentId']; 57 | } 58 | 59 | try { 60 | $container = $this->frontendContainerFactory->buildContainer($cObj, $this->context, $contentId); 61 | } catch (Exception $e) { 62 | // do nothing 63 | return $processedData; 64 | } 65 | 66 | $colPos = (int)$cObj->stdWrapValue('colPos', $processorConfiguration); 67 | if (empty($colPos)) { 68 | $allColPos = $container->getChildrenColPos(); 69 | foreach ($allColPos as $colPos) { 70 | $processedData = $this->processColPos( 71 | $cObj, 72 | $container, 73 | $colPos, 74 | 'children_' . $colPos, 75 | $processedData, 76 | $processorConfiguration 77 | ); 78 | } 79 | } else { 80 | $as = $cObj->stdWrapValue('as', $processorConfiguration, 'children'); 81 | $processedData = $this->processColPos( 82 | $cObj, 83 | $container, 84 | $colPos, 85 | $as, 86 | $processedData, 87 | $processorConfiguration 88 | ); 89 | } 90 | return $processedData; 91 | } 92 | 93 | protected function processColPos( 94 | ContentObjectRenderer $cObj, 95 | Container $container, 96 | int $colPos, 97 | string $as, 98 | array $processedData, 99 | array $processorConfiguration 100 | ): array { 101 | $children = $container->getChildrenByColPos($colPos); 102 | 103 | $contentRecordRenderer = $cObj->getContentObject('RECORDS'); 104 | if ($contentRecordRenderer === null) { 105 | throw new ContainerDataProcessingFailedException('RECORDS content object not available.', 1691483526); 106 | } 107 | $conf = [ 108 | 'tables' => 'tt_content', 109 | ]; 110 | foreach ($children as &$child) { 111 | if (!isset($processorConfiguration['skipRenderingChildContent']) || (int)$processorConfiguration['skipRenderingChildContent'] === 0) { 112 | $conf['source'] = $child['uid']; 113 | $child['renderedContent'] = $cObj->render($contentRecordRenderer, $conf); 114 | } 115 | /** @var ContentObjectRenderer $recordContentObjectRenderer */ 116 | $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); 117 | $recordContentObjectRenderer->start($child, 'tt_content'); 118 | $child = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $child); 119 | } 120 | $processedData[$as] = $children; 121 | return $processedData; 122 | } 123 | 124 | protected function getRequest(): ServerRequestInterface 125 | { 126 | return $GLOBALS['TYPO3_REQUEST']; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Classes/Domain/Factory/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | database = $database; 44 | $this->tcaRegistry = $tcaRegistry; 45 | $this->workspaceId = (int)$context->getPropertyFromAspect('workspace', 'id'); 46 | } 47 | 48 | protected function containerByUid(int $uid): ?array 49 | { 50 | return $this->database->fetchOneRecord($uid); 51 | } 52 | 53 | protected function defaultContainer(array $localizedContainer): ?array 54 | { 55 | return $this->database->fetchOneDefaultRecord($localizedContainer); 56 | } 57 | 58 | public function buildContainer(int $uid): Container 59 | { 60 | $record = $this->containerByUid($uid); 61 | if ($record === null) { 62 | throw new Exception('cannot fetch record with uid ' . $uid, 1576572850); 63 | } 64 | if (!$this->tcaRegistry->isContainerElement($record['CType'])) { 65 | throw new Exception('not a container element with uid ' . $uid, 1576572851); 66 | } 67 | 68 | $defaultRecord = null; 69 | $language = (int)$record['sys_language_uid']; 70 | if ($language > 0) { 71 | $defaultRecord = $this->defaultContainer($record); 72 | if ($defaultRecord === null) { 73 | // free mode 74 | $childRecords = $this->children($record, $language); 75 | } else { 76 | // connected mode 77 | $defaultRecords = $this->children($defaultRecord, 0); 78 | $childRecords = $this->localizedRecordsByDefaultRecords($defaultRecords, $language); 79 | } 80 | } else { 81 | $childRecords = $this->children($record, $language); 82 | } 83 | $childRecordByColPosKey = $this->recordsByColPosKey($childRecords); 84 | if ($defaultRecord === null) { 85 | $container = GeneralUtility::makeInstance(Container::class, $record, $childRecordByColPosKey, $language); 86 | } else { 87 | $container = GeneralUtility::makeInstance(Container::class, $defaultRecord, $childRecordByColPosKey, $language); 88 | } 89 | return $container; 90 | } 91 | 92 | protected function localizedRecordsByDefaultRecords(array $defaultRecords, int $language): array 93 | { 94 | $localizedRecords = $this->database->fetchOverlayRecords($defaultRecords, $language); 95 | $childRecords = $this->sortLocalizedRecordsByDefaultRecords($defaultRecords, $localizedRecords); 96 | return $childRecords; 97 | } 98 | 99 | protected function children(array $containerRecord, int $language): array 100 | { 101 | $records = $this->database->fetchRecordsByParentAndLanguage((int)$containerRecord['uid'], $language); 102 | $records = $this->workspaceOverlay($records); 103 | return $records; 104 | } 105 | 106 | protected function workspaceOverlay(array $records): array 107 | { 108 | $filtered = []; 109 | foreach ($records as $row) { 110 | BackendUtility::workspaceOL('tt_content', $row, $this->workspaceId, true); 111 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 12) { 112 | if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { 113 | $filtered[] = $row; 114 | } 115 | } else { 116 | if ($row && !VersionState::cast($row['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)) { 117 | $filtered[] = $row; 118 | } 119 | } 120 | } 121 | return $filtered; 122 | } 123 | 124 | protected function sortLocalizedRecordsByDefaultRecords(array $defaultRecords, array $localizedRecords): array 125 | { 126 | $sorted = []; 127 | foreach ($defaultRecords as $defaultRecord) { 128 | foreach ($localizedRecords as $localizedRecord) { 129 | if ($localizedRecord['l18n_parent'] === $defaultRecord['uid'] || 130 | $localizedRecord['l18n_parent'] === $defaultRecord['t3ver_oid'] 131 | ) { 132 | $sorted[] = $localizedRecord; 133 | } 134 | } 135 | } 136 | return $sorted; 137 | } 138 | 139 | protected function recordsByColPosKey(array $records): array 140 | { 141 | $recordsByColPosKey = []; 142 | foreach ($records as $record) { 143 | if (empty($recordsByColPosKey[$record['colPos']])) { 144 | $recordsByColPosKey[$record['colPos']] = []; 145 | } 146 | $recordsByColPosKey[$record['colPos']][] = $record; 147 | } 148 | return $recordsByColPosKey; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Classes/Domain/Factory/Database.php: -------------------------------------------------------------------------------- 1 | backendUserId = (int)$context->getPropertyFromAspect('backend.user', 'id', 0); 39 | $this->workspaceId = (int)$context->getPropertyFromAspect('workspace', 'id'); 40 | } 41 | 42 | protected function getQueryBuilder(): QueryBuilder 43 | { 44 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); 45 | $queryBuilder->getRestrictions() 46 | ->removeAll() 47 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 48 | ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->workspaceId)); 49 | return $queryBuilder; 50 | } 51 | 52 | public function fetchRecordsByPidAndLanguage(int $pid, int $language): array 53 | { 54 | $queryBuilder = $this->getQueryBuilder(); 55 | $rows = $queryBuilder->select('*') 56 | ->from('tt_content') 57 | ->where( 58 | $queryBuilder->expr()->eq( 59 | 'sys_language_uid', 60 | $queryBuilder->createNamedParameter($language, Connection::PARAM_INT) 61 | ), 62 | $queryBuilder->expr()->eq( 63 | 'pid', 64 | $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT) 65 | ) 66 | ) 67 | ->orderBy('sorting', 'ASC') 68 | ->executeQuery() 69 | ->fetchAllAssociative(); 70 | return $rows; 71 | } 72 | 73 | public function fetchOneRecord(int $uid): ?array 74 | { 75 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); 76 | $queryBuilder->getRestrictions()->removeAll(); 77 | $record = $queryBuilder->select('*') 78 | ->from('tt_content') 79 | ->where( 80 | $queryBuilder->expr()->eq( 81 | 'uid', 82 | $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) 83 | ) 84 | ) 85 | ->executeQuery() 86 | ->fetchAssociative(); 87 | if ($record === false) { 88 | return null; 89 | } 90 | return $record; 91 | } 92 | 93 | public function fetchOneDefaultRecord(array $record): ?array 94 | { 95 | $queryBuilder = $this->getQueryBuilder(); 96 | $record = $queryBuilder->select('*') 97 | ->from('tt_content') 98 | ->where( 99 | $queryBuilder->expr()->eq( 100 | 'uid', 101 | $queryBuilder->createNamedParameter($record['l18n_parent'], Connection::PARAM_INT) 102 | ), 103 | $queryBuilder->expr()->eq( 104 | 'sys_language_uid', 105 | $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) 106 | ) 107 | ) 108 | ->executeQuery() 109 | ->fetchAssociative(); 110 | if ($record === false) { 111 | return null; 112 | } 113 | return $record; 114 | } 115 | 116 | public function fetchRecordsByParentAndLanguage(int $parent, int $language): array 117 | { 118 | $queryBuilder = $this->getQueryBuilder(); 119 | 120 | $rows = $queryBuilder->select('*') 121 | ->from('tt_content') 122 | ->where( 123 | $queryBuilder->expr()->eq( 124 | 'tx_container_parent', 125 | $queryBuilder->createNamedParameter($parent, Connection::PARAM_INT) 126 | ), 127 | $queryBuilder->expr()->eq( 128 | 'sys_language_uid', 129 | $queryBuilder->createNamedParameter($language, Connection::PARAM_INT) 130 | ) 131 | ) 132 | ->orderBy('sorting', 'ASC') 133 | ->executeQuery() 134 | ->fetchAllAssociative(); 135 | return $rows; 136 | } 137 | 138 | public function fetchOverlayRecords(array $records, int $language): array 139 | { 140 | $uids = []; 141 | foreach ($records as $record) { 142 | $uids[] = $record['uid']; 143 | if ($record['t3ver_oid'] > 0) { 144 | $uids[] = $record['t3ver_oid']; 145 | } 146 | } 147 | $queryBuilder = $this->getQueryBuilder(); 148 | $rows = $queryBuilder->select('*') 149 | ->from('tt_content') 150 | ->where( 151 | $queryBuilder->expr()->in( 152 | 'l18n_parent', 153 | $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY) 154 | ), 155 | $queryBuilder->expr()->eq( 156 | 'sys_language_uid', 157 | $queryBuilder->createNamedParameter($language, Connection::PARAM_INT) 158 | ) 159 | ) 160 | ->executeQuery() 161 | ->fetchAllAssociative(); 162 | return $rows; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Classes/Domain/Factory/Exception.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 30 | } 31 | 32 | public function buildContainer(ContentObjectRenderer $cObj, Context $context, ?int $uid = null): Container 33 | { 34 | if ($uid === null) { 35 | $record = $cObj->data; 36 | } else { 37 | $records = $cObj->getRecords('tt_content', ['uidInList' => $uid, 'pidInList' => 0]); 38 | if (empty($records)) { 39 | throw new Exception('no record ' . $uid, 1734946029); 40 | } 41 | $record = $records[0]; 42 | } 43 | if (!$this->tcaRegistry->isContainerElement($record['CType'] ?? '')) { 44 | throw new Exception('not a container element with uid ' . $uid, 1734946028); 45 | } 46 | $conf = ['where' => 'tx_container_parent=' . $record['uid'], 'orderBy' => 'sorting', 'pidInList' => $record['pid']]; 47 | /** @var LanguageAspect $languageAspect */ 48 | $languageAspect = $context->getAspect('language'); 49 | if ($languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF && $record['l18n_parent'] > 0) { 50 | $conf['where'] .= ' OR tx_container_parent=' . $record['l18n_parent']; 51 | } 52 | $childRecords = $cObj->getRecords('tt_content', $conf); 53 | $childRecords = $this->recordsByColPosKey($childRecords); 54 | $container = new Container($record, $childRecords, (int)$record['sys_language_uid']); 55 | return $container; 56 | } 57 | 58 | protected function recordsByColPosKey(array $records): array 59 | { 60 | $recordsByColPosKey = []; 61 | foreach ($records as $record) { 62 | if (empty($recordsByColPosKey[$record['colPos']])) { 63 | $recordsByColPosKey[$record['colPos']] = []; 64 | } 65 | $recordsByColPosKey[$record['colPos']][] = $record; 66 | } 67 | return $recordsByColPosKey; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Classes/Domain/Factory/PageView/Backend/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | contentStorage = $contentStorage; 34 | } 35 | 36 | protected function children(array $containerRecord, int $language): array 37 | { 38 | return $this->contentStorage->getContainerChildren($containerRecord, $language); 39 | } 40 | 41 | protected function localizedRecordsByDefaultRecords(array $defaultRecords, int $language): array 42 | { 43 | $childRecords = parent::localizedRecordsByDefaultRecords($defaultRecords, $language); 44 | return $this->contentStorage->workspaceOverlay($childRecords); 45 | } 46 | 47 | protected function containerByUid(int $uid): ?array 48 | { 49 | $record = $this->database->fetchOneRecord($uid); 50 | if ($record === null) { 51 | return null; 52 | } 53 | return $this->contentStorage->containerRecordWorkspaceOverlay($record); 54 | } 55 | 56 | protected function defaultContainer(array $localizedContainer): ?array 57 | { 58 | if (isset($localizedContainer['_ORIG_uid'])) { 59 | $localizedContainer = $this->database->fetchOneRecord((int)$localizedContainer['uid']); 60 | } 61 | $defaultRecord = $this->database->fetchOneDefaultRecord($localizedContainer); 62 | if ($defaultRecord === null) { 63 | return null; 64 | } 65 | return $this->contentStorage->containerRecordWorkspaceOverlay($defaultRecord); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/Domain/Factory/PageView/Backend/ContentStorage.php: -------------------------------------------------------------------------------- 1 | database = $database; 42 | $this->workspaceId = (int)$context->getPropertyFromAspect('workspace', 'id'); 43 | } 44 | 45 | protected function buildRecords(int $pid, int $language): array 46 | { 47 | $records = $this->database->fetchRecordsByPidAndLanguage($pid, $language); 48 | $records = $this->workspaceOverlay($records); 49 | $records = $this->recordsByContainer($records); 50 | return $records; 51 | } 52 | 53 | protected function recordsByContainer(array $records): array 54 | { 55 | $recordsByContainer = []; 56 | foreach ($records as $record) { 57 | if ($record['tx_container_parent'] > 0) { 58 | if (empty($recordsByContainer[$record['tx_container_parent']])) { 59 | $recordsByContainer[$record['tx_container_parent']] = []; 60 | } 61 | $recordsByContainer[$record['tx_container_parent']][] = $record; 62 | } 63 | } 64 | return $recordsByContainer; 65 | } 66 | 67 | public function getContainerChildren(array $containerRecord, int $language): array 68 | { 69 | $pid = (int)$containerRecord['pid']; 70 | if (isset($containerRecord['t3ver_oid']) && $containerRecord['t3ver_oid'] > 0) { 71 | $defaultContainerRecord = $this->database->fetchOneRecord((int)$containerRecord['t3ver_oid']); 72 | $uid = (int)$defaultContainerRecord['uid']; 73 | } else { 74 | $uid = (int)$containerRecord['uid']; 75 | } 76 | if (!isset($this->records[$pid][$language])) { 77 | $this->records[$pid][$language] = $this->buildRecords($pid, $language); 78 | } 79 | if (empty($this->records[$pid][$language][$uid])) { 80 | return []; 81 | } 82 | return $this->records[$pid][$language][$uid]; 83 | } 84 | 85 | public function workspaceOverlay(array $records): array 86 | { 87 | $filtered = []; 88 | foreach ($records as $row) { 89 | BackendUtility::workspaceOL('tt_content', $row, $this->workspaceId, true); 90 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 12) { 91 | if ($row && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { 92 | $filtered[] = $row; 93 | } 94 | } else { 95 | if ($row && !VersionState::cast($row['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)) { 96 | $filtered[] = $row; 97 | } 98 | } 99 | } 100 | return $filtered; 101 | } 102 | 103 | public function containerRecordWorkspaceOverlay(array $record): ?array 104 | { 105 | BackendUtility::workspaceOL('tt_content', $record, $this->workspaceId, false); 106 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 12) { 107 | if ($record && VersionState::tryFrom($record['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { 108 | return $record; 109 | } 110 | } else { 111 | if ($record && !VersionState::cast($record['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)) { 112 | return $record; 113 | } 114 | } 115 | return null; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Container.php: -------------------------------------------------------------------------------- 1 | containerRecord = $containerRecord; 40 | $this->childRecords = $childRecords; 41 | $this->language = $language; 42 | } 43 | 44 | /** 45 | * @return int 46 | */ 47 | public function getUid(): int 48 | { 49 | return (int)$this->containerRecord['uid']; 50 | } 51 | 52 | public function getUidOfLiveWorkspace(): int 53 | { 54 | if (isset($this->containerRecord['t3ver_oid']) && $this->containerRecord['t3ver_oid'] > 0) { 55 | return (int)$this->containerRecord['t3ver_oid']; 56 | } 57 | return $this->getUid(); 58 | } 59 | 60 | /** 61 | * @return int 62 | */ 63 | public function getPid(): int 64 | { 65 | if (!empty($this->containerRecord['_ORIG_pid'])) { 66 | return (int)$this->containerRecord['_ORIG_pid']; 67 | } 68 | return (int)$this->containerRecord['pid']; 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function isConnectedMode(): bool 75 | { 76 | return (int)$this->containerRecord['sys_language_uid'] === 0; 77 | } 78 | 79 | /** 80 | * @return int 81 | */ 82 | public function getLanguage(): int 83 | { 84 | return $this->language; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function getCType(): string 91 | { 92 | return $this->containerRecord['CType']; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getContainerRecord(): array 99 | { 100 | return $this->containerRecord; 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | public function getChildRecords(): array 107 | { 108 | $childRecords = []; 109 | foreach ($this->childRecords as $colPos => $records) { 110 | $childRecords = array_merge($childRecords, $records); 111 | } 112 | return $childRecords; 113 | } 114 | 115 | /** 116 | * @param int $colPos 117 | * @return array 118 | */ 119 | public function getChildrenByColPos(int $colPos): array 120 | { 121 | if (empty($this->childRecords[$colPos])) { 122 | return []; 123 | } 124 | return $this->childRecords[$colPos]; 125 | } 126 | 127 | public function hasChildInColPos(int $colPos, int $childUid): bool 128 | { 129 | if (!isset($this->childRecords[$colPos])) { 130 | return false; 131 | } 132 | foreach ($this->childRecords[$colPos] as $childRecord) { 133 | if ((int)$childRecord['uid'] === $childUid) { 134 | return true; 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | /** 141 | * @return array 142 | */ 143 | public function getChildrenColPos(): array 144 | { 145 | return array_keys($this->childRecords); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Classes/Domain/Service/ContainerService.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 35 | $this->containerFactory = $containerFactory; 36 | } 37 | 38 | public function getNewContentElementAtTopTargetInColumn(Container $container, int $targetColPos): int 39 | { 40 | $target = -$container->getUid(); 41 | $previousRecord = null; 42 | $allColumns = $this->tcaRegistry->getAllAvailableColumnsColPos($container->getCType()); 43 | foreach ($allColumns as $colPos) { 44 | if ($colPos === $targetColPos && $previousRecord !== null) { 45 | $target = -(int)$previousRecord['uid']; 46 | } 47 | $children = $container->getChildrenByColPos($colPos); 48 | if (!empty($children)) { 49 | $last = array_pop($children); 50 | $previousRecord = $last; 51 | } 52 | } 53 | return $target; 54 | } 55 | 56 | public function getAfterContainerRecord(Container $container): array 57 | { 58 | $childRecords = $container->getChildRecords(); 59 | if (empty($childRecords)) { 60 | return $container->getContainerRecord(); 61 | } 62 | 63 | $lastChild = array_pop($childRecords); 64 | if (!$this->tcaRegistry->isContainerElement($lastChild['CType'])) { 65 | return $lastChild; 66 | } 67 | 68 | $container = $this->containerFactory->buildContainer((int)$lastChild['uid']); 69 | return $this->getAfterContainerRecord($container); 70 | } 71 | 72 | public function getAfterContainerElementTarget(Container $container): int 73 | { 74 | $target = $this->getAfterContainerRecord($container); 75 | 76 | return -$target['uid']; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Classes/Events/BeforeContainerConfigurationIsAppliedEvent.php: -------------------------------------------------------------------------------- 1 | containerConfiguration = $containerConfiguration; 25 | } 26 | 27 | public function skip(): void 28 | { 29 | $this->skip = true; 30 | } 31 | 32 | public function shouldBeSkipped(): bool 33 | { 34 | return $this->skip; 35 | } 36 | 37 | public function getContainerConfiguration(): ContainerConfiguration 38 | { 39 | return $this->containerConfiguration; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Events/BeforeContainerPreviewIsRenderedEvent.php: -------------------------------------------------------------------------------- 1 | container = $container; 33 | $this->view = $view; 34 | $this->grid = $grid; 35 | $this->item = $item; 36 | } 37 | 38 | public function getContainer(): Container 39 | { 40 | return $this->container; 41 | } 42 | 43 | public function getView(): StandaloneView 44 | { 45 | return $this->view; 46 | } 47 | 48 | public function getGrid(): Grid 49 | { 50 | return $this->grid; 51 | } 52 | 53 | public function getItem(): GridColumnItem 54 | { 55 | trigger_error('gridColumItem property will be removed on next major release', E_USER_DEPRECATED); 56 | return $this->item; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Classes/Exception.php: -------------------------------------------------------------------------------- 1 | database = $database; 28 | } 29 | 30 | public function processCmdmap_afterFinish(DataHandler $dataHandler): void 31 | { 32 | $cmdmap = $dataHandler->cmdmap; 33 | $copyMappingArray_merged = $dataHandler->copyMappingArray_merged; 34 | 35 | foreach ($cmdmap as $table => $incomingCmdArrayPerId) { 36 | if ($table !== 'tt_content') { 37 | continue; 38 | } 39 | foreach ($incomingCmdArrayPerId as $id => $incomingCmdArray) { 40 | if (!is_array($incomingCmdArray)) { 41 | continue; 42 | } 43 | if (empty($incomingCmdArray['copyToLanguage'])) { 44 | continue; 45 | } 46 | if (empty($copyMappingArray_merged['tt_content'][$id])) { 47 | continue; 48 | } 49 | $copyToLanguage = $incomingCmdArray['copyToLanguage']; 50 | $newId = $copyMappingArray_merged['tt_content'][$id]; 51 | $data = [ 52 | 'tt_content' => [], 53 | ]; 54 | // child in free mode is copied 55 | $child = $this->database->fetchOneRecord($newId); 56 | if ($child === null) { 57 | continue; 58 | } 59 | if ($child['tx_container_parent'] > 0) { 60 | $copiedFromChild = $this->database->fetchOneRecord($id); 61 | // copied from non default language (connectecd mode) children 62 | if ($copiedFromChild !== null && $copiedFromChild['sys_language_uid'] > 0 && $copiedFromChild['l18n_parent'] > 0) { 63 | // fetch orig container 64 | $origContainer = $this->database->fetchOneTranslatedRecordByLocalizationParent((int)$copiedFromChild['tx_container_parent'], $copiedFromChild['sys_language_uid']); 65 | // should never be null 66 | if ($origContainer !== null) { 67 | $freeModeContainer = $this->database->fetchContainerRecordLocalizedFreeMode((int)$origContainer['uid'], $copyToLanguage); 68 | if ($freeModeContainer !== null) { 69 | $data['tt_content'][$newId] = ['tx_container_parent' => (int)$freeModeContainer['uid']]; 70 | } 71 | } 72 | } else { 73 | $freeModeContainer = $this->database->fetchContainerRecordLocalizedFreeMode((int)$child['tx_container_parent'], $copyToLanguage); 74 | if ($freeModeContainer !== null) { 75 | $data['tt_content'][$newId] = ['tx_container_parent' => (int)$freeModeContainer['uid']]; 76 | } 77 | } 78 | } 79 | if (empty($data['tt_content'])) { 80 | continue; 81 | } 82 | $localDataHandler = GeneralUtility::makeInstance(DataHandler::class); 83 | $localDataHandler->enableLogging = $dataHandler->enableLogging; 84 | $localDataHandler->start($data, [], $dataHandler->BE_USER); 85 | $localDataHandler->process_datamap(); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Classes/Hooks/Datahandler/DatahandlerProcess.php: -------------------------------------------------------------------------------- 1 | containerInProcess, true); 24 | } 25 | 26 | public function startContainerProcess(int $containerId): void 27 | { 28 | if (!in_array($containerId, $this->containerInProcess, true)) { 29 | $this->containerInProcess[] = $containerId; 30 | } 31 | } 32 | 33 | public function endContainerProcess(int $containerId): void 34 | { 35 | if (in_array($containerId, $this->containerInProcess, true)) { 36 | $this->containerInProcess = array_diff($this->containerInProcess, [$containerId]); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/Hooks/Datahandler/DatamapBeforeStartHook.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 50 | $this->database = $database; 51 | $this->tcaRegistry = $tcaRegistry; 52 | $this->containerService = $containerService; 53 | } 54 | 55 | public function processDatamap_beforeStart(DataHandler $dataHandler): void 56 | { 57 | $dataHandler->datamap = $this->datamapForChildLocalizations($dataHandler->datamap); 58 | $dataHandler->datamap = $this->datamapForChildrenChangeContainerLanguage($dataHandler->datamap); 59 | } 60 | 61 | protected function datamapForChildLocalizations(array $datamap): array 62 | { 63 | $datamapForLocalizations = ['tt_content' => []]; 64 | if (!empty($datamap['tt_content'])) { 65 | foreach ($datamap['tt_content'] as $id => $data) { 66 | if (isset($data['colPos'])) { 67 | $record = $this->database->fetchOneRecord((int)$id); 68 | if ($record !== null && $record['sys_language_uid'] === 0) { 69 | $translations = $this->database->fetchOverlayRecords($record); 70 | foreach ($translations as $translation) { 71 | $datamapForLocalizations['tt_content'][$translation['uid']] = [ 72 | 'colPos' => $data['colPos'], 73 | ]; 74 | if (isset($data['tx_container_parent'])) { 75 | $datamapForLocalizations['tt_content'][$translation['uid']]['tx_container_parent'] = $data['tx_container_parent']; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | if (count($datamapForLocalizations['tt_content']) > 0) { 83 | $datamap['tt_content'] = array_replace($datamap['tt_content'], $datamapForLocalizations['tt_content']); 84 | } 85 | return $datamap; 86 | } 87 | 88 | protected function datamapForChildrenChangeContainerLanguage(array $datamap): array 89 | { 90 | $datamapForLocalizations = ['tt_content' => []]; 91 | if (!empty($datamap['tt_content'])) { 92 | foreach ($datamap['tt_content'] as $id => $data) { 93 | if (isset($data['sys_language_uid'])) { 94 | try { 95 | $container = $this->containerFactory->buildContainer((int)$id); 96 | $children = $container->getChildRecords(); 97 | foreach ($children as $child) { 98 | if ((int)$child['sys_language_uid'] !== (int)$data['sys_language_uid']) { 99 | $datamapForLocalizations['tt_content'][$child['uid']] = [ 100 | 'sys_language_uid' => $data['sys_language_uid'], 101 | ]; 102 | } 103 | } 104 | } catch (Exception $e) { 105 | // nothing todo 106 | } 107 | } 108 | } 109 | } 110 | if (count($datamapForLocalizations['tt_content']) > 0) { 111 | $datamap['tt_content'] = array_replace($datamap['tt_content'], $datamapForLocalizations['tt_content']); 112 | } 113 | return $datamap; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Classes/Hooks/Datahandler/DatamapPreProcessFieldArrayHook.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 52 | $this->database = $database; 53 | $this->tcaRegistry = $tcaRegistry; 54 | $this->containerService = $containerService; 55 | } 56 | 57 | protected function newElementAfterContainer(array $incomingFieldArray): array 58 | { 59 | if (isset($incomingFieldArray['sorting'])) { 60 | return $incomingFieldArray; 61 | } 62 | $record = $this->database->fetchOneRecord(-(int)$incomingFieldArray['pid']); 63 | if ($record === null) { 64 | // new elements in container have already correct target 65 | return $incomingFieldArray; 66 | } 67 | if ((int)$record['uid'] === (int)($incomingFieldArray['tx_container_parent'] ?? 0)) { 68 | return $incomingFieldArray; 69 | } 70 | if (!$this->tcaRegistry->isContainerElement($record['CType'])) { 71 | return $incomingFieldArray; 72 | } 73 | try { 74 | $container = $this->containerFactory->buildContainer((int)$record['uid']); 75 | if ($container->getLanguage() === 0 || !$container->isConnectedMode()) { 76 | $incomingFieldArray['pid'] = $this->containerService->getAfterContainerElementTarget($container); 77 | } 78 | } catch (Exception $e) { 79 | } 80 | return $incomingFieldArray; 81 | } 82 | 83 | protected function copyToLanguageElementInContainer(array $incomingFieldArray): array 84 | { 85 | if (!isset($incomingFieldArray['tx_container_parent']) || (int)$incomingFieldArray['tx_container_parent'] === 0) { 86 | return $incomingFieldArray; 87 | } 88 | if (!isset($incomingFieldArray['l10n_source']) || (int)$incomingFieldArray['l10n_source'] === 0) { 89 | return $incomingFieldArray; 90 | } 91 | if (!isset($incomingFieldArray['l18n_parent']) || (int)$incomingFieldArray['l18n_parent'] > 0) { 92 | return $incomingFieldArray; 93 | } 94 | if (!isset($incomingFieldArray['sys_language_uid']) || (int)$incomingFieldArray['sys_language_uid'] === 0) { 95 | return $incomingFieldArray; 96 | } 97 | $record = $this->database->fetchOneRecord(-$incomingFieldArray['pid']); 98 | $translatedContainerRecord = $this->database->fetchOneTranslatedRecordByl10nSource((int)$incomingFieldArray['tx_container_parent'], (int)$incomingFieldArray['sys_language_uid']); 99 | if ($record === null || $translatedContainerRecord === null) { 100 | return $incomingFieldArray; 101 | } 102 | try { 103 | $incomingFieldArray['tx_container_parent'] = $translatedContainerRecord['uid']; 104 | $container = $this->containerFactory->buildContainer((int)$translatedContainerRecord['uid']); 105 | if ((int)$record['sys_language_uid'] === 0 || empty($container->getChildrenByColPos((int)$incomingFieldArray['colPos']))) { 106 | $target = $this->containerService->getNewContentElementAtTopTargetInColumn($container, (int)$incomingFieldArray['colPos']); 107 | $incomingFieldArray['pid'] = $target; 108 | } 109 | } catch (Exception $e) { 110 | // not a container 111 | } 112 | return $incomingFieldArray; 113 | } 114 | 115 | public function processDatamap_preProcessFieldArray(array &$incomingFieldArray, string $table, $id, DataHandler $dataHandler): void 116 | { 117 | if ($table !== 'tt_content') { 118 | return; 119 | } 120 | if (MathUtility::canBeInterpretedAsInteger($id)) { 121 | return; 122 | } 123 | if (!isset($incomingFieldArray['pid']) || (int)$incomingFieldArray['pid'] >= 0) { 124 | return; 125 | } 126 | $incomingFieldArray = $this->newElementAfterContainer($incomingFieldArray); 127 | $incomingFieldArray = $this->copyToLanguageElementInContainer($incomingFieldArray); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Classes/Hooks/Datahandler/DeleteHook.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 31 | } 32 | 33 | public function processCmdmap_deleteAction(string $table, int $id, array $recordToDelete, bool $recordWasDeleted, DataHandler $dataHandler): void 34 | { 35 | if ($table === 'tt_content') { 36 | $this->deleteChildren($id, $dataHandler->BE_USER, $dataHandler->enableLogging); 37 | } 38 | } 39 | 40 | public function processCmdmap_discardAction(string $table, int $id, array $recordToDelete, bool $recordWasDeleted): void 41 | { 42 | if ($table === 'tt_content') { 43 | $this->deleteChildren($id, null); 44 | } 45 | } 46 | 47 | protected function deleteChildren(int $id, ?BackendUserAuthentication $backendUser, ?bool $enableLogging = null): void 48 | { 49 | try { 50 | $container = $this->containerFactory->buildContainer($id); 51 | $children = $container->getChildRecords(); 52 | $toDelete = []; 53 | foreach ($children as $colPos => $record) { 54 | $toDelete[$record['uid']] = ['delete' => 1]; 55 | } 56 | if (!empty($toDelete)) { 57 | $cmd = ['tt_content' => $toDelete]; 58 | $localDataHandler = GeneralUtility::makeInstance(DataHandler::class); 59 | if ($enableLogging !== null) { 60 | $localDataHandler->enableLogging = $enableLogging; 61 | } 62 | $localDataHandler->start([], $cmd, $backendUser); 63 | $localDataHandler->process_cmdmap(); 64 | } 65 | } catch (Exception $e) { 66 | // nothing todo 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Classes/Hooks/UsedRecords.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 35 | $this->tcaRegistry = $tcaRegistry; 36 | } 37 | 38 | public function addContainerChildren(array $params, PageLayoutView $pageLayoutView): bool 39 | { 40 | $record = $params['record']; 41 | 42 | if (isset($record['tx_container_parent']) && $record['tx_container_parent'] > 0) { 43 | try { 44 | $container = $this->containerFactory->buildContainer((int)$record['tx_container_parent']); 45 | $columns = $this->tcaRegistry->getAvailableColumns($container->getCType()); 46 | foreach ($columns as $column) { 47 | if ($column['colPos'] === (int)$record['colPos']) { 48 | if ($record['sys_language_uid'] > 0 && $container->isConnectedMode()) { 49 | return $container->hasChildInColPos((int)$record['colPos'], (int)$record['l18n_parent']); 50 | } 51 | return $container->hasChildInColPos((int)$record['colPos'], (int)$record['uid']); 52 | } 53 | } 54 | return false; 55 | } catch (Exception $e) { 56 | } 57 | } 58 | return $params['used']; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Hooks/WizardItems.php: -------------------------------------------------------------------------------- 1 | getParentIdFromRequest(); 23 | if ($parent !== null) { 24 | foreach ($wizardItems as $key => $wizardItem) { 25 | $wizardItems[$key]['tt_content_defValues']['tx_container_parent'] = $parent; 26 | if (!isset($wizardItems[$key]['params'])) { 27 | $wizardItems[$key]['params'] = '?defVals[tt_content][tx_container_parent]=' . $parent; 28 | } else { 29 | $wizardItems[$key]['params'] .= '&defVals[tt_content][tx_container_parent]=' . $parent; 30 | } 31 | } 32 | } 33 | } 34 | 35 | protected function getParentIdFromRequest(): ?int 36 | { 37 | $request = $this->getServerRequest(); 38 | if ($request === null) { 39 | return null; 40 | } 41 | $queryParams = $request->getQueryParams(); 42 | if (isset($queryParams['tx_container_parent']) && (int)$queryParams['tx_container_parent'] > 0) { 43 | return (int)$queryParams['tx_container_parent']; 44 | } 45 | return null; 46 | } 47 | 48 | protected function getServerRequest(): ?ServerRequest 49 | { 50 | return $GLOBALS['TYPO3_REQUEST'] ?? null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/ChildInTranslatedContainerError.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 41 | $this->containerRecord = $containerRecord; 42 | $this->errorMessage = self::IDENTIFIER . ': translated container child with uid ' . $childRecord['uid'] . 43 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 44 | ' has translated tx_container_parent ' . $containerRecord['uid'] 45 | . ' but should point to default language container record ' . $containerRecord['l18n_parent']; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getErrorMessage(): string 52 | { 53 | return $this->errorMessage; 54 | } 55 | 56 | /** 57 | * @return int 58 | */ 59 | public function getSeverity(): int 60 | { 61 | return ErrorInterface::ERROR; 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function getChildRecord(): array 68 | { 69 | return $this->childRecord; 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function getContainerRecord(): array 76 | { 77 | return $this->containerRecord; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/ErrorInterface.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 35 | $this->errorMessage = self::IDENTIFIER . ': container child with uid ' . $childRecord['uid'] . 36 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 37 | ' has non existing tx_container_parent ' . $childRecord['tx_container_parent']; 38 | } 39 | 40 | public function getChildRecord(): array 41 | { 42 | return $this->childRecord; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getErrorMessage(): string 49 | { 50 | return $this->errorMessage; 51 | } 52 | 53 | /** 54 | * @return int 55 | */ 56 | public function getSeverity(): int 57 | { 58 | return ErrorInterface::ERROR; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/UnusedColPosWarning.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 41 | $this->containerRecord = $containerRecord; 42 | $this->errorMessage = self::IDENTIFIER . ': container child with uid ' . $childRecord['uid'] . 43 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 44 | ' has invalid colPos ' . $childRecord['colPos'] 45 | . ' in container ' . $childRecord['tx_container_parent'] 46 | . ' with CType ' . $containerRecord['CType']; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getErrorMessage(): string 53 | { 54 | return $this->errorMessage; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getSeverity(): int 61 | { 62 | return ErrorInterface::WARNING; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/WrongL18nParentError.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 41 | $this->containerRecord = $containerRecord; 42 | $this->errorMessage = self::IDENTIFIER . ': container child with uid ' . $childRecord['uid'] . 43 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 44 | ' has l18n_parent ' . $childRecord['l18n_parent'] 45 | . ' but tx_container_parent ' . $childRecord['tx_container_parent'] 46 | . ' has l18n_parent ' . $containerRecord['l18n_parent'] . 47 | ' (page: ' . $containerRecord['pid'] . ' language: ' . $containerRecord['sys_language_uid'] . ')'; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getErrorMessage(): string 54 | { 55 | return $this->errorMessage; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getSeverity(): int 62 | { 63 | return ErrorInterface::ERROR; 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function getChildRecord(): array 70 | { 71 | return $this->childRecord; 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | public function getContainerRecord(): array 78 | { 79 | return $this->containerRecord; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/WrongLanguageWarning.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 41 | $this->containerRecord = $containerRecord; 42 | $this->errorMessage = self::IDENTIFIER . ': container child with uid ' . $childRecord['uid'] . 43 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 44 | ' has sys_language_uid ' . $childRecord['sys_language_uid'] 45 | . ' but tx_container_parent ' . $childRecord['tx_container_parent'] 46 | . ' has sys_language_uid ' . $containerRecord['sys_language_uid']; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getErrorMessage(): string 53 | { 54 | return $this->errorMessage; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getSeverity(): int 61 | { 62 | return ErrorInterface::WARNING; 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function getChildRecord(): array 69 | { 70 | return $this->childRecord; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getContainerRecord(): array 77 | { 78 | return $this->containerRecord; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/WrongParentError.php: -------------------------------------------------------------------------------- 1 | containerRecord = $containerRecord; 35 | $this->errorMessage = self::IDENTIFIER . ': container uid ' . $containerRecord['uid'] . 36 | ' (page: ' . $containerRecord['pid'] . ' language: ' . $containerRecord['sys_language_uid'] . ')' . 37 | ' has tx_container_parent ' . $containerRecord['tx_container_parent']; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getErrorMessage(): string 44 | { 45 | return $this->errorMessage; 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | public function getSeverity(): int 52 | { 53 | return ErrorInterface::ERROR; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getContainerRecord(): array 60 | { 61 | return $this->containerRecord; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/Integrity/Error/WrongPidError.php: -------------------------------------------------------------------------------- 1 | childRecord = $childRecord; 41 | $this->containerRecord = $containerRecord; 42 | $this->errorMessage = self::IDENTIFIER . ': container child with uid ' . $childRecord['uid'] . 43 | ' (page: ' . $childRecord['pid'] . ' language: ' . $childRecord['sys_language_uid'] . ')' . 44 | ' but tx_container_parent ' . $childRecord['tx_container_parent'] . 45 | ' has pid ' . $containerRecord['pid'] . ' language: ' . $containerRecord['sys_language_uid'] . ')'; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getErrorMessage(): string 52 | { 53 | return $this->errorMessage; 54 | } 55 | 56 | /** 57 | * @return int 58 | */ 59 | public function getSeverity(): int 60 | { 61 | return ErrorInterface::ERROR; 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function getChildRecord(): array 68 | { 69 | return $this->childRecord; 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function getContainerRecord(): array 76 | { 77 | return $this->containerRecord; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Classes/Integrity/IntegrityFix.php: -------------------------------------------------------------------------------- 1 | database = $database; 40 | $this->tcaRegistry = $tcaRegistry; 41 | } 42 | 43 | public function deleteChildrenWithWrongPid(WrongPidError $wrongPidError): void 44 | { 45 | $dataHandler = GeneralUtility::makeInstance(DataHandler::class); 46 | $dataHandler->enableLogging = false; 47 | $childRecord = $wrongPidError->getChildRecord(); 48 | $cmd = ['tt_content' => [$childRecord['uid'] => ['delete' => 1]]]; 49 | $dataHandler->start([], $cmd); 50 | $dataHandler->process_cmdmap(); 51 | } 52 | 53 | public function deleteChildrenWithNonExistingParent(NonExistingParentWarning $nonExistingParentWarning): void 54 | { 55 | $dataHandler = GeneralUtility::makeInstance(DataHandler::class); 56 | $dataHandler->enableLogging = false; 57 | $childRecord = $nonExistingParentWarning->getChildRecord(); 58 | $cmd = ['tt_content' => [$childRecord['uid'] => ['delete' => 1]]]; 59 | $dataHandler->start([], $cmd); 60 | $dataHandler->process_cmdmap(); 61 | } 62 | 63 | public function changeContainerParentToDefaultLanguageContainer(ChildInTranslatedContainerError $e): void 64 | { 65 | $translatedContainer = $e->getContainerRecord(); 66 | $child = $e->getChildRecord(); 67 | $l18nParentOfContainer = $translatedContainer['l18n_parent']; 68 | $queryBuilder = $this->database->getQueryBuilder(); 69 | $queryBuilder->update('tt_content') 70 | ->set('tx_container_parent', $l18nParentOfContainer) 71 | ->where( 72 | $queryBuilder->expr()->eq( 73 | 'uid', 74 | $queryBuilder->createNamedParameter($child['uid'], Connection::PARAM_INT) 75 | ) 76 | ) 77 | ->executeStatement(); 78 | } 79 | 80 | /** 81 | * @param WrongL18nParentError[] $errors 82 | */ 83 | public function languageMode(array $errors): void 84 | { 85 | $cTypes = $this->tcaRegistry->getRegisteredCTypes(); 86 | $defaultContainerRecords = $this->database->getContainerRecords($cTypes); 87 | $containerRecords = []; 88 | // uniq container records 89 | foreach ($errors as $error) { 90 | $containerRecord = $error->getContainerRecord(); 91 | $containerRecords[$containerRecord['uid']] = $containerRecord; 92 | } 93 | foreach ($containerRecords as $containerRecord) { 94 | if (!isset($defaultContainerRecords[$containerRecord['l18n_parent']])) { 95 | // should not happen 96 | continue; 97 | } 98 | $defaultContainerRecord = $defaultContainerRecords[$containerRecord['l18n_parent']]; 99 | $columns = $this->tcaRegistry->getAvailableColumns($defaultContainerRecord['CType']); 100 | foreach ($columns as $column) { 101 | $childRecords = $this->database->getChildrenByContainerAndColPos($containerRecord['uid'], (int)$column['colPos'], $containerRecord['sys_language_uid']); 102 | // some children may have corrent container parent set 103 | //$childRecords = array_merge($childRecords, $this->database->getChildrenByContainer($defaultContainerRecord['uid'], $containerRecord['sys_language_uid'])); 104 | $defaultChildRecords = $this->database->getChildrenByContainerAndColPos($defaultContainerRecord['uid'], (int)$column['colPos'], $defaultContainerRecord['sys_language_uid']); 105 | if (count($childRecords) <= count($defaultChildRecords)) { 106 | // connect children 107 | for ($i = 0; $i < count($childRecords); $i++) { 108 | $childRecord = $childRecords[$i]; 109 | $defaultChildRecord = $defaultChildRecords[$i]; 110 | $queryBuilder = $this->database->getQueryBuilder(); 111 | $stm = $queryBuilder->update('tt_content') 112 | ->set('tx_container_parent', $defaultContainerRecord['uid']) 113 | ->set('l18n_parent', $defaultChildRecord['uid']) 114 | ->where( 115 | $queryBuilder->expr()->eq( 116 | 'uid', 117 | $queryBuilder->createNamedParameter($childRecord['uid'], Connection::PARAM_INT) 118 | ) 119 | ); 120 | if ((int)$childRecord['l10n_source'] === 0) { 121 | // i think this is always true 122 | $stm->set('l10n_source', $defaultChildRecord['uid']); 123 | } 124 | $stm->executeStatement(); 125 | } 126 | } 127 | // disconnect container ? 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Classes/Integrity/SortingInPage.php: -------------------------------------------------------------------------------- 1 | database = $database; 50 | $this->tcaRegistry = $tcaRegistry; 51 | $this->containerFactory = $containerFactory; 52 | $this->containerService = $containerService; 53 | } 54 | 55 | public function run(bool $dryRun = true, bool $enableLogging = false, ?int $pid = null): array 56 | { 57 | $this->unsetContentDefenderConfiguration(); 58 | $dataHandler = GeneralUtility::makeInstance(DataHandler::class); 59 | $dataHandler->enableLogging = $enableLogging; 60 | $cTypes = $this->tcaRegistry->getRegisteredCTypes(); 61 | $containerUsedColPosArray = []; 62 | foreach ($cTypes as $cType) { 63 | $columns = $this->tcaRegistry->getAvailableColumns($cType); 64 | foreach ($columns as $column) { 65 | $containerUsedColPosArray[] = $column['colPos']; 66 | } 67 | } 68 | $rows = $this->database->getNonContainerChildrenPerColPos($containerUsedColPosArray, $pid); 69 | foreach ($rows as $recordsPerPageAndColPos) { 70 | $prevSorting = 0; 71 | $prevContainer = null; 72 | $prevChild = null; 73 | foreach ($recordsPerPageAndColPos as $record) { 74 | if (in_array($record['CType'], $cTypes, true)) { 75 | $container = $this->containerFactory->buildContainer($record['uid']); 76 | $lastChild = $this->containerService->getAfterContainerRecord($container); 77 | $sorting = $lastChild['sorting']; 78 | 79 | if ($record['uid'] !== $lastChild['uid']) { 80 | if ($prevChild === null || $prevContainer === null) { 81 | $prevChild = $lastChild; 82 | $prevContainer = $container; 83 | $prevSorting = $sorting; 84 | continue; 85 | } 86 | $containerSorting = $container->getContainerRecord()['sorting']; 87 | if ($containerSorting < $prevSorting) { 88 | $this->errors[] = 'record ' . $record['uid'] . ' (' . $record['sorting'] . ')' . 89 | ' on page ' . $record['pid'] . 90 | ' should be sorted after last child ' . $prevChild['uid'] . ' (' . $prevChild['sorting'] . ')' . 91 | ' of container ' . $prevContainer->getUid() . ' (' . $containerSorting . ')'; 92 | $this->moveRecordAfter((int)$record['uid'], $prevContainer->getUid(), $dryRun, $dataHandler); 93 | } 94 | $prevContainer = $container; 95 | $prevChild = $lastChild; 96 | } 97 | } else { 98 | $sorting = $record['sorting']; 99 | } 100 | $prevSorting = $sorting; 101 | } 102 | } 103 | return $this->errors; 104 | } 105 | 106 | protected function moveRecordAfter(int $recordUid, int $moveUid, bool $dryRun, DataHandler $dataHandler): void 107 | { 108 | if ($dryRun === false) { 109 | $cmdmap = [ 110 | 'tt_content' => [ 111 | $recordUid => [ 112 | 'move' => -1 * $moveUid, 113 | ], 114 | ], 115 | ]; 116 | $dataHandler->start([], $cmdmap); 117 | $dataHandler->process_datamap(); 118 | $dataHandler->process_cmdmap(); 119 | } 120 | } 121 | 122 | protected function unsetContentDefenderConfiguration(): void 123 | { 124 | // content_defender uses FormDataCompiler which expects a ServerRequest 125 | if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['content_defender'])) { 126 | unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['content_defender']); 127 | } 128 | if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['content_defender'])) { 129 | unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['content_defender']); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Classes/Listener/BootCompleted.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 28 | } 29 | 30 | public function __invoke(BootCompletedEvent $event): void 31 | { 32 | $this->tcaRegistry->registerIcons(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Listener/ContentUsedOnPage.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 35 | $this->tcaRegistry = $tcaRegistry; 36 | } 37 | 38 | public function __invoke(IsContentUsedOnPageLayoutEvent $event): void 39 | { 40 | $record = $event->getRecord(); 41 | if ($record['tx_container_parent'] > 0) { 42 | try { 43 | $container = $this->containerFactory->buildContainer((int)$record['tx_container_parent']); 44 | $columns = $this->tcaRegistry->getAvailableColumns($container->getCType()); 45 | foreach ($columns as $column) { 46 | if ($column['colPos'] === (int)$record['colPos']) { 47 | if ($record['sys_language_uid'] > 0 && $container->isConnectedMode()) { 48 | $used = $container->hasChildInColPos((int)$record['colPos'], (int)$record['l18n_parent']); 49 | $event->setUsed($used); 50 | return; 51 | } 52 | $used = $container->hasChildInColPos((int)$record['colPos'], (int)$record['uid']); 53 | $event->setUsed($used); 54 | return; 55 | } 56 | } 57 | } catch (Exception $e) { 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/Listener/LegacyPageTsConfig.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 30 | } 31 | 32 | public function __invoke(ModifyLoadedPageTsConfigEvent $event): void 33 | { 34 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() !== 11) { 35 | return; 36 | } 37 | $tsConfig = $event->getTsConfig(); 38 | $tsConfig['default'] = trim($this->tcaRegistry->getPageTsString() . "\n" . ($tsConfig['default'] ?? '')); 39 | $event->setTsConfig($tsConfig); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Listener/ModifyNewContentElementWizardItems.php: -------------------------------------------------------------------------------- 1 | getParentIdFromRequest(); 25 | if ($parent !== null) { 26 | $typo3Version = (GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion(); 27 | $wizardItems = $event->getWizardItems(); 28 | foreach ($wizardItems as $key => $wizardItem) { 29 | if ($typo3Version < 13) { 30 | $wizardItems[$key]['tt_content_defValues']['tx_container_parent'] = $parent; 31 | if (!isset($wizardItems[$key]['params'])) { 32 | $wizardItems[$key]['params'] = '?defVals[tt_content][tx_container_parent]=' . $parent; 33 | } else { 34 | $wizardItems[$key]['params'] .= '&defVals[tt_content][tx_container_parent]=' . $parent; 35 | } 36 | } else { 37 | $wizardItems[$key]['defaultValues']['tx_container_parent'] = $parent; 38 | } 39 | } 40 | $event->setWizardItems($wizardItems); 41 | } 42 | } 43 | 44 | protected function getParentIdFromRequest(): ?int 45 | { 46 | $request = $this->getServerRequest(); 47 | if ($request === null) { 48 | return null; 49 | } 50 | $queryParams = $request->getQueryParams(); 51 | if (isset($queryParams['tx_container_parent']) && (int)$queryParams['tx_container_parent'] > 0) { 52 | return (int)$queryParams['tx_container_parent']; 53 | } 54 | return null; 55 | } 56 | 57 | protected function getServerRequest(): ?ServerRequest 58 | { 59 | return $GLOBALS['TYPO3_REQUEST'] ?? null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/Listener/PageContentPreviewRendering.php: -------------------------------------------------------------------------------- 1 | gridRenderer = $gridRenderer; 27 | $this->tcaRegistry = $tcaRegistry; 28 | } 29 | 30 | public function __invoke(PageContentPreviewRenderingEvent $event): void 31 | { 32 | if ($event->getTable() !== 'tt_content') { 33 | return; 34 | } 35 | 36 | $record = $event->getRecord(); 37 | if (!$this->tcaRegistry->isContainerElement( (string) $record['CType'])) { 38 | return; 39 | } 40 | $record['tx_container_grid'] = $this->gridRenderer->renderGrid($record, $event->getPageLayoutContext()); 41 | $event->setRecord($record); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Listener/PageTsConfig.php: -------------------------------------------------------------------------------- 1 | tcaRegistry = $tcaRegistry; 30 | } 31 | 32 | public function __invoke(ModifyLoadedPageTsConfigEvent $event): void 33 | { 34 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() < 12) { 35 | return; 36 | } 37 | $tsConfig = $event->getTsConfig(); 38 | $tsConfig = array_merge(['pagesTsConfig-package-container' => $this->tcaRegistry->getPageTsString()], $tsConfig); 39 | $event->setTsConfig($tsConfig); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Listener/RecordSummaryForLocalization.php: -------------------------------------------------------------------------------- 1 | recordLocalizeSummaryModifier = $recordLocalizeSummaryModifier; 28 | } 29 | 30 | public function __invoke(AfterRecordSummaryForLocalizationEvent $event): void 31 | { 32 | $records = $event->getRecords(); 33 | $columns = $event->getColumns(); 34 | $records = $this->recordLocalizeSummaryModifier->filterRecords($records); 35 | $columns = $this->recordLocalizeSummaryModifier->rebuildColumns($columns); 36 | $event->setColumns($columns); 37 | $event->setRecords($records); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/Service/RecordLocalizeSummaryModifier.php: -------------------------------------------------------------------------------- 1 | containerRegistry = $containerRegistry; 33 | } 34 | 35 | public function rebuildPayload(array $payload): array 36 | { 37 | return [ 38 | 'records' => $this->filterRecords($payload['records']), 39 | 'columns' => $this->rebuildColumns($payload['columns']), 40 | ]; 41 | } 42 | 43 | public function filterRecords(array $recordsPerColPos): array 44 | { 45 | // cannot be done by event in v10 46 | $uids = []; 47 | foreach ($recordsPerColPos as $colPos => $records) { 48 | foreach ($records as $record) { 49 | $uids[] = $record['uid']; 50 | } 51 | } 52 | if (empty($uids)) { 53 | return $recordsPerColPos; 54 | } 55 | $containerUids = $this->getContainerUids($uids); 56 | if (empty($containerUids)) { 57 | return $recordsPerColPos; 58 | } 59 | $containerChildren = $this->getContainerChildren($uids); 60 | if (empty($containerChildren)) { 61 | return $recordsPerColPos; 62 | } 63 | // we have both: container to translate and container children to translate 64 | // unset all records in container to translate 65 | $filtered = []; 66 | foreach ($recordsPerColPos as $colPos => $records) { 67 | $filteredRecords = []; 68 | foreach ($records as $record) { 69 | if (empty($containerChildren[$record['uid']])) { 70 | $filteredRecords[] = $record; 71 | } else { 72 | $fullRecord = $containerChildren[$record['uid']]; 73 | if (!in_array($fullRecord['tx_container_parent'], $containerUids, true)) { 74 | $filteredRecords[] = $record; 75 | } 76 | } 77 | } 78 | if (!empty($filteredRecords)) { 79 | $filtered[$colPos] = $filteredRecords; 80 | } 81 | } 82 | return $filtered; 83 | } 84 | 85 | public function rebuildColumns(array $columns): array 86 | { 87 | // this can be done with AfterPageColumnsSelectedForLocalizationEvent event in v10 88 | $containerColumns = $this->containerRegistry->getAllAvailableColumns(); 89 | foreach ($containerColumns as $containerColumn) { 90 | $columns = [ 91 | 'columns' => array_replace([$containerColumn['colPos'] => 'Container Children (' . $containerColumn['colPos'] . ')'], $columns['columns']), 92 | 'columnList' => array_values(array_unique(array_merge([$containerColumn['colPos']], $columns['columnList']))), 93 | ]; 94 | } 95 | return $columns; 96 | } 97 | 98 | // database helper 99 | 100 | protected function getContainerUids(array $uids): array 101 | { 102 | $containerCTypes = $this->containerRegistry->getRegisteredCTypes(); 103 | if (empty($containerCTypes)) { 104 | return []; 105 | } 106 | $queryBuilder = $this->getQueryBuilder(); 107 | $rows = $queryBuilder->select('uid', 'l18n_parent') 108 | ->from('tt_content') 109 | ->where( 110 | $queryBuilder->expr()->in( 111 | 'uid', 112 | $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY) 113 | ), 114 | $queryBuilder->expr()->in( 115 | 'CType', 116 | $queryBuilder->createNamedParameter($containerCTypes, Connection::PARAM_STR_ARRAY) 117 | ) 118 | ) 119 | ->executeQuery() 120 | ->fetchAllAssociative(); 121 | $containerUids = []; 122 | foreach ($rows as $row) { 123 | $containerUids[] = $row['uid']; 124 | if ($row['l18n_parent'] > 0) { 125 | $containerUids[] = $row['l18n_parent']; 126 | } 127 | } 128 | return $containerUids; 129 | } 130 | 131 | protected function getContainerChildren(array $uids): array 132 | { 133 | $containerChildren = []; 134 | $queryBuilder = $this->getQueryBuilder(); 135 | $rows = $queryBuilder->select('*') 136 | ->from('tt_content') 137 | ->where( 138 | $queryBuilder->expr()->in( 139 | 'uid', 140 | $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY) 141 | ), 142 | $queryBuilder->expr()->neq( 143 | 'tx_container_parent', 144 | $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) 145 | ) 146 | ) 147 | ->executeQuery() 148 | ->fetchAllAssociative(); 149 | foreach ($rows as $row) { 150 | $containerChildren[$row['uid']] = $row; 151 | } 152 | return $containerChildren; 153 | } 154 | 155 | protected function getQueryBuilder(): QueryBuilder 156 | { 157 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); 158 | $queryBuilder->getRestrictions() 159 | ->removeAll() 160 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 161 | return $queryBuilder; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Classes/Tca/ItemProcFunc.php: -------------------------------------------------------------------------------- 1 | containerFactory = $containerFactory; 39 | $this->tcaRegistry = $tcaRegistry; 40 | $this->backendLayoutView = $backendLayoutView; 41 | } 42 | 43 | /** 44 | * Gets colPos items to be shown in the forms engine. 45 | * This method is called as "itemsProcFunc" with the accordant context 46 | * for tt_content.colPos. 47 | */ 48 | public function colPos(array &$parameters): void 49 | { 50 | $row = $parameters['row']; 51 | if (($row['tx_container_parent'] ?? 0) > 0) { 52 | try { 53 | $container = $this->containerFactory->buildContainer((int)$row['tx_container_parent']); 54 | $cType = $container->getCType(); 55 | $grid = $this->tcaRegistry->getGrid($cType); 56 | if (is_array($grid)) { 57 | $items = []; 58 | foreach ($grid as $rows) { 59 | foreach ($rows as $column) { 60 | // only one item is show, so it is not changeable 61 | if ((int)$column['colPos'] === (int)$row['colPos']) { 62 | $items[] = [ 63 | $column['name'], 64 | $column['colPos'], 65 | ]; 66 | } 67 | } 68 | } 69 | $parameters['items'] = $items; 70 | return; 71 | } 72 | } catch (Exception $e) { 73 | } 74 | } 75 | 76 | $this->backendLayoutView->colPosListItemProcFunc($parameters); 77 | } 78 | 79 | public function txContainerParent(array &$parameters): void 80 | { 81 | $row = $parameters['row']; 82 | $items = []; 83 | if (($row['tx_container_parent'] ?? 0) > 0) { 84 | try { 85 | $container = $this->containerFactory->buildContainer((int)$row['tx_container_parent']); 86 | $cType = $container->getCType(); 87 | $items[] = [ 88 | $cType, 89 | $row['tx_container_parent'], 90 | ]; 91 | } catch (Exception $e) { 92 | $items[] = [ 93 | '-', 94 | 0, 95 | ]; 96 | } 97 | } else { 98 | $items[] = [ 99 | '-', 100 | 0, 101 | ]; 102 | } 103 | $parameters['items'] = $items; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Classes/Updates/ContainerDeleteChildrenWithWrongPid.php: -------------------------------------------------------------------------------- 1 | integrity = $integrity; 49 | $this->integrityFix = $integrityFix; 50 | } 51 | 52 | public function getIdentifier(): string 53 | { 54 | return self::IDENTIFIER; 55 | } 56 | 57 | /** 58 | * @return string Title of this updater 59 | */ 60 | public function getTitle(): string 61 | { 62 | return 'EXT:container: Delete "container" children with wrong pid'; 63 | } 64 | 65 | /** 66 | * @return string Longer description of this updater 67 | */ 68 | public function getDescription(): string 69 | { 70 | return 'if you update from Version < 1.3 you may have children with wrong pid and they was never shown in BE/FE'; 71 | } 72 | 73 | public function updateNecessary(): bool 74 | { 75 | $res = $this->integrity->run(); 76 | foreach ($res['errors'] as $error) { 77 | if ($error instanceof WrongPidError) { 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | public function executeUpdate(): bool 85 | { 86 | if (Environment::isCli() === false) { 87 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 11) { 88 | $requestFactory = GeneralUtility::makeInstance(ServerRequestFactory::class); 89 | $request = $requestFactory::fromGlobals(); 90 | $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); 91 | Bootstrap::initializeBackendUser(BackendUserAuthentication::class, $request); 92 | } else { 93 | Bootstrap::initializeBackendUser(); 94 | } 95 | Bootstrap::initializeBackendAuthentication(); 96 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 97 | } 98 | $res = $this->integrity->run(); 99 | foreach ($res['errors'] as $error) { 100 | if ($error instanceof WrongPidError) { 101 | $this->integrityFix->deleteChildrenWithWrongPid($error); 102 | } 103 | } 104 | return true; 105 | } 106 | 107 | public function getPrerequisites(): array 108 | { 109 | return [ 110 | DatabaseUpdatedPrerequisite::class, 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Classes/Updates/ContainerMigrateSorting.php: -------------------------------------------------------------------------------- 1 | sorting = $sorting; 42 | } 43 | 44 | public function getIdentifier(): string 45 | { 46 | return self::IDENTIFIER; 47 | } 48 | 49 | /** 50 | * @return string Title of this updater 51 | */ 52 | public function getTitle(): string 53 | { 54 | return 'EXT:container: Migrate "container" sorting'; 55 | } 56 | 57 | /** 58 | * @return string Longer description of this updater 59 | */ 60 | public function getDescription(): string 61 | { 62 | return 'change sorting of container children (must be run multiple times for nested containers)'; 63 | } 64 | 65 | public function updateNecessary(): bool 66 | { 67 | $errors = $this->sorting->run(true); 68 | return !empty($errors); 69 | } 70 | 71 | public function executeUpdate(): bool 72 | { 73 | if (Environment::isCli() === false) { 74 | if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() > 11) { 75 | $requestFactory = GeneralUtility::makeInstance(ServerRequestFactory::class); 76 | $request = $requestFactory::fromGlobals(); 77 | $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); 78 | Bootstrap::initializeBackendUser(BackendUserAuthentication::class, $request); 79 | } else { 80 | Bootstrap::initializeBackendUser(); 81 | } 82 | Bootstrap::initializeBackendAuthentication(); 83 | $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromUserPreferences($GLOBALS['BE_USER']); 84 | } 85 | $this->sorting->run(false); 86 | return true; 87 | } 88 | 89 | public function getPrerequisites(): array 90 | { 91 | return [ 92 | DatabaseUpdatedPrerequisite::class, 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Xclasses/LocalizationController.php: -------------------------------------------------------------------------------- 1 | recordLocalizeSummaryModifier = $recordLocalizeSummaryModifier ?? GeneralUtility::makeInstance(RecordLocalizeSummaryModifier::class); 32 | } 33 | 34 | public function getRecordLocalizeSummary(ServerRequestInterface $request): ResponseInterface 35 | { 36 | $response = parent::getRecordLocalizeSummary($request); 37 | $payload = json_decode($response->getBody()->getContents(), true); 38 | $payload = $this->recordLocalizeSummaryModifier->rebuildPayload($payload); 39 | return new JsonResponse($payload); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Configuration/Icons.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 6 | 'source' => 'EXT:container/Resources/Public/Icons/container-1col.svg', 7 | ], 8 | 'container-2col' => [ 9 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 10 | 'source' => 'EXT:container/Resources/Public/Icons/container-2col.svg', 11 | ], 12 | 'container-2col-left' => [ 13 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 14 | 'source' => 'EXT:container/Resources/Public/Icons/container-2col-left.svg', 15 | ], 16 | 'container-2col-right' => [ 17 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 18 | 'source' => 'EXT:container/Resources/Public/Icons/container-2col-right.svg', 19 | ], 20 | 'container-3col' => [ 21 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 22 | 'source' => 'EXT:container/Resources/Public/Icons/container-3col.svg', 23 | ], 24 | 'container-4col' => [ 25 | 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, 26 | 'source' => 'EXT:container/Resources/Public/Icons/container-4col.svg', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /Configuration/JavaScriptModules.php: -------------------------------------------------------------------------------- 1 | getMajorVersion() < 13) { 4 | return [ 5 | 'imports' => [ 6 | '@typo3/backend/layout-module/drag-drop.js' => 'EXT:container/Resources/Public/JavaScript/Overrides12/drag-drop.js', 7 | '@typo3/backend/layout-module/paste.js' => 'EXT:container/Resources/Public/JavaScript/Overrides12/paste.js', 8 | ], 9 | ]; 10 | } 11 | return [ 12 | 'imports' => [ 13 | '@typo3/backend/layout-module/drag-drop.js' => 'EXT:container/Resources/Public/JavaScript/Overrides/drag-drop.js', 14 | '@typo3/backend/layout-module/paste.js' => 'EXT:container/Resources/Public/JavaScript/Overrides/paste.js', 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | B13\Container\: 8 | resource: '../Classes/*' 9 | exclude: 10 | - '../Classes/Domain/Model/*' 11 | - '../Classes/Backend/Grid/*' 12 | - '../Classes/Integrity/Error/*' 13 | - '../Classes/Tca/ContainerConfiguration.php' 14 | - '../Classes/**/Exception.php' 15 | 16 | B13\Container\Tca\Registry: 17 | public: true 18 | B13\Container\Backend\Preview\GridRenderer: 19 | arguments: 20 | $runtimeCache: '@cache.runtime' 21 | B13\Container\Backend\Preview\ContainerPreviewRenderer: 22 | public: true 23 | arguments: 24 | $runtimeCache: '@cache.runtime' 25 | B13\Container\Hooks\UsedRecords: 26 | public: true 27 | B13\Container\Hooks\Datahandler\CommandMapAfterFinishHook: 28 | public: true 29 | B13\Container\Hooks\Datahandler\CommandMapBeforeStartHook: 30 | public: true 31 | B13\Container\Hooks\Datahandler\CommandMapPostProcessingHook: 32 | public: true 33 | B13\Container\Hooks\Datahandler\DatamapBeforeStartHook: 34 | public: true 35 | B13\Container\Hooks\Datahandler\DatamapPreProcessFieldArrayHook: 36 | public: true 37 | B13\Container\Hooks\Datahandler\DeleteHook: 38 | public: true 39 | B13\Container\ContentDefender\Hooks\ColumnConfigurationManipulationHook: 40 | public: true 41 | B13\Container\DataProcessing\ContainerProcessor: 42 | public: true 43 | tags: 44 | - { name: 'data.processor', identifier: 'container' } 45 | B13\Container\Updates\ContainerMigrateSorting: 46 | public: true 47 | B13\Container\Updates\ContainerDeleteChildrenWithWrongPid: 48 | public: true 49 | B13\Container\Tca\ItemProcFunc: 50 | public: true 51 | 52 | B13\Container\Listener\RecordSummaryForLocalization: 53 | tags: 54 | - name: event.listener 55 | identifier: 'tx-container-record-summary-for-localization' 56 | B13\Container\Listener\ContentUsedOnPage: 57 | tags: 58 | - name: event.listener 59 | identifier: 'tx-container-content-used-on-page' 60 | B13\Container\Listener\ModifyNewContentElementWizardItems: 61 | tags: 62 | - name: event.listener 63 | identifier: 'tx-container-new-content-element-wizard' 64 | B13\Container\Listener\LegacyPageTsConfig: 65 | tags: 66 | - name: event.listener 67 | identifier: 'tx-container-legacy-page-ts-config' 68 | B13\Container\Listener\PageTsConfig: 69 | tags: 70 | - name: event.listener 71 | identifier: 'tx-container-page-ts-config' 72 | B13\Container\Listener\BootCompleted: 73 | tags: 74 | - name: event.listener 75 | identifier: 'tx-container-boot-completed' 76 | B13\Container\Listener\PageContentPreviewRendering: 77 | tags: 78 | - name: event.listener 79 | identifier: 'tx-container-page-content-preview-rendering' 80 | before: 'typo3-backend/fluid-preview/content' 81 | B13\Container\Command\FixLanguageModeCommand: 82 | tags: 83 | - name: 'console.command' 84 | command: 'container:fixLanguageMode' 85 | schedulable: false 86 | description: connect children of connected container if possible, else disconnect container 87 | B13\Container\Command\FixContainerParentForConnectedModeCommand: 88 | tags: 89 | - name: 'console.command' 90 | command: 'container:fixContainerParentForConnectedMode' 91 | schedulable: false 92 | description: tx_container_parent of children in connected mode should point to default language container 93 | B13\Container\Command\DeleteChildrenWithWrongPidCommand: 94 | tags: 95 | - name: 'console.command' 96 | command: 'container:deleteChildrenWithWrongPid' 97 | schedulable: false 98 | description: delete all child records with pid neq containers pid 99 | B13\Container\Command\DeleteChildrenWithNonExistingParentCommand: 100 | tags: 101 | - name: 'console.command' 102 | command: 'container:deleteChildrenWithNonExistingParent' 103 | schedulable: false 104 | description: delete all child records with a non existing parent record (they are displayed as unsued) 105 | B13\Container\Command\IntegrityCommand: 106 | tags: 107 | - name: 'console.command' 108 | command: 'container:integrity' 109 | schedulable: true 110 | description: Checks integrity of containers 111 | B13\Container\Command\SortingCommand: 112 | tags: 113 | - name: 'console.command' 114 | command: 'container:sorting' 115 | schedulable: false 116 | description: Resort Content Elements 117 | B13\Container\Command\SortingInPageCommand: 118 | tags: 119 | - name: 'console.command' 120 | command: 'container:sorting-in-page' 121 | schedulable: false 122 | description: Resort Content Elements 123 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/tt_content.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'label' => 'LLL:EXT:container/Resources/Private/Language/locallang.xlf:container', 9 | 'config' => [ 10 | 'default' => 0, 11 | 'type' => 'select', 12 | 'foreign_table' => 'tt_content', 13 | // if not set, all content elements are already loaded before itemsProcFunc is called 14 | 'foreign_table_where' => ' AND 1=2', 15 | 'itemsProcFunc' => \B13\Container\Tca\ItemProcFunc::class . '->txContainerParent', 16 | 'renderType' => 'selectSingle', 17 | ], 18 | ], 19 | ]; 20 | 21 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns( 22 | 'tt_content', 23 | $additionalColumns 24 | ); 25 | 26 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToPalette( 27 | 'tt_content', 28 | 'general', 29 | 'tx_container_parent' 30 | ); 31 | 32 | $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['container'] = 'LLL:EXT:container/Resources/Private/Language/locallang.xlf:container'; 33 | 34 | $GLOBALS['TCA']['tt_content']['columns']['colPos']['config']['itemsProcFunc'] = \B13\Container\Tca\ItemProcFunc::class . '->colPos'; 35 | 36 | // copyAfterDuplFields colPos,sys_language_uid 37 | // useColumnsForDefaultValues colPos,sys_language_uid,CType 38 | // new element 39 | $GLOBALS['TCA']['tt_content']['ctrl']['useColumnsForDefaultValues'] .= ',tx_container_parent'; 40 | }); 41 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Container 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/Private/Partials/PageLayout/Grid/Column.html: -------------------------------------------------------------------------------- 1 | 2 | Styling requires the colpos to be set to the string 'unused'. To preserve type safety in the 3 | controller, the string is only used in the template by setting the below "colpos" variable. 4 | 5 | 6 | 7 | 15 | 16 | 17 |
21 | 22 | 23 | 24 |
25 |
26 | {column.afterSectionMarkup} 27 | 28 | -------------------------------------------------------------------------------- /Resources/Private/Partials/PageLayout/Grid/ColumnHeader.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | {column.title} 10 |
11 | 12 | 13 | 14 | 15 | {column.titleUnassigned} 16 | 17 | 18 | {column.titleInaccessible} 19 | 20 |
21 |
22 | {column.beforeSectionMarkup} 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 |
43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /Resources/Private/Partials/PageLayout/Record.html: -------------------------------------------------------------------------------- 1 | {f:if(condition: '{item.disabled} && {item.context.drawingConfiguration.showHidden} == 0', then: 'display: none;') -> f:variable(name: 'style')} 2 |
10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 |
42 |
43 | 44 |
45 | -------------------------------------------------------------------------------- /Resources/Private/Partials11/PageLayout/Grid/Column.html: -------------------------------------------------------------------------------- 1 | 4 | remove class t3-grid-cell (for tests) 5 | change data-colpos attr 6 | 12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 |
20 | {column.title} 21 |
22 | 23 | {column.title} ({column.titleUnassigned}) 24 | 25 | 26 | {column.titleInaccessible} 27 | 28 | 29 | {column.titleInaccessible} 30 | 31 |
32 |
33 | use column.allowNewContent instead of pageContext 34 | 35 |
36 | 54 |
55 |
56 |
57 | 58 |
59 |
Empty Colpos
60 |
61 |
62 |
63 |
64 | This column has no "colPos". This is only for display Purposes. 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | change data-colpos attr 73 |
76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /Resources/Private/Partials11/PageLayout/Record.html: -------------------------------------------------------------------------------- 1 | 4 | {f:if(condition: '{item.disabled} && {item.context.drawingConfiguration.showHidden} == 0', then: 'display: none;') -> f:variable(name: 'style')} 5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | use column.allowNewContent instead of pageContext 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /Resources/Private/Partials12/PageLayout/Grid/Column.html: -------------------------------------------------------------------------------- 1 | 2 | Styling requires the colpos to be set to the string 'unused'. To preserve type safety in the 3 | controller, the string is only used in the template by setting the below "colpos" variable. 4 | 5 | 6 | 7 | 15 | 16 | 17 |
21 | 22 | 23 | 24 |
25 |
26 | {column.afterSectionMarkup} 27 | 28 | -------------------------------------------------------------------------------- /Resources/Private/Partials12/PageLayout/Grid/ColumnHeader.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | {column.title} 10 |
11 | 12 | 13 | 14 | 15 | {column.titleUnassigned} 16 | 17 | 18 | {column.titleInaccessible} 19 | 20 |
21 |
22 | {column.beforeSectionMarkup} 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /Resources/Private/Partials12/PageLayout/Record.html: -------------------------------------------------------------------------------- 1 | {f:if(condition: '{item.disabled} && {item.context.drawingConfiguration.showHidden} == 0', then: 'display: none;') -> f:variable(name: 'style')} 2 |
10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Container.html: -------------------------------------------------------------------------------- 1 | 5 | {tx_container_grid} 6 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Grid.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | EXT:container 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-1col.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-2col-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-2col-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-2col.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-3col.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Public/Icons/container-4col.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Overrides/paste.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the TYPO3 CMS project. 3 | * 4 | * It is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License, either version 2 6 | * of the License, or any later version. 7 | * 8 | * For the full copyright and license information, please read the 9 | * LICENSE.txt file that was distributed with this source code. 10 | * 11 | * The TYPO3 project - inspiring people to share! 12 | */ 13 | import DocumentService from"@typo3/core/document-service.js";import DataHandler from"@typo3/backend/ajax-data-handler.js";import{default as Modal}from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import RegularEvent from"@typo3/core/event/regular-event.js";class Paste{constructor(t){this.itemOnClipboardUid=0,this.itemOnClipboardTitle="",this.copyMode="",this.elementIdentifier=".t3js-page-ce",this.pasteAfterLinkTemplate="",this.pasteIntoLinkTemplate="",this.itemOnClipboardUid=t.itemOnClipboardUid,this.itemOnClipboardTitle=t.itemOnClipboardTitle,this.copyMode=t.copyMode,DocumentService.ready().then((()=>{document.querySelectorAll(".t3js-page-columns").length>0&&(this.generateButtonTemplates(),this.activatePasteIcons(),this.initializeEvents())}))}static determineColumn(t){const e=t.closest("[data-colpos]");return parseInt(e?.dataset?.colpos??"0",10)}static determineTxContainerParent(t){const e=t.closest("[data-tx-container-parent]");return parseInt(e?.dataset?.txContainerParent??"0",10)}initializeEvents(){new RegularEvent("click",((t,e)=>{t.preventDefault(),this.activatePasteModal(e)})).delegateTo(document,".t3js-paste")}generateButtonTemplates(){this.itemOnClipboardUid&&(this.pasteAfterLinkTemplate='',this.pasteIntoLinkTemplate='')}activatePasteIcons(){this.pasteAfterLinkTemplate&&this.pasteIntoLinkTemplate&&document.querySelectorAll(".t3js-page-new-ce").forEach((t=>{const e=t.parentElement.dataset.page?this.pasteIntoLinkTemplate:this.pasteAfterLinkTemplate;t.append(document.createRange().createContextualFragment(e))}))}activatePasteModal(t){const e=(TYPO3.lang["paste.modal.title.paste"]||"Paste record")+': "'+this.itemOnClipboardTitle+'"',a=TYPO3.lang["paste.modal.paste"]||"Do you want to paste the record to this position?";let n=[];n=[{text:TYPO3.lang["paste.modal.button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",trigger:(t,e)=>e.hideModal()},{text:TYPO3.lang["paste.modal.button.paste"]||"Paste",btnClass:"btn-"+Severity.getCssClass(SeverityEnum.warning),trigger:(e,a)=>{a.hideModal(),this.execute(t)}}],Modal.show(e,a,SeverityEnum.warning,n)}execute(t){const e=Paste.determineColumn(t),a=Paste.determineTxContainerParent(t),n=t.closest(this.elementIdentifier),s=n.dataset.uid;let o;o=void 0===s?parseInt(n.dataset.page,10):0-parseInt(s,10);const i={CB:{paste:"tt_content|"+o,pad:"normal",update:{colPos:e,sys_language_uid:parseInt(t.closest("[data-language-uid]").dataset.languageUid,10),tx_container_parent:a}}};DataHandler.process(i).then((t=>{t.hasErrors||window.location.reload()}))}}export default Paste; -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Overrides12/drag-drop.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the TYPO3 CMS project. 3 | * 4 | * It is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License, either version 2 6 | * of the License, or any later version. 7 | * 8 | * For the full copyright and license information, please read the 9 | * LICENSE.txt file that was distributed with this source code. 10 | * 11 | * The TYPO3 project - inspiring people to share! 12 | */ 13 | import interact from"interactjs";import DocumentService from"@typo3/core/document-service.js";import DataHandler from"@typo3/backend/ajax-data-handler.js";import Icons from"@typo3/backend/icons.js";import RegularEvent from"@typo3/core/event/regular-event.js";class DragDrop{constructor(){DocumentService.ready().then((()=>{DragDrop.initialize()}))}static initialize(){const e=document.querySelector(".module");new RegularEvent("wheel",(t=>{e.scrollLeft+=t.deltaX,e.scrollTop+=t.deltaY})).delegateTo(document,".draggable-dragging"),interact(DragDrop.draggableContentIdentifier).draggable({allowFrom:DragDrop.draggableContentHandleIdentifier,onstart:DragDrop.onDragStart,onmove:DragDrop.onDragMove,onend:DragDrop.onDragEnd}).pointerEvents({allowFrom:DragDrop.draggableContentHandleIdentifier}).on("move",(function(e){const t=e.interaction,a=e.currentTarget;if(t.pointerIsDown&&!t.interacting()&&"false"!=a.getAttribute("clone")){const r=a.cloneNode(!0);r.setAttribute("data-dragdrop-clone","true"),a.parentNode.insertBefore(r,a.nextSibling),t.start({name:"drag"},e.interactable,a)}})),interact(DragDrop.dropZoneIdentifier).dropzone({accept:this.draggableContentIdentifier,ondrop:DragDrop.onDrop,checker:(e,t,a,r,o)=>{const n=o.getBoundingClientRect();return t.pageX>=n.left&&t.pageX<=n.left+n.width&&t.pageY>=n.top&&t.pageY<=n.top+n.height}}).on("dragenter",(e=>{e.target.classList.add(DragDrop.dropPossibleHoverClass)})).on("dragleave",(e=>{e.target.classList.remove(DragDrop.dropPossibleHoverClass)}))}static onDragStart(e){e.target.dataset.dragStartX=(e.client.x-e.rect.left).toString(),e.target.dataset.dragStartY=(e.client.y-e.rect.top).toString(),e.target.style.width=getComputedStyle(e.target).getPropertyValue("width"),e.target.classList.add("draggable-dragging");const t=document.createElement("div");t.classList.add("draggable-copy-message"),t.textContent=TYPO3.lang["dragdrop.copy.message"],e.target.append(t),e.target.closest(DragDrop.columnIdentifier).classList.remove("active"),e.target.querySelector(DragDrop.dropZoneIdentifier).hidden=!0,document.querySelectorAll(DragDrop.dropZoneIdentifier).forEach((e=>{const t=e.parentElement.querySelector(DragDrop.addContentIdentifier);null!==t&&(t.hidden=!0,e.classList.add(DragDrop.validDropZoneClass))}))}static onDragMove(e){const t=document.querySelector(".module");e.target.style.left=e.client.x-parseInt(e.target.dataset.dragStartX,10)+"px",e.target.style.top=e.client.y-parseInt(e.target.dataset.dragStartY,10)+"px",e.delta.x<0&&e.pageX-20<0?t.scrollLeft-=20:e.delta.x>0&&e.pageX+20>t.offsetWidth&&(t.scrollLeft+=20),e.delta.y<0&&e.pageY-20-document.querySelector(".t3js-module-docheader").clientHeight<0?t.scrollTop-=20:e.delta.y>0&&e.pageY+20>t.offsetHeight&&(t.scrollTop+=20)}static onDragEnd(e){e.target.dataset.dragStartX="",e.target.dataset.dragStartY="",e.target.classList.remove("draggable-dragging"),e.target.style.width="unset",e.target.style.left="unset",e.target.style.top="unset",e.target.closest(DragDrop.columnIdentifier).classList.add("active"),e.target.querySelector(DragDrop.dropZoneIdentifier).hidden=!1,e.target.querySelector(".draggable-copy-message").remove(),document.querySelectorAll(DragDrop.dropZoneIdentifier+"."+DragDrop.validDropZoneClass).forEach((e=>{const t=e.parentElement.querySelector(DragDrop.addContentIdentifier);null!==t&&(t.hidden=!1),e.classList.remove(DragDrop.validDropZoneClass)})),document.querySelectorAll(DragDrop.draggableContentCloneIdentifier).forEach((e=>{e.remove()}))}static onDrop(e){const t=e.target,a=e.relatedTarget,r=DragDrop.getColumnPositionForElement(t),o=DragDrop.getTxContainerParentPositionForElement(t),n=parseInt(a.dataset.uid,10);if("number"==typeof n&&n>0){const s={},d=t.closest(DragDrop.contentIdentifier).dataset.uid;let l;l=void 0===d?parseInt(t.closest("[data-page]").dataset.page,10):0-parseInt(d,10);let g=parseInt(a.dataset.languageUid,10);-1!==g&&(g=parseInt(t.closest("[data-language-uid]").dataset.languageUid,10));let i=0,c=0;0!==l&&(i=r,c=o);const p=e.dragEvent.ctrlKey||t.classList.contains("t3js-paste-copy"),D=p?"copy":"move";s.cmd={tt_content:{[n]:{[D]:{action:"paste",target:l,update:{colPos:i,sys_language_uid:g,tx_container_parent:c}}}}},DragDrop.ajaxAction(t,a,s,p).then((()=>{const e=document.querySelector(`.t3-page-column-lang-name[data-language-uid="${g}"]`);if(null===e)return;const t=e.dataset.flagIdentifier,r=e.dataset.languageTitle;Icons.getIcon(t,Icons.sizes.small).then((e=>{const t=a.querySelector(".t3js-flag");t.title=r,t.innerHTML=e}))}))}}static ajaxAction(e,t,a,r){const o=Object.keys(a.cmd).shift(),n=parseInt(Object.keys(a.cmd[o]).shift(),10),s={component:"dragdrop",action:r?"copy":"move",table:o,uid:n},d=document.querySelector(".t3-grid-container");return DataHandler.process(a,s).then((a=>{if(a.hasErrors)throw a.messages;e.parentElement.classList.contains(DragDrop.contentIdentifier.substring(1))?e.closest(DragDrop.contentIdentifier).after(t):e.closest(DragDrop.dropZoneIdentifier).after(t),(r||"1"===d?.dataset.defaultLanguageBinding)&&self.location.reload()}))}static getColumnPositionForElement(e){const t=e.closest("[data-colpos]");return null!==t&&void 0!==t.dataset.colpos&&parseInt(t.dataset.colpos,10)}static getTxContainerParentPositionForElement(e){const t=e.closest("[data-tx-container-parent]");return null!==t&&void 0!==t.dataset.txContainerParent?parseInt(t.dataset.txContainerParent,10):0}}DragDrop.contentIdentifier=".t3js-page-ce",DragDrop.draggableContentIdentifier=".t3js-page-ce-sortable",DragDrop.draggableContentHandleIdentifier=".t3js-page-ce-draghandle",DragDrop.draggableContentCloneIdentifier="[data-dragdrop-clone]",DragDrop.dropZoneIdentifier=".t3js-page-ce-dropzone-available",DragDrop.columnIdentifier=".t3js-page-column",DragDrop.validDropZoneClass="active",DragDrop.dropPossibleHoverClass="t3-page-ce-dropzone-possible",DragDrop.addContentIdentifier=".t3js-page-new-ce";export default new DragDrop; -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Overrides12/paste.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the TYPO3 CMS project. 3 | * 4 | * It is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU General Public License, either version 2 6 | * of the License, or any later version. 7 | * 8 | * For the full copyright and license information, please read the 9 | * LICENSE.txt file that was distributed with this source code. 10 | * 11 | * The TYPO3 project - inspiring people to share! 12 | */ 13 | import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import DataHandler from"@typo3/backend/ajax-data-handler.js";import{default as Modal}from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import"@typo3/backend/element/icon-element.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";class Paste{constructor(t){this.itemOnClipboardUid=0,this.itemOnClipboardTitle="",this.copyMode="",this.elementIdentifier=".t3js-page-ce",this.pasteAfterLinkTemplate="",this.pasteIntoLinkTemplate="",this.itemOnClipboardUid=t.itemOnClipboardUid,this.itemOnClipboardTitle=t.itemOnClipboardTitle,this.copyMode=t.copyMode,DocumentService.ready().then((()=>{$(".t3js-page-columns").length&&(this.generateButtonTemplates(),this.activatePasteIcons(),this.initializeEvents())}))}static determineColumn(t){const e=t.closest("[data-colpos]");return e.length&&"undefined"!==e.data("colpos")?e.data("colpos"):0}static determineTxContainerParent(t){const e=t.closest("[data-tx-container-parent]");return e.length&&"undefined"!==e.data("txContainerParent")?e.data("txContainerParent"):0}initializeEvents(){$(document).on("click",".t3js-paste",(t=>{t.preventDefault(),this.activatePasteModal($(t.currentTarget))}))}generateButtonTemplates(){this.itemOnClipboardUid&&(this.pasteAfterLinkTemplate='',this.pasteIntoLinkTemplate='')}activatePasteIcons(){this.pasteAfterLinkTemplate&&this.pasteIntoLinkTemplate&&document.querySelectorAll(".t3js-page-new-ce").forEach((t=>{const e=t.parentElement.dataset.page?this.pasteIntoLinkTemplate:this.pasteAfterLinkTemplate;t.append(document.createRange().createContextualFragment(e))}))}activatePasteModal(t){const e=(TYPO3.lang["paste.modal.title.paste"]||"Paste record")+': "'+this.itemOnClipboardTitle+'"',a=TYPO3.lang["paste.modal.paste"]||"Do you want to paste the record to this position?";let n=[];n=[{text:TYPO3.lang["paste.modal.button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",trigger:(t,e)=>e.hideModal()},{text:TYPO3.lang["paste.modal.button.paste"]||"Paste",btnClass:"btn-"+Severity.getCssClass(SeverityEnum.warning),trigger:(e,a)=>{a.hideModal(),this.execute(t)}}],Modal.show(e,a,SeverityEnum.warning,n)}execute(t){const e=Paste.determineColumn(t),a=Paste.determineTxContainerParent(t),n=t.closest(this.elementIdentifier),s=n.data("uid");let o;o=void 0===s?parseInt(n.data("page"),10):0-parseInt(s,10);const i={CB:{paste:"tt_content|"+o,pad:"normal",update:{colPos:e,sys_language_uid:parseInt(t.closest("[data-language-uid]").data("language-uid"),10),tx_container_parent:a}}};DataHandler.process(i).then((t=>{t.hasErrors||window.location.reload()}))}}export default Paste; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b13/container", 3 | "description": "Create Custom Container Content Elements for TYPO3", 4 | "type": "typo3-cms-extension", 5 | "homepage": "https://b13.com", 6 | "license": ["GPL-2.0-or-later"], 7 | "require": { 8 | "typo3/cms-backend": "^11.5 || ^12.4 || ^13.4 || 14.0.x-dev" 9 | }, 10 | "autoload": { 11 | "psr-4": { 12 | "B13\\Container\\": "Classes/" 13 | } 14 | }, 15 | "minimum-stability": "dev", 16 | "prefer-stable": true, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "B13\\Container\\Tests\\": "Tests", 20 | "TYPO3\\JsonResponse\\": ".Build/Web/typo3conf/ext/json_response/Classes" 21 | } 22 | }, 23 | "config": { 24 | "vendor-dir": ".Build/vendor", 25 | "bin-dir": ".Build/bin", 26 | "allow-plugins": { 27 | "typo3/class-alias-loader": true, 28 | "typo3/cms-composer-installers": true 29 | } 30 | }, 31 | "require-dev": { 32 | "b13/container-example": "dev-master", 33 | "typo3/cms-install": "^11.5 || ^12.4 || ^13.4 || 14.0.x-dev", 34 | "typo3/cms-fluid-styled-content": "^11.5 || ^12.4 || ^13.4 || 14.0.x-dev", 35 | "typo3/cms-info": "^11.5 || ^12.4 || ^13.4 || 14.0.x-dev", 36 | "typo3/cms-workspaces": "^11.5 || ^12.4 || ^13.4 || 14.0.x-dev", 37 | "typo3/testing-framework": "^7.1.1 || ^8.2.7 || ^9.1", 38 | "phpstan/phpstan": "^1.10", 39 | "typo3/coding-standards": "^0.5.5", 40 | "friendsofphp/php-cs-fixer": "^3.51", 41 | "codeception/codeception": "^4.1 || ^5.1", 42 | "codeception/module-asserts": "^1.0 || ^3.0", 43 | "codeception/module-webdriver": "^1.0 || ^4.0", 44 | "codeception/module-db": "^1.0 || ^3.1", 45 | "phpunit/phpunit": "9.6 || ^10.5 || ^11.3" 46 | }, 47 | "replace": { 48 | "typo3-ter/container": "self.version" 49 | }, 50 | "scripts": { 51 | "prepare-tests": [ 52 | "if [ ! -e .Build/Web/typo3conf/ext/container_example -a -e .Build/Web/typo3conf/ext ]; then cd .Build/Web/typo3conf/ext && ln -s ../../../vendor/b13/container-example container_example && cd -; fi", 53 | "if [ ! -e .Build/Web/typo3conf/ext/content_defender -a -e .Build/vendor/ichhabrecht/content-defender -a -e .Build/Web/typo3conf/ext ]; then cd .Build/Web/typo3conf/ext && ln -s ../../../vendor/ichhabrecht/content-defender content_defender && cd -; fi", 54 | "if [ ! -e .Build/Web/typo3conf/sites -a -e .Build/Web/typo3conf ]; then cd .Build/Web/typo3conf && ln -s ../../../Build/sites && cd -; fi" 55 | ] 56 | }, 57 | "extra": { 58 | "typo3/cms": { 59 | "cms-package-dir": "{$vendor-dir}/typo3/cms", 60 | "web-dir": ".Build/Web", 61 | "app-dir": ".Build", 62 | "extension-key": "container" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Container Content Elements', 5 | 'description' => 'Create Custom Container Content Elements for TYPO3', 6 | 'category' => 'misc', 7 | 'author' => 'b13 GmbH', 8 | 'author_email' => 'typo3@b13.com', 9 | 'author_company' => 'b13 GmbH', 10 | 'state' => 'stable', 11 | 'uploadfolder' => false, 12 | 'createDirs' => '', 13 | 'clearCacheOnLoad' => true, 14 | 'version' => '3.1.10', 15 | 'constraints' => [ 16 | 'depends' => ['typo3' => '11.5.0-13.99.99'], 17 | 'conflicts' => [], 18 | 'suggests' => [], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | getMajorVersion() < 12) { 9 | // remove container colPos from "unused" page-elements (v12: IsContentUsedOnPageLayoutEvent) 10 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['record_is_used']['tx_container'] = 11 | \B13\Container\Hooks\UsedRecords::class . '->addContainerChildren'; 12 | // add tx_container_parent parameter to wizard items (v12: ModifyNewContentElementWizardItemsEvent) 13 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook']['tx_container'] = 14 | \B13\Container\Hooks\WizardItems::class; 15 | // LocalizationController Xclass (v12: AfterRecordSummaryForLocalizationEvent) 16 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Backend\Controller\Page\LocalizationController::class] = [ 17 | 'className' => \B13\Container\Xclasses\LocalizationController::class, 18 | ]; 19 | } 20 | 21 | $commandMapHooks = [ 22 | 'tx_container-post-process' => \B13\Container\Hooks\Datahandler\CommandMapPostProcessingHook::class, 23 | 'tx_container-before-start' => \B13\Container\Hooks\Datahandler\CommandMapBeforeStartHook::class, 24 | 'tx_container-delete' => \B13\Container\Hooks\Datahandler\DeleteHook::class, 25 | 'tx_container-after-finish' => \B13\Container\Hooks\Datahandler\CommandMapAfterFinishHook::class, 26 | ]; 27 | 28 | $datamapHooks = [ 29 | 'tx_container-before-start' => \B13\Container\Hooks\Datahandler\DatamapBeforeStartHook::class, 30 | 'tx_container-pre-process-field-array' => \B13\Container\Hooks\Datahandler\DatamapPreProcessFieldArrayHook::class, 31 | ]; 32 | 33 | // EXT:content_defender 34 | $packageManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Package\PackageManager::class); 35 | if ($packageManager->isPackageActive('content_defender')) { 36 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['content_defender']['ColumnConfigurationManipulationHook']['tx_container'] = 37 | \B13\Container\ContentDefender\Hooks\ColumnConfigurationManipulationHook::class; 38 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\IchHabRecht\ContentDefender\Hooks\DatamapDataHandlerHook::class] = [ 39 | 'className' => \B13\Container\ContentDefender\Xclasses\DatamapHook::class, 40 | ]; 41 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\IchHabRecht\ContentDefender\Hooks\CmdmapDataHandlerHook::class] = [ 42 | 'className' => \B13\Container\ContentDefender\Xclasses\CommandMapHook::class, 43 | ]; 44 | } 45 | 46 | // set our hooks at the beginning of Datamap Hooks 47 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] = array_merge( 48 | $commandMapHooks, 49 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'] ?? [] 50 | ); 51 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] = array_merge( 52 | $datamapHooks, 53 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] 54 | ); 55 | 56 | if ($packageManager->isPackageActive('typo3/cms-install') && $typo3Version->getMajorVersion() < 12) { 57 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][B13\Container\Updates\ContainerMigrateSorting::IDENTIFIER] 58 | = B13\Container\Updates\ContainerMigrateSorting::class; 59 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][B13\Container\Updates\ContainerDeleteChildrenWithWrongPid::IDENTIFIER] 60 | = B13\Container\Updates\ContainerDeleteChildrenWithWrongPid::class; 61 | } 62 | 63 | if(TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(TYPO3\CMS\Core\Information\Typo3Version::class)->getMajorVersion() < 12) { 64 | $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1729106358] = \B13\Container\Backend\ContextMenu\RecordContextMenuItemProvider::class; 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /ext_tables.sql: -------------------------------------------------------------------------------- 1 | # 2 | # Table structure for table 'tt_content' 3 | # 4 | CREATE TABLE tt_content ( 5 | tx_container_parent int(11) DEFAULT '0' NOT NULL, 6 | KEY container_parent (tx_container_parent) 7 | ); 8 | --------------------------------------------------------------------------------