‪TYPO3CMS  11.5
CleanFlexFormsCommand.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;
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
33 
37 class ‪CleanFlexFormsCommand extends Command
38 {
42  private ‪$connectionPool;
43 
45  {
46  $this->connectionPool = ‪$connectionPool;
47  parent::__construct();
48  }
49 
53  public function ‪configure()
54  {
55  $this
56  ->setHelp('Traverse page tree and find and clean/update records with dirty FlexForm values. If you want to get more detailed information, use the --verbose option.')
57  ->addOption(
58  'pid',
59  'p',
60  InputOption::VALUE_REQUIRED,
61  'Setting start page in page tree. Default is the page tree root, 0 (zero)'
62  )
63  ->addOption(
64  'depth',
65  'd',
66  InputOption::VALUE_REQUIRED,
67  'Setting traversal depth. 0 (zero) will only analyze start page (see --pid), 1 will traverse one level of subpages etc.'
68  )
69  ->addOption(
70  'dry-run',
71  null,
72  InputOption::VALUE_NONE,
73  'If this option is set, the records will not be updated, but only show the output which records would have been updated.'
74  );
75  }
76 
84  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
85  {
86  // Make sure the _cli_ user is loaded
88 
89  $io = new SymfonyStyle($input, ‪$output);
90  $io->title($this->getDescription());
91 
92  $startingPoint = 0;
93  if ($input->hasOption('pid') && ‪MathUtility::canBeInterpretedAsInteger($input->getOption('pid'))) {
94  $startingPoint = ‪MathUtility::forceIntegerInRange((int)$input->getOption('pid'), 0);
95  }
96 
97  $depth = 1000;
98  if ($input->hasOption('depth') && ‪MathUtility::canBeInterpretedAsInteger($input->getOption('depth'))) {
99  $depth = ‪MathUtility::forceIntegerInRange((int)$input->getOption('depth'), 0);
100  }
101 
102  if ($io->isVerbose()) {
103  $io->section('Searching the database now for records with FlexForms that need to be updated.');
104  }
105 
106  // Type unsafe comparison and explicit boolean setting on purpose
107  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
108 
109  // Find all records that should be updated
110  $recordsToUpdate = $this->‪findAllDirtyFlexformsInPage($startingPoint, $depth);
111 
112  if (!$io->isQuiet()) {
113  $io->note('Found ' . count($recordsToUpdate) . ' records with wrong FlexForms information.');
114  }
115 
116  if (!empty($recordsToUpdate)) {
117  $io->section('Cleanup process starting now.' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
118 
119  // Clean up the records now
120  $this->‪cleanFlexFormRecords($recordsToUpdate, $dryRun, $io);
121 
122  $io->success('All done!');
123  } else {
124  $io->success('Nothing to do - You\'re all set!');
125  }
126  return 0;
127  }
128 
137  protected function ‪findAllDirtyFlexformsInPage(int $pageId, int $depth, array $dirtyFlexFormFields = []): array
138  {
139  if ($pageId > 0) {
140  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord('pages', $pageId, $dirtyFlexFormFields);
141  }
142 
143  // Traverse tables of records that belongs to this page
144  foreach (‪$GLOBALS['TCA'] as $tableName => $tableConfiguration) {
145  if ($tableName !== 'pages') {
146  // Select all records belonging to page:
147  $queryBuilder = $this->connectionPool
148  ->getQueryBuilderForTable($tableName);
149 
150  $queryBuilder->getRestrictions()
151  ->removeAll();
152 
153  $result = $queryBuilder
154  ->select('uid')
155  ->from($tableName)
156  ->where(
157  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT))
158  )
159  ->executeQuery();
160 
161  while ($rowSub = $result->fetchAssociative()) {
162  // Traverse flexforms
163  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord($tableName, $rowSub['uid'], $dirtyFlexFormFields);
164  // Add any versions of those records
165  $versions = BackendUtility::selectVersionsOfRecord(
166  $tableName,
167  $rowSub['uid'],
168  'uid,t3ver_wsid',
169  null,
170  true
171  );
172  if (is_array($versions)) {
173  foreach ($versions as $verRec) {
174  if (!($verRec['_CURRENT_VERSION'] ?? false)) {
175  // Traverse flexforms
176  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord($tableName, $verRec['uid'], $dirtyFlexFormFields);
177  }
178  }
179  }
180  }
181  }
182  }
183 
184  // Find subpages
185  if ($depth > 0) {
186  $depth--;
187  $queryBuilder = $this->connectionPool
188  ->getQueryBuilderForTable('pages');
189 
190  $queryBuilder->getRestrictions()
191  ->removeAll();
192 
193  $result = $queryBuilder
194  ->select('uid')
195  ->from('pages')
196  ->where(
197  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT))
198  )
199  ->orderBy('sorting')
200  ->executeQuery();
201 
202  while ($row = $result->fetchAssociative()) {
203  $dirtyFlexFormFields = $this->‪findAllDirtyFlexformsInPage($row['uid'], $depth, $dirtyFlexFormFields);
204  }
205  }
206  // Add any versions of pages
207  if ($pageId > 0) {
208  $versions = BackendUtility::selectVersionsOfRecord('pages', $pageId, 'uid,t3ver_oid,t3ver_wsid', null, true);
209  if (is_array($versions)) {
210  foreach ($versions as $verRec) {
211  if (!($verRec['_CURRENT_VERSION'] ?? false)) {
212  $dirtyFlexFormFields = $this->‪findAllDirtyFlexformsInPage($verRec['uid'], $depth, $dirtyFlexFormFields);
213  }
214  }
215  }
216  }
217  return $dirtyFlexFormFields;
218  }
219 
229  protected function ‪compareAllFlexFormsInRecord(string $tableName, int $uid, array $dirtyFlexFormFields = []): array
230  {
231  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
232  foreach (‪$GLOBALS['TCA'][$tableName]['columns'] as $columnName => $columnConfiguration) {
233  if ($columnConfiguration['config']['type'] === 'flex') {
234  $queryBuilder = $this->connectionPool
235  ->getQueryBuilderForTable($tableName);
236  $queryBuilder->getRestrictions()->removeAll();
237 
238  $fullRecord = $queryBuilder->select('*')
239  ->from($tableName)
240  ->where(
241  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT))
242  )
243  ->executeQuery()
244  ->fetchAssociative();
245 
246  if ($fullRecord[$columnName]) {
247  // Clean XML and check against the record fetched from the database
248  $newXML = $flexObj->cleanFlexFormXML($tableName, $columnName, $fullRecord);
249  if (!hash_equals(md5($fullRecord[$columnName]), md5($newXML))) {
250  $dirtyFlexFormFields[$tableName . ':' . $uid . ':' . $columnName] = $fullRecord;
251  }
252  }
253  }
254  }
255  return $dirtyFlexFormFields;
256  }
257 
265  protected function ‪cleanFlexFormRecords(array $records, bool $dryRun, SymfonyStyle $io)
266  {
267  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
268 
269  // Set up the data handler instance
270  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
271  $dataHandler->dontProcessTransformations = true;
272  $dataHandler->bypassWorkspaceRestrictions = true;
273  // Setting this option allows to also update deleted records (or records on deleted pages) within DataHandler
274  $dataHandler->bypassAccessCheckForRecords = true;
275 
276  // Loop through all tables and their records
277  foreach ($records as $recordIdentifier => $fullRecord) {
278  [$table, $uid, $field] = explode(':', $recordIdentifier);
279  if ($io->isVerbose()) {
280  $io->writeln('Cleaning FlexForm XML in "' . $recordIdentifier . '"');
281  }
282  if (!$dryRun) {
283  // Clean XML now
284  $data = [];
285  if ($fullRecord[$field]) {
286  $data[$table][$uid][$field] = $flexObj->cleanFlexFormXML($table, $field, $fullRecord);
287  } else {
288  $io->note('The field "' . $field . '" in record "' . $table . ':' . $uid . '" was not found.');
289  continue;
290  }
291  $dataHandler->start($data, []);
292  $dataHandler->process_datamap();
293  // Return errors if any:
294  if (!empty($dataHandler->errorLog)) {
295  $errorMessage = array_merge(['DataHandler reported an error'], $dataHandler->errorLog);
296  $io->error($errorMessage);
297  } elseif (!$io->isQuiet()) {
298  $io->writeln('Updated FlexForm in record "' . $table . ':' . $uid . '".');
299  }
300  }
301  }
302  }
303 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:86
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\compareAllFlexFormsInRecord
‪array compareAllFlexFormsInRecord(string $tableName, int $uid, array $dirtyFlexFormFields=[])
Definition: CleanFlexFormsCommand.php:228
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange($theInt, $min, $max=2000000000, $defaultValue=0)
Definition: MathUtility.php:32
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\__construct
‪__construct(ConnectionPool $connectionPool)
Definition: CleanFlexFormsCommand.php:43
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:588
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\cleanFlexFormRecords
‪cleanFlexFormRecords(array $records, bool $dryRun, SymfonyStyle $io)
Definition: CleanFlexFormsCommand.php:264
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\configure
‪configure()
Definition: CleanFlexFormsCommand.php:52
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\execute
‪int execute(InputInterface $input, OutputInterface $output)
Definition: CleanFlexFormsCommand.php:83
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:40
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand
Definition: CleanFlexFormsCommand.php:38
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\findAllDirtyFlexformsInPage
‪array findAllDirtyFlexformsInPage(int $pageId, int $depth, array $dirtyFlexFormFields=[])
Definition: CleanFlexFormsCommand.php:136
‪$output
‪$output
Definition: annotationChecker.php:121
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\$connectionPool
‪ConnectionPool $connectionPool
Definition: CleanFlexFormsCommand.php:41
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:70
‪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:50