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