TYPO3 CMS  TYPO3_8-7
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 
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 analyse 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
74  Bootstrap::getInstance()->initializeBackendAuthentication();
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  $pageRecordIsDeleted = $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 
154  // Register if page itself is deleted
155  if ($pageRecordIsDeleted->rowCount() > 0) {
156  $deletedRecords['pages'][$pageId] = $pageId;
157  }
158  }
159 
160  $databaseTables = $this->getTablesWithDeletedFlags();
161  // Traverse tables of records that belongs to page
162  foreach ($databaseTables as $tableName => $deletedField) {
163  // Select all records belonging to page
164  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
165  ->getQueryBuilderForTable($tableName);
166 
167  $queryBuilder->getRestrictions()->removeAll();
168 
169  $result = $queryBuilder
170  ->select('uid', $deletedField)
171  ->from($tableName)
172  ->where(
173  $queryBuilder->expr()->eq(
174  'pid',
175  $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
176  )
177  )
178  ->execute();
179 
180  while ($recordOnPage = $result->fetch()) {
181  // Register record as deleted
182  if ($recordOnPage[$deletedField]) {
183  $deletedRecords[$tableName][$recordOnPage['uid']] = $recordOnPage['uid'];
184  }
185  // Add any versions of those records
187  $tableName,
188  $recordOnPage['uid'],
189  'uid,t3ver_wsid,t3ver_count,' . $deletedField,
190  null,
191  true
192  ) ?: [];
193  if (is_array($versions)) {
194  foreach ($versions as $verRec) {
195  // Mark as deleted
196  if (!$verRec['_CURRENT_VERSION'] && $verRec[$deletedField]) {
197  $deletedRecords[$tableName][$verRec['uid']] = $verRec['uid'];
198  }
199  }
200  }
201  }
202  }
203 
204  // Find subpages to root ID and go recursive
205  if ($depth > 0) {
206  $depth--;
207  $result = $queryBuilderForPages
208  ->select('uid')
209  ->from('pages')
210  ->where(
211  $queryBuilderForPages->expr()->eq('pid', $pageId)
212  )
213  ->orderBy('sorting')
214  ->execute();
215 
216  while ($subPage = $result->fetch()) {
217  $deletedRecords = $this->findAllFlaggedRecordsInPage($subPage['uid'], $depth, $deletedRecords);
218  }
219  }
220 
221  // Add any versions of the page
222  if ($pageId > 0) {
224  'pages',
225  $pageId,
226  'uid,t3ver_oid,t3ver_wsid,t3ver_count',
227  null,
228  true
229  ) ?: [];
230  if (is_array($versions)) {
231  foreach ($versions as $verRec) {
232  if (!$verRec['_CURRENT_VERSION']) {
233  $deletedRecords = $this->findAllFlaggedRecordsInPage($verRec['uid'], $depth, $deletedRecords);
234  }
235  }
236  }
237  }
238 
239  return $deletedRecords;
240  }
241 
248  protected function getTablesWithDeletedFlags(): array
249  {
250  static $tables;
251  if (!is_array($tables)) {
252  $tables = [];
253  foreach ($GLOBALS['TCA'] as $tableName => $configuration) {
254  if ($tableName !== 'pages' && isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
255  $tables[$tableName] = $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
256  }
257  }
258  ksort($tables);
259  }
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 }
execute(InputInterface $input, OutputInterface $output)
static forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:31
deleteRecords(array $deletedRecords, bool $dryRun, SymfonyStyle $io)
static makeInstance($className,... $constructorArguments)
static selectVersionsOfRecord( $table, $uid, $fields=' *', $workspace=0, $includeDeletedRecords=false, $row=null)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']