‪TYPO3CMS  9.5
DeletedRecordsCommand.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
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;
30 
34 class ‪DeletedRecordsCommand extends Command
35 {
36 
40  public function ‪configure()
41  {
42  $this
43  ->setDescription('Permanently deletes all records marked as "deleted" in the database.')
44  ->setHelp('Traverse page tree and find and flush deleted records. If you want to get more detailed information, use the --verbose option.')
45  ->addOption(
46  'pid',
47  'p',
48  InputOption::VALUE_REQUIRED,
49  'Setting start page in page tree. Default is the page tree root, 0 (zero)'
50  )
51  ->addOption(
52  'depth',
53  'd',
54  InputOption::VALUE_REQUIRED,
55  'Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.'
56  )
57  ->addOption(
58  'dry-run',
59  null,
60  InputOption::VALUE_NONE,
61  'If this option is set, the records will not actually be deleted, but just the output which records would be deleted are shown'
62  );
63  }
64 
71  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
72  {
73  // Make sure the _cli_ user is loaded
75 
76  $io = new SymfonyStyle($input, ‪$output);
77  $io->title($this->getDescription());
78 
79  $startingPoint = 0;
80  if ($input->hasOption('pid') && ‪MathUtility::canBeInterpretedAsInteger($input->getOption('pid'))) {
81  $startingPoint = ‪MathUtility::forceIntegerInRange((int)$input->getOption('pid'), 0);
82  }
83 
84  $depth = 1000;
85  if ($input->hasOption('depth') && ‪MathUtility::canBeInterpretedAsInteger($input->getOption('depth'))) {
86  $depth = ‪MathUtility::forceIntegerInRange((int)$input->getOption('depth'), 0);
87  }
88 
89  if ($io->isVerbose()) {
90  $io->section('Searching the database now for deleted records.');
91  }
92 
93  // type unsafe comparison and explicit boolean setting on purpose
94  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
95 
96  // find all records that should be deleted
97  $deletedRecords = $this->‪findAllFlaggedRecordsInPage($startingPoint, $depth);
98 
99  if (!$io->isQuiet()) {
100  $totalAmountOfTables = count($deletedRecords);
101  $totalAmountOfRecords = 0;
102  foreach ($deletedRecords as $tableName => $itemsInTable) {
103  $totalAmountOfRecords += count($itemsInTable);
104 
105  if ($io->isVeryVerbose()) {
106  $io->writeln('Found ' . count($itemsInTable) . ' deleted records in table "' . $tableName . '".');
107  }
108  }
109  $io->note('Found ' . $totalAmountOfRecords . ' records in ' . $totalAmountOfTables . ' database tables ready to be deleted.');
110  }
111 
112  $io->section('Deletion process starting now.' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
113 
114  // actually permanently delete them
115  $this->‪deleteRecords($deletedRecords, $dryRun, $io);
116 
117  $io->success('All done!');
118  }
119 
131  protected function ‪findAllFlaggedRecordsInPage(int $pageId, int $depth, array $deletedRecords = []): array
132  {
134  $queryBuilderForPages = GeneralUtility::makeInstance(ConnectionPool::class)
135  ->getQueryBuilderForTable('pages');
136  $queryBuilderForPages->getRestrictions()->removeAll();
137 
138  $pageId = (int)$pageId;
139  if ($pageId > 0) {
140  $queryBuilderForPages
141  ->select('uid', 'deleted')
142  ->from('pages')
143  ->where(
144  $queryBuilderForPages->expr()->andX(
145  $queryBuilderForPages->expr()->eq(
146  'uid',
147  $queryBuilderForPages->createNamedParameter($pageId, \PDO::PARAM_INT)
148  ),
149  $queryBuilderForPages->expr()->neq('deleted', 0)
150  )
151  )
152  ->‪execute();
153  $rowCount = $queryBuilderForPages
154  ->count('uid')
155  ->execute()
156  ->fetchColumn(0);
157  // Register if page itself is deleted
158  if ($rowCount > 0) {
159  $deletedRecords['pages'][$pageId] = $pageId;
160  }
161  }
162 
163  $databaseTables = $this->‪getTablesWithDeletedFlags();
164  // Traverse tables of records that belongs to page
165  foreach ($databaseTables as $tableName => $deletedField) {
166  // Select all records belonging to page
167  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
168  ->getQueryBuilderForTable($tableName);
169 
170  $queryBuilder->getRestrictions()->removeAll();
171 
172  $result = $queryBuilder
173  ->select('uid', $deletedField)
174  ->from($tableName)
175  ->where(
176  $queryBuilder->expr()->eq(
177  'pid',
178  $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
179  )
180  )
181  ->execute();
182 
183  while ($recordOnPage = $result->fetch()) {
184  // Register record as deleted
185  if ($recordOnPage[$deletedField]) {
186  $deletedRecords[$tableName][$recordOnPage['uid']] = $recordOnPage['uid'];
187  }
188  // Add any versions of those records
190  $tableName,
191  $recordOnPage['uid'],
192  'uid,t3ver_wsid,t3ver_count,' . $deletedField,
193  null,
194  true
195  ) ?: [];
196  if (is_array($versions)) {
197  foreach ($versions as $verRec) {
198  // Mark as deleted
199  if (!$verRec['_CURRENT_VERSION'] && $verRec[$deletedField]) {
200  $deletedRecords[$tableName][$verRec['uid']] = $verRec['uid'];
201  }
202  }
203  }
204  }
205  }
206 
207  // Find subpages to root ID and go recursive
208  if ($depth > 0) {
209  $depth--;
210  $result = $queryBuilderForPages
211  ->select('uid')
212  ->from('pages')
213  ->where(
214  $queryBuilderForPages->expr()->eq('pid', $pageId)
215  )
216  ->orderBy('sorting')
217  ->execute();
218 
219  while ($subPage = $result->fetch()) {
220  $deletedRecords = $this->‪findAllFlaggedRecordsInPage($subPage['uid'], $depth, $deletedRecords);
221  }
222  }
223 
224  // Add any versions of the page
225  if ($pageId > 0) {
227  'pages',
228  $pageId,
229  'uid,t3ver_oid,t3ver_wsid,t3ver_count',
230  null,
231  true
232  ) ?: [];
233  if (is_array($versions)) {
234  foreach ($versions as $verRec) {
235  if (!$verRec['_CURRENT_VERSION']) {
236  $deletedRecords = $this->‪findAllFlaggedRecordsInPage($verRec['uid'], $depth, $deletedRecords);
237  }
238  }
239  }
240  }
241 
242  return $deletedRecords;
243  }
244 
251  protected function ‪getTablesWithDeletedFlags(): array
252  {
253  $tables = [];
254  foreach (‪$GLOBALS['TCA'] as $tableName => $configuration) {
255  if ($tableName !== 'pages' && isset(‪$GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
256  $tables[$tableName] = ‪$GLOBALS['TCA'][$tableName]['ctrl']['delete'];
257  }
258  }
259  ksort($tables);
260  return $tables;
261  }
262 
270  protected function ‪deleteRecords(array $deletedRecords, bool $dryRun, SymfonyStyle $io)
271  {
272  // Putting "pages" table in the bottom
273  if (isset($deletedRecords['pages'])) {
274  $_pages = $deletedRecords['pages'];
275  unset($deletedRecords['pages']);
276  // To delete sub pages first assuming they are accumulated from top of page tree.
277  $deletedRecords['pages'] = array_reverse($_pages);
278  }
279 
280  // set up the data handler instance
281  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
282  $dataHandler->start([], []);
283 
284  // Loop through all tables and their records
285  foreach ($deletedRecords as $table => $list) {
286  if ($io->isVerbose()) {
287  $io->writeln('Flushing ' . count($list) . ' deleted records from table "' . $table . '"');
288  }
289  foreach ($list as $uid) {
290  if ($io->isVeryVerbose()) {
291  $io->writeln('Flushing record "' . $table . ':' . $uid . '"');
292  }
293  if (!$dryRun) {
294  // Notice, we are deleting pages with no regard to subpages/subrecords - we do this since they
295  // should also be included in the set of deleted pages of course (no un-deleted record can exist
296  // under a deleted page...)
297  $dataHandler->deleteRecord($table, $uid, true, true);
298  // Return errors if any:
299  if (!empty($dataHandler->errorLog)) {
300  $errorMessage = array_merge(['DataHandler reported an error'], $dataHandler->errorLog);
301  $io->error($errorMessage);
302  } elseif (!$io->isQuiet()) {
303  $io->writeln('Permanently deleted record "' . $table . ':' . $uid . '".');
304  }
305  }
306  }
307  }
308  }
309 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:81
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:73
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand
Definition: DeletedRecordsCommand.php:35
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:31
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand\getTablesWithDeletedFlags
‪array getTablesWithDeletedFlags()
Definition: DeletedRecordsCommand.php:251
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand\findAllFlaggedRecordsInPage
‪array findAllFlaggedRecordsInPage(int $pageId, int $depth, array $deletedRecords=[])
Definition: DeletedRecordsCommand.php:131
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:47
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand\execute
‪execute(InputInterface $input, OutputInterface $output)
Definition: DeletedRecordsCommand.php:71
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand\configure
‪configure()
Definition: DeletedRecordsCommand.php:40
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static Bootstrap null initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:974
‪TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand\deleteRecords
‪deleteRecords(array $deletedRecords, bool $dryRun, SymfonyStyle $io)
Definition: DeletedRecordsCommand.php:270
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:72
‪$output
‪$output
Definition: annotationChecker.php:113
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:50
‪TYPO3\CMS\Backend\Utility\BackendUtility\selectVersionsOfRecord
‪static array null selectVersionsOfRecord( $table, $uid, $fields=' *', $workspace=0, $includeDeletedRecords=false, $row=null)
Definition: BackendUtility.php:3891
‪TYPO3\CMS\Lowlevel\Command
Definition: CleanFlexFormsCommand.php:3
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:21
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45