‪TYPO3CMS  10.4
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;
32 
36 class ‪CleanFlexFormsCommand extends Command
37 {
38 
42  public function ‪configure()
43  {
44  $this
45  ->setDescription('Updates all database records which have a FlexForm field and the XML data does not match the chosen datastructure.')
46  ->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.')
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 be updated, but only show the output which records would have been updated.'
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 records with FlexForms that need to be updated.');
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 updated
100  $recordsToUpdate = $this->‪findAllDirtyFlexformsInPage($startingPoint, $depth);
101 
102  if (!$io->isQuiet()) {
103  $io->note('Found ' . count($recordsToUpdate) . ' records with wrong FlexForms information.');
104  }
105 
106  if (!empty($recordsToUpdate)) {
107  $io->section('Cleanup process starting now.' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
108 
109  // Clean up the records now
110  $this->‪cleanFlexFormRecords($recordsToUpdate, $dryRun, $io);
111 
112  $io->success('All done!');
113  } else {
114  $io->success('Nothing to do - You\'re all set!');
115  }
116  return 0;
117  }
118 
127  protected function ‪findAllDirtyFlexformsInPage(int $pageId, int $depth, array $dirtyFlexFormFields = []): array
128  {
129  if ($pageId > 0) {
130  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord('pages', $pageId, $dirtyFlexFormFields);
131  }
132 
133  // Traverse tables of records that belongs to this page
134  foreach (‪$GLOBALS['TCA'] as $tableName => $tableConfiguration) {
135  if ($tableName !== 'pages') {
136  // Select all records belonging to page:
137  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
138  ->getQueryBuilderForTable($tableName);
139 
140  $queryBuilder->getRestrictions()
141  ->removeAll();
142 
143  $result = $queryBuilder
144  ->select('uid')
145  ->from($tableName)
146  ->where(
147  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
148  )
149  ->execute();
150 
151  while ($rowSub = $result->fetch()) {
152  // Traverse flexforms
153  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord($tableName, $rowSub['uid'], $dirtyFlexFormFields);
154  // Add any versions of those records
156  $tableName,
157  $rowSub['uid'],
158  'uid,t3ver_wsid,t3ver_count',
159  null,
160  true
161  );
162  if (is_array($versions)) {
163  foreach ($versions as $verRec) {
164  if (!$verRec['_CURRENT_VERSION']) {
165  // Traverse flexforms
166  $dirtyFlexFormFields = $this->‪compareAllFlexFormsInRecord($tableName, $verRec['uid'], $dirtyFlexFormFields);
167  }
168  }
169  }
170  }
171  }
172  }
173 
174  // Find subpages
175  if ($depth > 0) {
176  $depth--;
177  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
178  ->getQueryBuilderForTable('pages');
179 
180  $queryBuilder->getRestrictions()
181  ->removeAll();
182 
183  $result = $queryBuilder
184  ->select('uid')
185  ->from('pages')
186  ->where(
187  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT))
188  )
189  ->orderBy('sorting')
190  ->execute();
191 
192  while ($row = $result->fetch()) {
193  $dirtyFlexFormFields = $this->‪findAllDirtyFlexformsInPage($row['uid'], $depth, $dirtyFlexFormFields);
194  }
195  }
196  // Add any versions of pages
197  if ($pageId > 0) {
198  $versions = ‪BackendUtility::selectVersionsOfRecord('pages', $pageId, 'uid,t3ver_oid,t3ver_wsid,t3ver_count', null, true);
199  if (is_array($versions)) {
200  foreach ($versions as $verRec) {
201  if (!$verRec['_CURRENT_VERSION']) {
202  $dirtyFlexFormFields = $this->‪findAllDirtyFlexformsInPage($verRec['uid'], $depth, $dirtyFlexFormFields);
203  }
204  }
205  }
206  }
207  return $dirtyFlexFormFields;
208  }
209 
219  protected function ‪compareAllFlexFormsInRecord(string $tableName, int $uid, array $dirtyFlexFormFields = []): array
220  {
221  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
222  foreach (‪$GLOBALS['TCA'][$tableName]['columns'] as $columnName => $columnConfiguration) {
223  if ($columnConfiguration['config']['type'] === 'flex') {
224  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
225  ->getQueryBuilderForTable($tableName);
226  $queryBuilder->getRestrictions()->removeAll();
227 
228  $fullRecord = $queryBuilder->select('*')
229  ->from($tableName)
230  ->where(
231  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
232  )
233  ->execute()
234  ->fetch();
235 
236  if ($fullRecord[$columnName]) {
237  // Clean XML and check against the record fetched from the database
238  $newXML = $flexObj->cleanFlexFormXML($tableName, $columnName, $fullRecord);
239  if (!hash_equals(md5($fullRecord[$columnName]), md5($newXML))) {
240  $dirtyFlexFormFields[$tableName . ':' . $uid . ':' . $columnName] = $fullRecord;
241  }
242  }
243  }
244  }
245  return $dirtyFlexFormFields;
246  }
247 
255  protected function ‪cleanFlexFormRecords(array $records, bool $dryRun, SymfonyStyle $io)
256  {
257  $flexObj = GeneralUtility::makeInstance(FlexFormTools::class);
258 
259  // Set up the data handler instance
260  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
261  $dataHandler->dontProcessTransformations = true;
262  $dataHandler->bypassWorkspaceRestrictions = true;
263  // Setting this option allows to also update deleted records (or records on deleted pages) within DataHandler
264  $dataHandler->bypassAccessCheckForRecords = true;
265 
266  // Loop through all tables and their records
267  foreach ($records as $recordIdentifier => $fullRecord) {
268  [$table, $uid, $field] = explode(':', $recordIdentifier);
269  if ($io->isVerbose()) {
270  $io->writeln('Cleaning FlexForm XML in "' . $recordIdentifier . '"');
271  }
272  if (!$dryRun) {
273  // Clean XML now
274  $data = [];
275  if ($fullRecord[$field]) {
276  $data[$table][$uid][$field] = $flexObj->cleanFlexFormXML($table, $field, $fullRecord);
277  } else {
278  $io->note('The field "' . $field . '" in record "' . $table . ':' . $uid . '" was not found.');
279  continue;
280  }
281  $dataHandler->start($data, []);
282  $dataHandler->process_datamap();
283  // Return errors if any:
284  if (!empty($dataHandler->errorLog)) {
285  $errorMessage = array_merge(['DataHandler reported an error'], $dataHandler->errorLog);
286  $io->error($errorMessage);
287  } elseif (!$io->isQuiet()) {
288  $io->writeln('Updated FlexForm in record "' . $table . ':' . $uid . '".');
289  }
290  }
291  }
292  }
293 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:84
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\compareAllFlexFormsInRecord
‪array compareAllFlexFormsInRecord(string $tableName, int $uid, array $dirtyFlexFormFields=[])
Definition: CleanFlexFormsCommand.php:219
‪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\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:607
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\cleanFlexFormRecords
‪cleanFlexFormRecords(array $records, bool $dryRun, SymfonyStyle $io)
Definition: CleanFlexFormsCommand.php:255
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\configure
‪configure()
Definition: CleanFlexFormsCommand.php:42
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\execute
‪int execute(InputInterface $input, OutputInterface $output)
Definition: CleanFlexFormsCommand.php:74
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:38
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand
Definition: CleanFlexFormsCommand.php:37
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:75
‪TYPO3\CMS\Lowlevel\Command\CleanFlexFormsCommand\findAllDirtyFlexformsInPage
‪array findAllDirtyFlexformsInPage(int $pageId, int $depth, array $dirtyFlexFormFields=[])
Definition: CleanFlexFormsCommand.php:127
‪$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