diff --git a/Classes/Command/SortingInPageCommand.php b/Classes/Command/SortingInPageCommand.php new file mode 100644 index 00000000..a75445f6 --- /dev/null +++ b/Classes/Command/SortingInPageCommand.php @@ -0,0 +1,68 @@ +addArgument('pid', InputArgument::OPTIONAL, 'limit to this pid', 0); + $this->addOption('apply', null, InputOption::VALUE_NONE, 'apply migration'); + $this->addOption( + 'enable-logging', + null, + InputOption::VALUE_NONE, + 'enables datahandler logging, should only use for debug issues, not in production' + ); + } + + public function __construct(SortingInPage $sorting, string $name = null) + { + parent::__construct($name); + $this->sorting = $sorting; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $dryrun = $input->getOption('apply') !== true; + $pid = (int)$input->getArgument('pid'); + + Bootstrap::initializeBackendAuthentication(); + Bootstrap::initializeLanguageObject(); + $errors = $this->sorting->run( + $dryrun, + $input->getOption('enable-logging'), + $pid + ); + foreach ($errors as $error) { + $output->writeln($error); + } + if (empty($errors)) { + $output->writeln('migration finished'); + } + return 0; + } +} diff --git a/Classes/Integrity/Database.php b/Classes/Integrity/Database.php index 23bb75b6..ecb1d8b4 100644 --- a/Classes/Integrity/Database.php +++ b/Classes/Integrity/Database.php @@ -129,6 +129,54 @@ public function getChildrenByContainerAndColPos(int $containerId, int $colPos, i return (array)$stm->fetchAllAssociative(); } + public function getNonContainerChildrenPerColPos(array $containerUsedColPosArray, ?int $pid = null): array + { + $queryBuilder = $this->getQueryBuilder(); + $stm = $queryBuilder + ->select(...$this->fields) + ->from('tt_content') + ->where( + $queryBuilder->expr()->notIn( + 'colPos', + $queryBuilder->createNamedParameter($containerUsedColPosArray, Connection::PARAM_INT_ARRAY) + ), + $queryBuilder->expr()->eq( + 'sys_language_uid', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ) + ); + if (!empty($pid)) { + $stm->andWhere( + $queryBuilder->expr()->eq( + 'pid', + $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT) + ) + ); + } + $stm->orderBy('pid'); + $stm->addOrderBy('colPos'); + $stm->addOrderBy('sorting'); + if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() >= 12) { + $stm = $stm->executeQuery(); + } else { + $stm = $stm->execute(); + } + if ((GeneralUtility::makeInstance(Typo3Version::class))->getMajorVersion() === 10) { + $results = $stm->fetchAll(); + } else { + $results = $stm->fetchAllAssociative(); + } + $rows = []; + foreach ($results as $result) { + $key = $result['pid'] . '-' . $result['colPos']; + if (!isset($rows[$key])) { + $rows[$key] = []; + } + $rows[$key][$result['uid']] = $result; + } + return $rows; + } + public function getContainerRecords(array $cTypes, ?int $pid = null): array { $queryBuilder = $this->getQueryBuilder(); diff --git a/Classes/Integrity/SortingInPage.php b/Classes/Integrity/SortingInPage.php new file mode 100644 index 00000000..39f48868 --- /dev/null +++ b/Classes/Integrity/SortingInPage.php @@ -0,0 +1,135 @@ +database = $database; + $this->tcaRegistry = $tcaRegistry; + $this->containerFactory = $containerFactory; + $this->containerService = $containerService; + } + + public function run(bool $dryRun = true, bool $enableLogging = false, ?int $pid = null): array + { + $this->unsetContentDefenderConfiguration(); + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->enableLogging = $enableLogging; + $cTypes = $this->tcaRegistry->getRegisteredCTypes(); + $containerUsedColPosArray = []; + foreach ($cTypes as $cType) { + $columns = $this->tcaRegistry->getAvailableColumns($cType); + foreach ($columns as $column) { + $containerUsedColPosArray[] = $column['colPos']; + } + } + $rows = $this->database->getNonContainerChildrenPerColPos($containerUsedColPosArray, $pid); + foreach ($rows as $recordsPerPageAndColPos) { + $prevSorting = 0; + $prevContainer = null; + $prevChild = null; + foreach ($recordsPerPageAndColPos as $record) { + if (in_array($record['CType'], $cTypes, true)) { + $container = $this->containerFactory->buildContainer($record['uid']); + $children = $container->getChildRecords(); + if (empty($children)) { + $sorting = $record['sorting']; + } else { + $lastChild = array_pop($children); + $sorting = $lastChild['sorting']; + + if ($prevChild === null || $prevContainer === null) { + $prevChild = $lastChild; + $prevContainer = $container; + $prevSorting = $sorting; + continue; + } + $containerSorting = $container->getContainerRecord()['sorting']; + if ($containerSorting < $prevSorting) { + $this->errors[] = 'record ' . $record['uid'] . ' (' . $record['sorting'] . ')' . + ' on page ' . $record['pid'] . + ' should be sorted after last child ' . $prevChild['uid'] . ' (' . $prevChild['sorting'] . ')' . + ' of container ' . $prevContainer->getUid() . ' (' . $containerSorting . ')'; + $this->moveRecordAfter((int)$record['uid'], $prevContainer->getUid(), $dryRun, $dataHandler); + } + $prevContainer = $container; + $prevChild = $lastChild; + } + } else { + $sorting = $record['sorting']; + } + $prevSorting = $sorting; + } + } + return $this->errors; + } + + protected function moveRecordAfter(int $recordUid, int $moveUid, bool $dryRun, DataHandler $dataHandler): void + { + if ($dryRun === false) { + $cmdmap = [ + 'tt_content' => [ + $recordUid => [ + 'move' => -1 * $moveUid, + ], + ], + ]; + $dataHandler->start([], $cmdmap); + $dataHandler->process_datamap(); + $dataHandler->process_cmdmap(); + } + } + + protected function unsetContentDefenderConfiguration(): void + { + // content_defender uses FormDataCompiler which expects a ServerRequest + if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['content_defender'])) { + unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['content_defender']); + } + if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['content_defender'])) { + unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['content_defender']); + } + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 93ea2915..07db4eda 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -107,3 +107,9 @@ services: command: 'container:sorting' schedulable: false description: Resort Content Elements + B13\Container\Command\SortingInPageCommand: + tags: + - name: 'console.command' + command: 'container:sorting-in-page' + schedulable: false + description: Resort Content Elements \ No newline at end of file diff --git a/README.md b/README.md index 2dca63f7..1d9e6380 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,9 @@ vendor/bin/typo3 container:sorting # Fix the sorting of container children on page 123 vendor/bin/typo3 container:sorting --apply 123 +# Check the sorting of records in page colPos +vendor/bin/typo3 container:sorting-in-page + # ?? bin/typo3 container:fixLanguageMode bin/typo3 container:fixContainerParentForConnectedMode diff --git a/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container.csv b/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container.csv new file mode 100644 index 00000000..4267d89d --- /dev/null +++ b/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid" +,1,0 +"tt_content" +,"uid","pid","colPos","CType","sorting","tx_container_parent" +,1,1,0,"b13-2cols-with-header-container",1, +,2,1,0,"b13-2cols-with-header-container",2, +,3,1,202,,4,1 +,4,1,202,,3,2 \ No newline at end of file diff --git a/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container_with_changed_children_sorting.csv b/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container_with_changed_children_sorting.csv new file mode 100644 index 00000000..f9c6a1fa --- /dev/null +++ b/Tests/Functional/Integrity/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container_with_changed_children_sorting.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid" +,1,0 +"tt_content" +,"uid","pid","colPos","CType","sorting","tx_container_parent" +,1,1,0,"b13-2cols-with-header-container",1, +,2,1,0,"b13-2cols-with-header-container",2, +,3,1,202,,3,1 +,4,1,202,,4,2 \ No newline at end of file diff --git a/Tests/Functional/Integrity/Fixtures/SortingInPage/correct_sorted.csv b/Tests/Functional/Integrity/Fixtures/SortingInPage/correct_sorted.csv new file mode 100644 index 00000000..6405d8fc --- /dev/null +++ b/Tests/Functional/Integrity/Fixtures/SortingInPage/correct_sorted.csv @@ -0,0 +1,9 @@ +"pages" +,"uid","pid" +,1,0 +"tt_content" +,"uid","pid","colPos","CType","sorting","tx_container_parent" +,1,1,0,"b13-2cols-with-header-container",1, +,2,1,0,"b13-2cols-with-header-container",3, +,3,1,202,,2,1 +,4,1,202,,4,2 \ No newline at end of file diff --git a/Tests/Functional/Integrity/SortingInPageTest.php b/Tests/Functional/Integrity/SortingInPageTest.php new file mode 100644 index 00000000..6cd120d3 --- /dev/null +++ b/Tests/Functional/Integrity/SortingInPageTest.php @@ -0,0 +1,104 @@ +importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $GLOBALS['BE_USER'] = $this->setUpBackendUser(1); + Bootstrap::initializeLanguageObject(); + $context = GeneralUtility::makeInstance(Context::class); + $containerRegistry = GeneralUtility::makeInstance(Registry::class); + $sortingDatabase = GeneralUtility::makeInstance(Database::class); + $factoryDatabase = GeneralUtility::makeInstance(\B13\Container\Domain\Factory\Database::class, $context); + $containerFactory = GeneralUtility::makeInstance(ContainerFactory::class, $factoryDatabase, $containerRegistry, $context); + $containerService = GeneralUtility::makeInstance(ContainerService::class, $containerRegistry, $containerFactory); + $this->sorting = GeneralUtility::makeInstance(SortingInPage::class, $sortingDatabase, $containerRegistry, $containerFactory, $containerService); + } + + /** + * @test + */ + public function containerIsSortedAfterChildOfPreviousContainer(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container.csv'); + $errors = $this->sorting->run(false); + self::assertTrue(count($errors) === 1, 'should get one error'); + $rows = $this->getContentsByUid(); + self::assertTrue($rows[2]['sorting'] > $rows[3]['sorting'], 'container should be sorted after child of previous container'); + } + + /** + * @test + */ + public function containerIsSortedAfterChildOfPreviousContainerWithChangedChildrenSorting(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SortingInPage/container_is_sorted_before_child_of_previous_container_with_changed_children_sorting.csv'); + $errors = $this->sorting->run(false); + self::assertTrue(count($errors) === 1, 'should get one error'); + $rows = $this->getContentsByUid(); + self::assertTrue($rows[2]['sorting'] > $rows[3]['sorting'], 'container should be sorted after child of previous container'); + } + + /** + * @test + */ + public function nothingDoneForAlreadyCorrectSorted(): void + { + $this->importCSVDataSet(__DIR__ . '/Fixtures/SortingInPage/correct_sorted.csv'); + $errors = $this->sorting->run(); + self::assertTrue(count($errors) === 0, 'should get no error'); + } + + protected function getContentsByUid(): array + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); + $res = $queryBuilder->select('uid', 'sorting', 'colPos') + ->from('tt_content') + ->orderBy('sorting') + ->execute() + ->fetchAllAssociative(); + $rows = []; + foreach ($res as $row) { + $rows[$row['uid']] = $row; + } + return $rows; + } +} diff --git a/Tests/Functional/Integrity/SortingTest.php b/Tests/Functional/Integrity/SortingTest.php index 177d270c..1f2801eb 100644 --- a/Tests/Functional/Integrity/SortingTest.php +++ b/Tests/Functional/Integrity/SortingTest.php @@ -26,7 +26,7 @@ class SortingTest extends FunctionalTestCase { /** - * @var sorting + * @var Sorting */ protected $sorting; diff --git a/Tests/Functional/Integrity/SortingWithContentDefenderTest.php b/Tests/Functional/Integrity/SortingWithContentDefenderTest.php index 36fddeb6..1fb5f6c7 100644 --- a/Tests/Functional/Integrity/SortingWithContentDefenderTest.php +++ b/Tests/Functional/Integrity/SortingWithContentDefenderTest.php @@ -35,7 +35,7 @@ class SortingWithContentDefenderTest extends FunctionalTestCase ]; /** - * @var sorting + * @var Sorting */ protected $sorting;