[!!!][TASK] Use native trigger_error and ErrorHandler for deprecations
[Packages/TYPO3.CMS.git] / typo3 / sysext / lowlevel / Classes / Command / WorkspaceVersionRecordsCommand.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Lowlevel\Command;
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;
23 use TYPO3\CMS\Backend\Utility\BackendUtility;
24 use TYPO3\CMS\Core\Core\Bootstrap;
25 use TYPO3\CMS\Core\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27 use TYPO3\CMS\Core\DataHandling\DataHandler;
28 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Core\Utility\MathUtility;
31 use TYPO3\CMS\Core\Versioning\VersionState;
32
33 /**
34 * Fetches all versions in the database, and checks for integrity
35 */
36 class WorkspaceVersionRecordsCommand extends Command
37 {
38
39 /**
40 * List of all workspaces
41 * @var array
42 */
43 protected $allWorkspaces = [0 => 'Live Workspace'];
44
45 /**
46 * Array with all records found when traversing the database
47 * @var array
48 */
49 protected $foundRecords = [
50 // All versions of records found
51 // Subset of "all" which are offline versions (pid=-1) [Informational]
52 'all_versioned_records' => [],
53 // All records that has been published and can therefore be removed permanently
54 // Subset of "versions" that is a count of 1 or more (has been published) [Informational]
55 'published_versions' => [],
56 // All versions that are offline versions in the Live workspace. You may wish to flush these if you only use
57 // workspaces for versioning since then you might find lots of versions piling up in the live workspace which
58 // have simply been disconnected from the workspace before they were published.
59 'versions_in_live' => [],
60 // Versions that has lost their connection to a workspace in TYPO3.
61 // Subset of "versions" that doesn't belong to an existing workspace [Warning: Fix by move to live workspace]
62 'invalid_workspace' => []
63 ];
64
65 /**
66 * Configuring the command options
67 */
68 public function configure()
69 {
70 $this
71 ->setDescription('Find all versioned records and possibly cleans up invalid records in the database.')
72 ->setHelp('Traverse page tree and find versioned records. Also list all versioned records, additionally with some inconsistencies in the database, which can cleaned up with the "action" option. If you want to get more detailed information, use the --verbose option.')
73 ->addOption(
74 'pid',
75 'p',
76 InputOption::VALUE_REQUIRED,
77 'Setting start page in page tree. Default is the page tree root, 0 (zero)'
78 )
79 ->addOption(
80 'depth',
81 'd',
82 InputOption::VALUE_REQUIRED,
83 'Setting traversal depth. 0 (zero) will only analyse start page (see --pid), 1 will traverse one level of subpages etc.'
84 )
85 ->addOption(
86 'dry-run',
87 null,
88 InputOption::VALUE_NONE,
89 'If this option is set, the records will not actually be deleted/modified, but just the output which records would be touched are shown'
90 )
91 ->addOption(
92 'action',
93 null,
94 InputOption::VALUE_NONE,
95 'Specify which action should be taken. Set it to "versions_in_live", "published_versions", "invalid_workspace" or "unused_placeholders"'
96 );
97 }
98
99 /**
100 * Executes the command to find versioned records
101 *
102 * @param InputInterface $input
103 * @param OutputInterface $output
104 */
105 protected function execute(InputInterface $input, OutputInterface $output)
106 {
107 // Make sure the _cli_ user is loaded
108 Bootstrap::getInstance()->initializeBackendAuthentication();
109
110 $io = new SymfonyStyle($input, $output);
111 $io->title($this->getDescription());
112
113 $startingPoint = 0;
114 if ($input->hasOption('pid') && MathUtility::canBeInterpretedAsInteger($input->getOption('pid'))) {
115 $startingPoint = MathUtility::forceIntegerInRange((int)$input->getOption('pid'), 0);
116 }
117
118 $depth = 1000;
119 if ($input->hasOption('depth') && MathUtility::canBeInterpretedAsInteger($input->getOption('depth'))) {
120 $depth = MathUtility::forceIntegerInRange((int)$input->getOption('depth'), 0);
121 }
122
123 $action = '';
124 if ($input->hasOption('action') && !empty($input->getOption('action'))) {
125 $action = $input->getOption('action');
126 }
127
128 // type unsafe comparison and explicit boolean setting on purpose
129 $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
130
131 if ($io->isVerbose()) {
132 $io->section('Searching the database now for versioned records.');
133 }
134
135 $this->loadAllWorkspaceRecords();
136
137 // Find all records that are versioned
138 $this->traversePageTreeForVersionedRecords($startingPoint, $depth);
139 // Sort recStats (for diff'able displays)
140 foreach ($this->foundRecords as $kk => $vv) {
141 foreach ($this->foundRecords[$kk] as $tables => $recArrays) {
142 ksort($this->foundRecords[$kk][$tables]);
143 }
144 ksort($this->foundRecords[$kk]);
145 }
146
147 $unusedPlaceholders = $this->findUnusedPlaceholderRecords();
148
149 // Finding all move placeholders with inconsistencies
150 // Move-to placeholder records which have bad integrity
151 $invalidMovePlaceholders = $this->findInvalidMovePlaceholderRecords();
152
153 // Finding move_id_check inconsistencies
154 // Checking if t3ver_move_id is correct. t3ver_move_id must only be set with online records having t3ver_state=3.
155 $recordsWithInvalidMoveIds = $this->findInvalidMoveIdRecords();
156
157 if (!$io->isQuiet()) {
158 $numberOfVersionedRecords = 0;
159 foreach ($this->foundRecords['all_versioned_records'] as $records) {
160 $numberOfVersionedRecords += count($records);
161 }
162
163 $io->section('Found ' . $numberOfVersionedRecords . ' versioned records in the database.');
164 if ($io->isVeryVerbose()) {
165 foreach ($this->foundRecords['all_versioned_records'] as $table => $records) {
166 $io->writeln('Table "' . $table . '"');
167 $io->listing($records);
168 }
169 }
170
171 $numberOfPublishedVersions = 0;
172 foreach ($this->foundRecords['published_versions'] as $records) {
173 $numberOfPublishedVersions += count($records);
174 }
175 $io->section('Found ' . $numberOfPublishedVersions . ' versioned records that have been published.');
176 if ($io->isVeryVerbose()) {
177 foreach ($this->foundRecords['published_versions'] as $table => $records) {
178 $io->writeln('Table "' . $table . '"');
179 $io->listing($records);
180 }
181 }
182
183 $numberOfVersionsInLiveWorkspace = 0;
184 foreach ($this->foundRecords['versions_in_live'] as $records) {
185 $numberOfVersionsInLiveWorkspace += count($records);
186 }
187 $io->section('Found ' . $numberOfVersionsInLiveWorkspace . ' versioned records that are in the live workspace.');
188 if ($io->isVeryVerbose()) {
189 foreach ($this->foundRecords['versions_in_live'] as $table => $records) {
190 $io->writeln('Table "' . $table . '"');
191 $io->listing($records);
192 }
193 }
194
195 $numberOfVersionsWithInvalidWorkspace = 0;
196 foreach ($this->foundRecords['invalid_workspace'] as $records) {
197 $numberOfVersionsWithInvalidWorkspace += count($records);
198 }
199 $io->section('Found ' . $numberOfVersionsWithInvalidWorkspace . ' versioned records with an invalid workspace.');
200 if ($io->isVeryVerbose()) {
201 foreach ($this->foundRecords['invalid_workspace'] as $table => $records) {
202 $io->writeln('Table "' . $table . '"');
203 $io->listing($records);
204 }
205 }
206
207 $io->section('Found ' . count($unusedPlaceholders) . ' unused placeholder records.');
208 if ($io->isVeryVerbose()) {
209 $io->listing(array_keys($unusedPlaceholders));
210 }
211
212 $io->section('Found ' . count($invalidMovePlaceholders) . ' invalid move placeholders.');
213 if ($io->isVeryVerbose()) {
214 $io->listing($invalidMovePlaceholders);
215 }
216
217 $io->section('Found ' . count($recordsWithInvalidMoveIds) . ' versions with an invalid move ID.');
218 if ($io->isVeryVerbose()) {
219 $io->listing($recordsWithInvalidMoveIds);
220 }
221 }
222
223 // Actually permanently delete / update records
224 switch ($action) {
225 // All versions that are offline versions in the Live workspace. You may wish to flush these if you only use
226 // workspaces for versioning since then you might find lots of versions piling up in the live workspace which
227 // have simply been disconnected from the workspace before they were published.
228 case 'versions_in_live':
229 $io->section('Deleting versioned records in live workspace now. ' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
230 $this->deleteRecords($this->foundRecords['versions_in_live'], $dryRun, $io);
231 break;
232
233 // All records that has been published and can therefore be removed permanently
234 // Subset of "versions" that is a count of 1 or more (has been published)
235 case 'published_versions':
236 $io->section('Deleting published records in live workspace now. ' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
237 $this->deleteRecords($this->foundRecords['published_versions'], $dryRun, $io);
238 break;
239
240 // Versions that has lost their connection to a workspace in TYPO3.
241 // Subset of "versions" that doesn't belong to an existing workspace [Warning: Fix by move to live workspace]
242 case 'invalid_workspace':
243 $io->section('Moving versions in invalid workspaces to live workspace now. ' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
244 $this->resetRecordsWithoutValidWorkspace($this->foundRecords['invalid_workspace'], $dryRun, $io);
245 break;
246
247 // Finding all placeholders with no records attached
248 // Placeholder records which are not used anymore by offline versions.
249 case 'unused_placeholders':
250 $io->section('Deleting unused placeholder records now. ' . ($dryRun ? ' (Not deleting now, just a dry run)' : ''));
251 $this->deleteUnusedPlaceholders($unusedPlaceholders, $dryRun, $io);
252 break;
253
254 default:
255 $io->note('No action specified, just displaying statistics. See --action option for details.');
256 break;
257 }
258 $io->success('All done!');
259 }
260
261 /**
262 * Recursive traversal of page tree, fetching ALL versioned records found in the database
263 *
264 * @param int $rootID Page root id (must be online, valid page record - or zero for page tree root)
265 * @param int $depth Depth
266 * @param bool $isInsideVersionedPage DON'T set from outside, internal. (indicates we are inside a version of a page)
267 * @param bool $rootIsVersion DON'T set from outside, internal. Indicates that rootID is a version of a page
268 */
269 protected function traversePageTreeForVersionedRecords(int $rootID, int $depth, bool $isInsideVersionedPage = false, bool $rootIsVersion = false)
270 {
271 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
272 $queryBuilder->getRestrictions()->removeAll();
273
274 $pageRecord = $queryBuilder
275 ->select(
276 'deleted',
277 'title',
278 't3ver_count',
279 't3ver_wsid'
280 )
281 ->from('pages')
282 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)))
283 ->execute()
284 ->fetch();
285
286 // If rootIsVersion is set it means that the input rootID is that of a version of a page. See below where the recursive call is made.
287 if ($rootIsVersion) {
288 $workspaceId = (int)$pageRecord['t3ver_wsid'];
289 $this->foundRecords['all_versioned_records']['pages'][$rootID] = $rootID;
290 // If it has been published and is in archive now...
291 if ($pageRecord['t3ver_count'] >= 1 && $workspaceId === 0) {
292 $this->foundRecords['published_versions']['pages'][$rootID] = $rootID;
293 }
294 // If it has been published and is in archive now...
295 if ($workspaceId === 0) {
296 $this->foundRecords['versions_in_live']['pages'][$rootID] = $rootID;
297 }
298 // If it doesn't belong to a workspace...
299 if (!isset($this->allWorkspaces[$workspaceId])) {
300 $this->foundRecords['invalid_workspace']['pages'][$rootID] = $rootID;
301 }
302 }
303 // Only check for records if not inside a version
304 if (!$isInsideVersionedPage) {
305 // Traverse tables of records that belongs to page
306 $tableNames = $this->getAllVersionableTables();
307 foreach ($tableNames as $tableName) {
308 if ($tableName !== 'pages') {
309 // Select all records belonging to page:
310 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
311 ->getQueryBuilderForTable($tableName);
312
313 $queryBuilder->getRestrictions()->removeAll();
314
315 $result = $queryBuilder
316 ->select('uid')
317 ->from($tableName)
318 ->where(
319 $queryBuilder->expr()->eq(
320 'pid',
321 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
322 )
323 )
324 ->execute();
325 while ($rowSub = $result->fetch()) {
326 // Add any versions of those records
327 $versions = BackendUtility::selectVersionsOfRecord($tableName, $rowSub['uid'], 'uid,t3ver_wsid,t3ver_count' . ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ? ',' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] : ''), null, true);
328 if (is_array($versions)) {
329 foreach ($versions as $verRec) {
330 if (!$verRec['_CURRENT_VERSION']) {
331 // Register version
332 $this->foundRecords['all_versioned_records'][$tableName][$verRec['uid']] = $verRec['uid'];
333 $workspaceId = (int)$verRec['t3ver_wsid'];
334 if ($verRec['t3ver_count'] >= 1 && $workspaceId === 0) {
335 // Only register published versions in LIVE workspace
336 // (published versions in draft workspaces are allowed)
337 $this->foundRecords['published_versions'][$tableName][$verRec['uid']] = $verRec['uid'];
338 }
339 if ($workspaceId === 0) {
340 $this->foundRecords['versions_in_live'][$tableName][$verRec['uid']] = $verRec['uid'];
341 }
342 if (!isset($this->allWorkspaces[$workspaceId])) {
343 $this->foundRecords['invalid_workspace'][$tableName][$verRec['uid']] = $verRec['uid'];
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352 // Find subpages to root ID and traverse (only when rootID is not a version or is a branch-version):
353 if ($depth > 0) {
354 $depth--;
355 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
356 ->getQueryBuilderForTable('pages');
357
358 $queryBuilder->getRestrictions()->removeAll();
359 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
360
361 $queryBuilder
362 ->select('uid')
363 ->from('pages')
364 ->where(
365 $queryBuilder->expr()->eq(
366 'pid',
367 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
368 )
369 )
370 ->orderBy('sorting');
371
372 $result = $queryBuilder->execute();
373 while ($row = $result->fetch()) {
374 $this->traversePageTreeForVersionedRecords((int)$row['uid'], $depth, $isInsideVersionedPage, false);
375 }
376 }
377 // Add any versions of pages
378 if ($rootID > 0) {
379 $versions = BackendUtility::selectVersionsOfRecord('pages', $rootID, 'uid,t3ver_oid,t3ver_wsid,t3ver_count', null, true);
380 if (is_array($versions)) {
381 foreach ($versions as $verRec) {
382 if (!$verRec['_CURRENT_VERSION']) {
383 $this->traversePageTreeForVersionedRecords((int)$verRec['uid'], $depth, true, true);
384 }
385 }
386 }
387 }
388 }
389
390 /**
391 * Find all records where the field t3ver_state=1 (new placeholder)
392 *
393 * @return array the records (md5 as hash) with "table:uid" as value
394 */
395 protected function findUnusedPlaceholderRecords(): array
396 {
397 $unusedPlaceholders = [];
398 $tableNames = $this->getAllVersionableTables();
399 foreach ($tableNames as $table) {
400 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
401 ->getQueryBuilderForTable($table);
402
403 $queryBuilder->getRestrictions()
404 ->removeAll()
405 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
406
407 $result = $queryBuilder
408 ->select('uid', 'pid')
409 ->from($table)
410 ->where(
411 $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
412 $queryBuilder->expr()->eq(
413 't3ver_state',
414 $queryBuilder->createNamedParameter(
415 (string)new VersionState(VersionState::NEW_PLACEHOLDER),
416 \PDO::PARAM_INT
417 )
418 )
419 )
420 ->execute();
421
422 while ($placeholderRecord = $result->fetch()) {
423 $versions = BackendUtility::selectVersionsOfRecord($table, $placeholderRecord['uid'], 'uid', '*', null);
424 if (count($versions) <= 1) {
425 $unusedPlaceholders[$table . ':' . $placeholderRecord['uid']] = [
426 'table' => $table,
427 'uid' => $placeholderRecord['uid']
428 ];
429 }
430 }
431 }
432 ksort($unusedPlaceholders);
433 return $unusedPlaceholders;
434 }
435
436 /**
437 * Find all records where the field t3ver_state=3 (move placeholder)
438 * and checks against the ws_id etc.
439 *
440 * @return array the records (md5 as hash) with an array of data
441 */
442 protected function findInvalidMovePlaceholderRecords(): array
443 {
444 $invalidMovePlaceholders = [];
445 $tableNames = $this->getAllVersionableTables();
446 foreach ($tableNames as $table) {
447 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
448 ->getQueryBuilderForTable($table);
449
450 $queryBuilder->getRestrictions()
451 ->removeAll()
452 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
453
454 $result = $queryBuilder
455 ->select('uid', 'pid', 't3ver_move_id', 't3ver_wsid', 't3ver_state')
456 ->from($table)
457 ->where(
458 $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
459 $queryBuilder->expr()->eq(
460 't3ver_state',
461 $queryBuilder->createNamedParameter(
462 (string)new VersionState(VersionState::MOVE_PLACEHOLDER),
463 \PDO::PARAM_INT
464 )
465 )
466 )
467 ->execute();
468 while ($placeholderRecord = $result->fetch()) {
469 $shortID = GeneralUtility::shortMD5($table . ':' . $placeholderRecord['uid']);
470 if ((int)$placeholderRecord['t3ver_wsid'] !== 0) {
471 $phrecCopy = $placeholderRecord;
472 if (BackendUtility::movePlhOL($table, $placeholderRecord)) {
473 if ($wsAlt = BackendUtility::getWorkspaceVersionOfRecord($phrecCopy['t3ver_wsid'], $table, $placeholderRecord['uid'], 'uid,pid,t3ver_state')) {
474 if (!VersionState::cast($wsAlt['t3ver_state'])->equals(VersionState::MOVE_POINTER)) {
475 $invalidMovePlaceholders[$shortID] = $table . ':' . $placeholderRecord['uid'] . ' - State for version was not "4" as it should be!';
476 }
477 } else {
478 $invalidMovePlaceholders[$shortID] = $table . ':' . $placeholderRecord['uid'] . ' - No version was found for online record to be moved. A version must exist.';
479 }
480 } else {
481 $invalidMovePlaceholders[$shortID] = $table . ':' . $placeholderRecord['uid'] . ' - Did not find online record for "t3ver_move_id" value ' . $placeholderRecord['t3ver_move_id'];
482 }
483 } else {
484 $invalidMovePlaceholders[$shortID] = $table . ':' . $placeholderRecord['uid'] . ' - Placeholder was not assigned a workspace value in t3ver_wsid.';
485 }
486 }
487 }
488 ksort($invalidMovePlaceholders);
489 return $invalidMovePlaceholders;
490 }
491
492 /**
493 * Find records with a t3ver_move_id field != 0 that are
494 * neither a move placeholder or, if it is a move placeholder is offline
495 *
496 * @return array
497 */
498 protected function findInvalidMoveIdRecords(): array
499 {
500 $records = [];
501 $tableNames = $this->getAllVersionableTables();
502 foreach ($tableNames as $table) {
503 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
504 ->getQueryBuilderForTable($table);
505
506 $queryBuilder->getRestrictions()
507 ->removeAll()
508 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
509
510 $result = $queryBuilder
511 ->select('uid', 'pid', 't3ver_move_id', 't3ver_wsid', 't3ver_state')
512 ->from($table)
513 ->where(
514 $queryBuilder->expr()->neq(
515 't3ver_move_id',
516 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
517 )
518 )
519 ->execute();
520
521 while ($placeholderRecord = $result->fetch()) {
522 if (VersionState::cast($placeholderRecord['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)) {
523 if ((int)$placeholderRecord['pid'] === -1) {
524 $records[] = $table . ':' . $placeholderRecord['uid'] . ' - Record was offline, must not be!';
525 }
526 } else {
527 $records[] = $table . ':' . $placeholderRecord['uid'] . ' - Record had t3ver_move_id set to "' . $placeholderRecord['t3ver_move_id'] . '" while having t3ver_state=' . $placeholderRecord['t3ver_state'];
528 }
529 }
530 }
531 return $records;
532 }
533
534 /**************************
535 * actions / delete methods
536 **************************/
537
538 /**
539 * Deletes records via DataHandler
540 *
541 * @param array $records two level array with tables and uids
542 * @param bool $dryRun check if the records should NOT be deleted (use --dry-run to avoid)
543 * @param SymfonyStyle $io
544 */
545 protected function deleteRecords(array $records, bool $dryRun, SymfonyStyle $io)
546 {
547 // Putting "pages" table in the bottom
548 if (isset($records['pages'])) {
549 $_pages = $records['pages'];
550 unset($records['pages']);
551 // To delete sub pages first assuming they are accumulated from top of page tree.
552 $records['pages'] = array_reverse($_pages);
553 }
554
555 // Set up the data handler instance
556 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
557 $dataHandler->start([], []);
558
559 // Traversing records
560 foreach ($records as $table => $uidsInTable) {
561 if ($io->isVerbose()) {
562 $io->writeln('Flushing published records from table "' . $table . '"');
563 }
564 foreach ($uidsInTable as $uid) {
565 if ($io->isVeryVerbose()) {
566 $io->writeln('Flushing record "' . $table . ':' . $uid . '"');
567 }
568 if (!$dryRun) {
569 $dataHandler->deleteEl($table, $uid, true, true);
570 if (!empty($dataHandler->errorLog)) {
571 $errorMessage = array_merge(['DataHandler reported an error'], $dataHandler->errorLog);
572 $io->error($errorMessage);
573 } elseif (!$io->isQuiet()) {
574 $io->writeln('Flushed published record "' . $table . ':' . $uid . '".');
575 }
576 }
577 }
578 }
579 }
580
581 /**
582 * Set the workspace ID to "0" (= live) for records that have a workspace not found
583 * in the system (e.g. hard deleted in the database)
584 *
585 * @param array $records array with array of table and uid of each record
586 * @param bool $dryRun check if the records should NOT be deleted (use --dry-run to avoid)
587 * @param SymfonyStyle $io
588 */
589 protected function resetRecordsWithoutValidWorkspace(array $records, bool $dryRun, SymfonyStyle $io)
590 {
591 foreach ($records as $table => $uidsInTable) {
592 if ($io->isVerbose()) {
593 $io->writeln('Resetting workspace to zero for records from table "' . $table . '"');
594 }
595 foreach ($uidsInTable as $uid) {
596 if ($io->isVeryVerbose()) {
597 $io->writeln('Flushing record "' . $table . ':' . $uid . '"');
598 }
599 if (!$dryRun) {
600 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
601 ->getQueryBuilderForTable($table);
602
603 $queryBuilder
604 ->update($table)
605 ->where(
606 $queryBuilder->expr()->eq(
607 'uid',
608 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
609 )
610 )
611 ->set('t3ver_wsid', 0)
612 ->execute();
613 if (!$io->isQuiet()) {
614 $io->writeln('Flushed record "' . $table . ':' . $uid . '".');
615 }
616 }
617 }
618 }
619 }
620
621 /**
622 * Delete unused placeholders
623 *
624 * @param array $records array with array of table and uid of each record
625 * @param bool $dryRun check if the records should NOT be deleted (use --dry-run to avoid)
626 * @param SymfonyStyle $io
627 */
628 protected function deleteUnusedPlaceholders(array $records, bool $dryRun, SymfonyStyle $io)
629 {
630 $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
631 $dataHandler->start([], []);
632 foreach ($records as $record) {
633 $table = $record['table'];
634 $uid = $record['uid'];
635 if ($io->isVeryVerbose()) {
636 $io->writeln('Deleting unused placeholder (soft) "' . $table . ':' . $uid . '"');
637 }
638 if (!$dryRun) {
639 $dataHandler->deleteAction($table, $uid);
640 // Return errors if any
641 if (!empty($dataHandler->errorLog)) {
642 $errorMessage = array_merge(['DataHandler reported an error'], $dataHandler->errorLog);
643 $io->error($errorMessage);
644 } elseif (!$io->isQuiet()) {
645 $io->writeln('Permanently deleted unused placeholder "' . $table . ':' . $uid . '".');
646 }
647 }
648 }
649 }
650
651 /**
652 * HELPER FUNCTIONS
653 */
654
655 /**
656 * Fetches all sys_workspace records from the database
657 *
658 * @return array all workspaces with UID as key, and the title as value
659 */
660 protected function loadAllWorkspaceRecords(): array
661 {
662 if (ExtensionManagementUtility::isLoaded('workspaces')) {
663 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
664 ->getQueryBuilderForTable('sys_workspace');
665
666 $queryBuilder->getRestrictions()
667 ->removeAll()
668 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
669
670 $result = $queryBuilder
671 ->select('uid', 'title')
672 ->from('sys_workspace')
673 ->execute();
674
675 while ($workspaceRecord = $result->fetch()) {
676 $this->allWorkspaces[(int)$workspaceRecord['uid']] = $workspaceRecord['title'];
677 }
678 }
679 return $this->allWorkspaces;
680 }
681
682 /**
683 * Returns all TCA tables where workspaces is enabled
684 *
685 * @return array
686 */
687 protected function getAllVersionableTables(): array
688 {
689 static $tables;
690 if (!is_array($tables)) {
691 $tables = [];
692 foreach ($GLOBALS['TCA'] as $tableName => $config) {
693 if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
694 $tables[] = $tableName;
695 }
696 }
697 }
698 return $tables;
699 }
700 }