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