2 declare(strict_types = 1);
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Symfony\Component\Console\Style\SymfonyStyle;
51 'all_versioned_records' => [],
54 'published_versions' => [],
58 'versions_in_live' => [],
61 'invalid_workspace' => []
70 ->setDescription(
'Find all versioned records and possibly cleans up invalid records in the database.')
71 ->setHelp(
'Traverse page tree and find versioned records. Also list all versioned records, additionally with some inconsistencies in the database, which can cleaned up with the "action" option. If you want to get more detailed information, use the --verbose option.')
75 InputOption::VALUE_REQUIRED,
76 'Setting start page in page tree. Default is the page tree root, 0 (zero)'
81 InputOption::VALUE_REQUIRED,
82 'Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.'
87 InputOption::VALUE_NONE,
88 'If this option is set, the records will not actually be deleted/modified, but just the output which records would be touched are shown'
93 InputOption::VALUE_OPTIONAL,
94 'Specify which action should be taken. Set it to "versions_in_live", "published_versions", "invalid_workspace" or "unused_placeholders"'
104 protected function execute(InputInterface $input, OutputInterface
$output)
109 $io =
new SymfonyStyle($input,
$output);
110 $io->title($this->getDescription());
123 if ($input->hasOption(
'action') && !empty($input->getOption(
'action'))) {
124 $action = $input->getOption(
'action');
128 $dryRun = $input->hasOption(
'dry-run') && $input->getOption(
'dry-run') !=
false ? true :
false;
130 if ($io->isVerbose()) {
131 $io->section(
'Searching the database now for versioned records.');
139 foreach ($this->foundRecords as $kk => $vv) {
140 foreach ($this->foundRecords[$kk] as $tables => $recArrays) {
141 ksort($this->foundRecords[$kk][$tables]);
143 ksort($this->foundRecords[$kk]);
156 if (!$io->isQuiet()) {
157 $numberOfVersionedRecords = 0;
158 foreach ($this->foundRecords[
'all_versioned_records'] as $records) {
159 $numberOfVersionedRecords += count($records);
162 $io->section(
'Found ' . $numberOfVersionedRecords .
' versioned records in the database.');
163 if ($io->isVeryVerbose()) {
164 foreach ($this->foundRecords[
'all_versioned_records'] as $table => $records) {
165 $io->writeln(
'Table "' . $table .
'"');
166 $io->listing($records);
170 $numberOfPublishedVersions = 0;
171 foreach ($this->foundRecords[
'published_versions'] as $records) {
172 $numberOfPublishedVersions += count($records);
174 $io->section(
'Found ' . $numberOfPublishedVersions .
' versioned records that have been published.');
175 if ($io->isVeryVerbose()) {
176 foreach ($this->foundRecords[
'published_versions'] as $table => $records) {
177 $io->writeln(
'Table "' . $table .
'"');
178 $io->listing($records);
182 $numberOfVersionsInLiveWorkspace = 0;
183 foreach ($this->foundRecords[
'versions_in_live'] as $records) {
184 $numberOfVersionsInLiveWorkspace += count($records);
186 $io->section(
'Found ' . $numberOfVersionsInLiveWorkspace .
' versioned records that are in the live workspace.');
187 if ($io->isVeryVerbose()) {
188 foreach ($this->foundRecords[
'versions_in_live'] as $table => $records) {
189 $io->writeln(
'Table "' . $table .
'"');
190 $io->listing($records);
194 $numberOfVersionsWithInvalidWorkspace = 0;
195 foreach ($this->foundRecords[
'invalid_workspace'] as $records) {
196 $numberOfVersionsWithInvalidWorkspace += count($records);
198 $io->section(
'Found ' . $numberOfVersionsWithInvalidWorkspace .
' versioned records with an invalid workspace.');
199 if ($io->isVeryVerbose()) {
200 foreach ($this->foundRecords[
'invalid_workspace'] as $table => $records) {
201 $io->writeln(
'Table "' . $table .
'"');
202 $io->listing($records);
206 $io->section(
'Found ' . count($unusedPlaceholders) .
' unused placeholder records.');
207 if ($io->isVeryVerbose()) {
208 $io->listing(array_keys($unusedPlaceholders));
211 $io->section(
'Found ' . count($invalidMovePlaceholders) .
' invalid move placeholders.');
212 if ($io->isVeryVerbose()) {
213 $io->listing($invalidMovePlaceholders);
216 $io->section(
'Found ' . count($recordsWithInvalidMoveIds) .
' versions with an invalid move ID.');
217 if ($io->isVeryVerbose()) {
218 $io->listing($recordsWithInvalidMoveIds);
227 case 'versions_in_live':
228 $io->section(
'Deleting versioned records in live workspace now. ' . ($dryRun ?
' (Not deleting now, just a dry run)' :
''));
229 $this->
deleteRecords($this->foundRecords[
'versions_in_live'], $dryRun, $io);
234 case 'published_versions':
235 $io->section(
'Deleting published records in live workspace now. ' . ($dryRun ?
' (Not deleting now, just a dry run)' :
''));
236 $this->
deleteRecords($this->foundRecords[
'published_versions'], $dryRun, $io);
241 case 'invalid_workspace':
242 $io->section(
'Moving versions in invalid workspaces to live workspace now. ' . ($dryRun ?
' (Not deleting now, just a dry run)' :
''));
248 case 'unused_placeholders':
249 $io->section(
'Deleting unused placeholder records now. ' . ($dryRun ?
' (Not deleting now, just a dry run)' :
''));
254 $io->note(
'No action specified, just displaying statistics. See --action option for details.');
257 $io->success(
'All done!');
270 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(
'pages');
271 $queryBuilder->getRestrictions()->removeAll();
273 $pageRecord = $queryBuilder
281 ->where($queryBuilder->expr()->eq(
'uid', $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)))
286 if ($rootIsVersion) {
287 $workspaceId = (int)$pageRecord[
't3ver_wsid'];
288 $this->foundRecords[
'all_versioned_records'][
'pages'][$rootID] = $rootID;
290 if ($pageRecord[
't3ver_count'] >= 1 && $workspaceId === 0) {
291 $this->foundRecords[
'published_versions'][
'pages'][$rootID] = $rootID;
294 if ($workspaceId === 0) {
295 $this->foundRecords[
'versions_in_live'][
'pages'][$rootID] = $rootID;
298 if (!isset($this->allWorkspaces[$workspaceId])) {
299 $this->foundRecords[
'invalid_workspace'][
'pages'][$rootID] = $rootID;
303 if (!$isInsideVersionedPage) {
306 foreach ($tableNames as $tableName) {
307 if ($tableName !==
'pages') {
309 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
310 ->getQueryBuilderForTable($tableName);
312 $queryBuilder->getRestrictions()->removeAll();
314 $result = $queryBuilder
318 $queryBuilder->expr()->eq(
320 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
324 while ($rowSub = $result->fetch()) {
327 if (is_array($versions)) {
328 foreach ($versions as $verRec) {
329 if (!$verRec[
'_CURRENT_VERSION']) {
331 $this->foundRecords[
'all_versioned_records'][$tableName][$verRec[
'uid']] = $verRec[
'uid'];
332 $workspaceId = (int)$verRec[
't3ver_wsid'];
333 if ($verRec[
't3ver_count'] >= 1 && $workspaceId === 0) {
336 $this->foundRecords[
'published_versions'][$tableName][$verRec[
'uid']] = $verRec[
'uid'];
338 if ($workspaceId === 0) {
339 $this->foundRecords[
'versions_in_live'][$tableName][$verRec[
'uid']] = $verRec[
'uid'];
341 if (!isset($this->allWorkspaces[$workspaceId])) {
342 $this->foundRecords[
'invalid_workspace'][$tableName][$verRec[
'uid']] = $verRec[
'uid'];
354 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
355 ->getQueryBuilderForTable(
'pages');
357 $queryBuilder->getRestrictions()->removeAll();
358 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
364 $queryBuilder->expr()->eq(
366 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
369 ->orderBy(
'sorting');
371 $result = $queryBuilder->execute();
372 while ($row = $result->fetch()) {
379 if (is_array($versions)) {
380 foreach ($versions as $verRec) {
381 if (!$verRec[
'_CURRENT_VERSION']) {
396 $unusedPlaceholders = [];
398 foreach ($tableNames as $table) {
399 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
400 ->getQueryBuilderForTable($table);
402 $queryBuilder->getRestrictions()
404 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
406 $result = $queryBuilder
407 ->select(
'uid',
'pid')
410 $queryBuilder->expr()->gte(
'pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
411 $queryBuilder->expr()->eq(
413 $queryBuilder->createNamedParameter(
421 while ($placeholderRecord = $result->fetch()) {
423 if (count($versions) <= 1) {
424 $unusedPlaceholders[$table .
':' . $placeholderRecord[
'uid']] = [
426 'uid' => $placeholderRecord[
'uid']
431 ksort($unusedPlaceholders);
432 return $unusedPlaceholders;
443 $invalidMovePlaceholders = [];
445 foreach ($tableNames as $table) {
446 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
447 ->getQueryBuilderForTable($table);
449 $queryBuilder->getRestrictions()
451 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
453 $result = $queryBuilder
454 ->select(
'uid',
'pid',
't3ver_move_id',
't3ver_wsid',
't3ver_state')
457 $queryBuilder->expr()->gte(
'pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
458 $queryBuilder->expr()->eq(
460 $queryBuilder->createNamedParameter(
467 while ($placeholderRecord = $result->fetch()) {
468 $shortID = GeneralUtility::shortMD5($table .
':' . $placeholderRecord[
'uid']);
469 if ((
int)$placeholderRecord[
't3ver_wsid'] !== 0) {
470 $phrecCopy = $placeholderRecord;
474 $invalidMovePlaceholders[$shortID] = $table .
':' . $placeholderRecord[
'uid'] .
' - State for version was not "4" as it should be!';
477 $invalidMovePlaceholders[$shortID] = $table .
':' . $placeholderRecord[
'uid'] .
' - No version was found for online record to be moved. A version must exist.';
480 $invalidMovePlaceholders[$shortID] = $table .
':' . $placeholderRecord[
'uid'] .
' - Did not find online record for "t3ver_move_id" value ' . $placeholderRecord[
't3ver_move_id'];
483 $invalidMovePlaceholders[$shortID] = $table .
':' . $placeholderRecord[
'uid'] .
' - Placeholder was not assigned a workspace value in t3ver_wsid.';
487 ksort($invalidMovePlaceholders);
488 return $invalidMovePlaceholders;
501 foreach ($tableNames as $table) {
502 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
503 ->getQueryBuilderForTable($table);
505 $queryBuilder->getRestrictions()
507 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
509 $result = $queryBuilder
510 ->select(
'uid',
'pid',
't3ver_move_id',
't3ver_wsid',
't3ver_state')
513 $queryBuilder->expr()->neq(
515 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
520 while ($placeholderRecord = $result->fetch()) {
522 if ((
int)$placeholderRecord[
'pid'] === -1) {
523 $records[] = $table .
':' . $placeholderRecord[
'uid'] .
' - Record was offline, must not be!';
526 $records[] = $table .
':' . $placeholderRecord[
'uid'] .
' - Record had t3ver_move_id set to "' . $placeholderRecord[
't3ver_move_id'] .
'" while having t3ver_state=' . $placeholderRecord[
't3ver_state'];
544 protected function deleteRecords(array $records,
bool $dryRun, SymfonyStyle $io)
547 if (isset($records[
'pages'])) {
548 $_pages = $records[
'pages'];
549 unset($records[
'pages']);
551 $records[
'pages'] = array_reverse($_pages);
555 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
556 $dataHandler->start([], []);
559 foreach ($records as $table => $uidsInTable) {
560 if ($io->isVerbose()) {
561 $io->writeln(
'Flushing published records from table "' . $table .
'"');
563 foreach ($uidsInTable as $uid) {
564 if ($io->isVeryVerbose()) {
565 $io->writeln(
'Flushing record "' . $table .
':' . $uid .
'"');
568 $dataHandler->deleteEl($table, $uid,
true,
true);
569 if (!empty($dataHandler->errorLog)) {
570 $errorMessage = array_merge([
'DataHandler reported an error'], $dataHandler->errorLog);
571 $io->error($errorMessage);
572 } elseif (!$io->isQuiet()) {
573 $io->writeln(
'Flushed published record "' . $table .
':' . $uid .
'".');
590 foreach ($records as $table => $uidsInTable) {
591 if ($io->isVerbose()) {
592 $io->writeln(
'Resetting workspace to zero for records from table "' . $table .
'"');
594 foreach ($uidsInTable as $uid) {
595 if ($io->isVeryVerbose()) {
596 $io->writeln(
'Flushing record "' . $table .
':' . $uid .
'"');
599 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
600 ->getQueryBuilderForTable($table);
605 $queryBuilder->expr()->eq(
607 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
610 ->set(
't3ver_wsid', 0)
612 if (!$io->isQuiet()) {
613 $io->writeln(
'Flushed record "' . $table .
':' . $uid .
'".');
629 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
630 $dataHandler->start([], []);
631 foreach ($records as $record) {
632 $table = $record[
'table'];
633 $uid = $record[
'uid'];
634 if ($io->isVeryVerbose()) {
635 $io->writeln(
'Deleting unused placeholder (soft) "' . $table .
':' . $uid .
'"');
638 $dataHandler->deleteAction($table, $uid);
640 if (!empty($dataHandler->errorLog)) {
641 $errorMessage = array_merge([
'DataHandler reported an error'], $dataHandler->errorLog);
642 $io->error($errorMessage);
643 } elseif (!$io->isQuiet()) {
644 $io->writeln(
'Permanently deleted unused placeholder "' . $table .
':' . $uid .
'".');
661 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
662 ->getQueryBuilderForTable(
'sys_workspace');
664 $queryBuilder->getRestrictions()
666 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
668 $result = $queryBuilder
669 ->select(
'uid',
'title')
670 ->from(
'sys_workspace')
673 while ($workspaceRecord = $result->fetch()) {
674 $this->allWorkspaces[(int)$workspaceRecord[
'uid']] = $workspaceRecord[
'title'];
687 foreach (
$GLOBALS[
'TCA'] as $tableName => $config) {
689 $tables[] = $tableName;