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