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