[TASK] Deprecate CommandLineController and Cleaner Command
[Packages/TYPO3.CMS.git] / typo3 / sysext / lowlevel / Classes / CleanerCommand.php
1 <?php
2 namespace TYPO3\CMS\Lowlevel;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
20 use TYPO3\CMS\Core\Database\ReferenceIndex;
21 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\MathUtility;
24
25 /**
26 * Core functions for cleaning and analysing
27 *
28 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
29 */
30 class CleanerCommand extends \TYPO3\CMS\Core\Controller\CommandLineController
31 {
32 /**
33 * @var bool
34 */
35 public $genTree_traverseDeleted = true;
36
37 /**
38 * @var bool
39 */
40 public $genTree_traverseVersions = true;
41
42 /**
43 * @var string
44 */
45 public $label_infoString = 'The list of records is organized as [table]:[uid]:[field]:[flexpointer]:[softref_key]';
46
47 /**
48 * @var array
49 */
50 public $pagetreePlugins = [];
51
52 /**
53 * @var array
54 */
55 public $cleanerModules = [];
56
57 /**
58 * @var array
59 */
60 protected $recStats = [];
61
62 /**
63 * @var array
64 */
65 protected $workspaceIndex = [0 => true];
66
67 /**
68 * Constructor
69 */
70 public function __construct()
71 {
72 // Running parent class constructor
73 parent::__construct();
74 $this->cleanerModules = (array)$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules'];
75 // Adding options to help archive:
76 $this->cli_options[] = ['-r', 'Execute this tool, otherwise help is shown'];
77 $this->cli_options[] = ['-v level', 'Verbosity level 0-3', 'The value of level can be:
78 0 = all output
79 1 = info and greater (default)
80 2 = warnings and greater
81 3 = errors'];
82 $this->cli_options[] = ['--refindex mode', 'Mode for reference index handling for operations that require a clean reference index ("update"/"ignore")', 'Options are "check" (default), "update" and "ignore". By default, the reference index is checked before running analysis that require a clean index. If the check fails, the analysis is not run. You can choose to bypass this completely (using value "ignore") or ask to have the index updated right away before the analysis (using value "update")'];
83 $this->cli_options[] = ['--AUTOFIX [testName]', 'Repairs errors that can be automatically fixed.', 'Only add this option after having run the test without it so you know what will happen when you add this option! The optional parameter "[testName]" works for some tool keys to limit the fixing to a particular test.'];
84 $this->cli_options[] = ['--dryrun', 'With --AUTOFIX it will only simulate a repair process', 'You may like to use this to see what the --AUTOFIX option will be doing. It will output the whole process like if a fix really occurred but nothing is in fact happening'];
85 $this->cli_options[] = ['--YES', 'Implicit YES to all questions', 'Use this with EXTREME care. The option "-i" is not affected by this option.'];
86 $this->cli_options[] = ['-i', 'Interactive', 'Will ask you before running the AUTOFIX on each element.'];
87 $this->cli_options[] = ['--filterRegex expr', 'Define an expression for preg_match() that must match the element ID in order to auto repair it', 'The element ID is the string in quotation marks when the text \'Cleaning ... in "ELEMENT ID"\'. "expr" is the expression for preg_match(). To match for example "Nature3.JPG" and "Holiday3.JPG" you can use "/.*3.JPG/". To match for example "Image.jpg" and "Image.JPG" you can use "/.*.jpg/i". Try a --dryrun first to see what the matches are!'];
88 $this->cli_options[] = ['--showhowto', 'Displays HOWTO file for cleaner script.'];
89 // Setting help texts:
90 $this->cli_help['name'] = 'lowlevel_cleaner -- Analysis and clean-up tools for TYPO3 installations';
91 $this->cli_help['synopsis'] = 'toolkey ###OPTIONS###';
92 $this->cli_help['description'] = 'Dispatches to various analysis and clean-up tools which can plug into the API of this script. Typically you can run tests that will take longer than the usual max execution time of PHP. Such tasks could be checking for orphan records in the page tree or flushing all published versions in the system. For the complete list of options, please explore each of the \'toolkey\' keywords below:
93
94 ' . implode('
95 ', array_keys($this->cleanerModules));
96 $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner missing_files -s -r
97 This will show you missing files in the TYPO3 system and only report back if errors were found.';
98 $this->cli_help['author'] = 'Kasper Skaarhoej, (c) 2006';
99 }
100
101 /**************************
102 *
103 * CLI functionality
104 *
105 *************************/
106 /**
107 * CLI engine
108 *
109 * @param array $argv Command line arguments
110 * @return string
111 * @deprecated the Cleaner Command is deprecated since TYPO3 v8 and will be removed in TYPO3 v9, use a separate CLI Command instead
112 */
113 public function cli_main($argv)
114 {
115 GeneralUtility::logDeprecatedFunction();
116 $this->cli_setArguments($argv);
117
118 // Force user to admin state and set workspace to "Live":
119 $GLOBALS['BE_USER']->user['admin'] = 1;
120 $GLOBALS['BE_USER']->setWorkspace(0);
121 // Print Howto:
122 if ($this->cli_isArg('--showhowto')) {
123 $howto = file_get_contents(ExtensionManagementUtility::extPath('lowlevel') . 'README.rst');
124 echo wordwrap($howto, 120) . LF;
125 die;
126 }
127 // Print help
128 $analysisType = (string)$this->cli_args['_DEFAULT'][1];
129 if (!$analysisType) {
130 $this->cli_validateArgs();
131 $this->cli_help();
132 die;
133 }
134
135 if (is_array($this->cleanerModules[$analysisType])) {
136 $cleanerMode = GeneralUtility::getUserObj($this->cleanerModules[$analysisType][0]);
137 $cleanerMode->cli_validateArgs();
138 // Run it...
139 if ($this->cli_isArg('-r')) {
140 if (!$cleanerMode->checkRefIndex || $this->cli_referenceIndexCheck()) {
141 $res = $cleanerMode->main();
142 $this->cli_printInfo($analysisType, $res);
143 // Autofix...
144 if ($this->cli_isArg('--AUTOFIX')) {
145 if ($this->cli_isArg('--YES') || $this->cli_keyboardInput_yes('
146
147 NOW Running --AUTOFIX on result. OK?' . ($this->cli_isArg('--dryrun') ? ' (--dryrun simulation)' : ''))) {
148 $cleanerMode->main_autofix($res);
149 } else {
150 $this->cli_echo('ABORTING AutoFix...
151 ', 1);
152 }
153 }
154 }
155 } else {
156 // Help only...
157 $cleanerMode->cli_help();
158 die;
159 }
160 } else {
161 $this->cli_echo('ERROR: Analysis Type \'' . $analysisType . '\' is unknown.
162 ', 1);
163 die;
164 }
165 }
166
167 /**
168 * Checks reference index
169 *
170 * @return bool TRUE if reference index was OK (either OK, updated or ignored)
171 */
172 public function cli_referenceIndexCheck()
173 {
174 // Reference index option:
175 $refIndexMode = isset($this->cli_args['--refindex']) ? $this->cli_args['--refindex'][0] : 'check';
176 switch ($refIndexMode) {
177 case 'check':
178
179 case 'update':
180 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
181 list($headerContent, $bodyContent, $errorCount) = $refIndexObj->updateIndex($refIndexMode === 'check', $this->cli_echo());
182 if ($errorCount && $refIndexMode === 'check') {
183 $ok = false;
184 $this->cli_echo('ERROR: Reference Index Check failed! (run with \'--refindex update\' to fix)
185 ', 1);
186 } else {
187 $ok = true;
188 }
189 break;
190 case 'ignore':
191 $this->cli_echo('Reference Index Check: Bypassing reference index check...
192 ');
193 $ok = true;
194 break;
195 default:
196 $this->cli_echo('ERROR: Wrong value for --refindex argument.
197 ', 1);
198 die();
199
200 }
201 return $ok;
202 }
203
204 /**
205 * @param string $matchString
206 * @return string If string, it's the reason for not executing. Returning FALSE means it should execute.
207 */
208 public function cli_noExecutionCheck($matchString)
209 {
210 // Check for filter:
211 if ($this->cli_isArg('--filterRegex') && ($regex = $this->cli_argValue('--filterRegex', 0))) {
212 if (!preg_match($regex, $matchString)) {
213 return 'BYPASS: Filter Regex "' . $regex . '" did not match string "' . $matchString . '"';
214 }
215 }
216 // Check for interactive mode
217 if ($this->cli_isArg('-i')) {
218 if (!$this->cli_keyboardInput_yes(' EXECUTE?')) {
219 return 'BYPASS...';
220 }
221 }
222 // Check for
223 if ($this->cli_isArg('--dryrun')) {
224 return 'BYPASS: --dryrun set';
225 }
226 }
227
228 /**
229 * Formats a result array from a test so it fits output in the shell
230 *
231 * @param string $header Name of the test (eg. function name)
232 * @param array $res Result array from an analyze function
233 * @return void Outputs with echo - capture content with output buffer if needed.
234 */
235 public function cli_printInfo($header, $res)
236 {
237 $detailLevel = MathUtility::forceIntegerInRange($this->cli_isArg('-v') ? $this->cli_argValue('-v') : 1, 0, 3);
238 $silent = !$this->cli_echo();
239 $severity = [
240 0 => 'MESSAGE',
241 1 => 'INFO',
242 2 => 'WARNING',
243 3 => 'ERROR'
244 ];
245 // Header output:
246 if ($detailLevel <= 1) {
247 $this->cli_echo('*********************************************
248 ' . $header . LF . '*********************************************
249 ');
250 $this->cli_echo(wordwrap(trim($res['message'])) . LF . LF);
251 }
252 // Traverse headers for output:
253 if (is_array($res['headers'])) {
254 foreach ($res['headers'] as $key => $value) {
255 if ($detailLevel <= (int)$value[2]) {
256 if (is_array($res[$key]) && (count($res[$key]) || !$silent)) {
257 // Header and explanaion:
258 $this->cli_echo('---------------------------------------------' . LF, 1);
259 $this->cli_echo('[' . $header . ']' . LF, 1);
260 $this->cli_echo($value[0] . ' [' . $severity[$value[2]] . ']' . LF, 1);
261 $this->cli_echo('---------------------------------------------' . LF, 1);
262 if (trim($value[1])) {
263 $this->cli_echo('Explanation: ' . wordwrap(trim($value[1])) . LF . LF, 1);
264 }
265 }
266 // Content:
267 if (is_array($res[$key])) {
268 if (count($res[$key])) {
269 if ($this->cli_echo('', 1)) {
270 print_r($res[$key]);
271 }
272 } else {
273 $this->cli_echo('(None)' . LF . LF);
274 }
275 } else {
276 $this->cli_echo($res[$key] . LF . LF);
277 }
278 }
279 }
280 }
281 }
282
283 /**************************
284 *
285 * Page tree traversal
286 *
287 *************************/
288 /**
289 * Traverses the FULL/part of page tree, mainly to register ALL validly connected records (to find orphans) but also to register deleted records, versions etc.
290 * Output (in $this->recStats) can be useful for multiple purposes.
291 *
292 * @param int $rootID Root page id from where to start traversal. Use "0" (zero) to have full page tree (necessary when spotting orphans, otherwise you can run it on parts only)
293 * @param int $depth Depth to traverse. zero is do not traverse at all. 1 = 1 sublevel, 1000= 1000 sublevels (all...)
294 * @param int $echoLevel If >0, will echo information about the traversal process.
295 * @param string $callBack Call back function (from this class or subclass)
296 * @return void
297 */
298 public function genTree($rootID, $depth = 1000, $echoLevel = 0, $callBack = '')
299 {
300 // Initialize:
301 if (ExtensionManagementUtility::isLoaded('workspaces')) {
302 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
303 ->getQueryBuilderForTable('sys_workspace');
304
305 $queryBuilder->getRestrictions()
306 ->removeAll()
307 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
308
309 $workspaceRecords = $queryBuilder
310 ->select('uid', 'title')
311 ->from('sys_workspace')
312 ->execute()
313 ->fetchAll();
314
315 foreach ($workspaceRecords as $workspaceRecord) {
316 $this->workspaceIndex[$workspaceRecord['uid']] = true;
317 }
318 }
319
320 $this->recStats = [
321 'all' => [],
322 // All records connected in tree including versions (the reverse are orphans). All Info and Warning categories below are included here (and therefore safe if you delete the reverse of the list)
323 'deleted' => [],
324 // Subset of "alL" that are deleted-flagged [Info]
325 'versions' => [],
326 // Subset of "all" which are offline versions (pid=-1). [Info]
327 'versions_published' => [],
328 // Subset of "versions" that is a count of 1 or more (has been published) [Info]
329 'versions_liveWS' => [],
330 // Subset of "versions" that exists in live workspace [Info]
331 'versions_lost_workspace' => [],
332 // Subset of "versions" that doesn't belong to an existing workspace [Warning: Fix by move to live workspace]
333 'versions_inside_versioned_page' => [],
334 // Subset of "versions" This is versions of elements found inside an already versioned branch / page. In real life this can work out, but is confusing and the backend should prevent this from happening to people. [Warning: Fix by deleting those versions (or publishing them)]
335 'illegal_record_under_versioned_page' => [],
336 // If a page is "element" or "page" version and records are found attached to it, they might be illegally attached, so this will tell you. [Error: Fix by deleting orphans since they are not registered in "all" category]
337 'misplaced_at_rootlevel' => [],
338 // Subset of "all": Those that should not be at root level but are. [Warning: Fix by moving record into page tree]
339 'misplaced_inside_tree' => []
340 ];
341 // Start traversal:
342 $this->genTree_traverse($rootID, $depth, $echoLevel, $callBack);
343 // Sort recStats (for diff'able displays)
344 foreach ($this->recStats as $kk => $vv) {
345 foreach ($this->recStats[$kk] as $tables => $recArrays) {
346 ksort($this->recStats[$kk][$tables]);
347 }
348 ksort($this->recStats[$kk]);
349 }
350 if ($echoLevel > 0) {
351 echo LF . LF;
352 }
353 }
354
355 /**
356 * Recursive traversal of page tree:
357 *
358 * @param int $rootID Page root id (must be online, valid page record - or zero for page tree root)
359 * @param int $depth Depth
360 * @param int $echoLevel Echo Level
361 * @param string $callBack Call back function (from this class or subclass)
362 * @param bool $versionSwapmode DON'T set from outside, internal. (indicates we are inside a version of a page)
363 * @param int $rootIsVersion DON'T set from outside, internal. (1: Indicates that rootID is a version of a page, 2: ...that it is even a version of a version (which triggers a warning!)
364 * @param string $accumulatedPath Internal string that accumulates the path
365 * @return void
366 * @access private
367 */
368 public function genTree_traverse($rootID, $depth, $echoLevel = 0, $callBack = '', $versionSwapmode = false, $rootIsVersion = 0, $accumulatedPath = '')
369 {
370 // Register page:
371 $this->recStats['all']['pages'][$rootID] = $rootID;
372 $pageRecord = BackendUtility::getRecordRaw('pages', 'uid=' . (int)$rootID, 'deleted,title,t3ver_count,t3ver_wsid');
373 $accumulatedPath .= '/' . $pageRecord['title'];
374 // Register if page is deleted:
375 if ($pageRecord['deleted']) {
376 $this->recStats['deleted']['pages'][$rootID] = $rootID;
377 }
378 // 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.
379 if ($rootIsVersion) {
380 $this->recStats['versions']['pages'][$rootID] = $rootID;
381 // If it has been published and is in archive now...
382 if ($pageRecord['t3ver_count'] >= 1 && $pageRecord['t3ver_wsid'] == 0) {
383 $this->recStats['versions_published']['pages'][$rootID] = $rootID;
384 }
385 // If it has been published and is in archive now...
386 if ($pageRecord['t3ver_wsid'] == 0) {
387 $this->recStats['versions_liveWS']['pages'][$rootID] = $rootID;
388 }
389 // If it doesn't belong to a workspace...
390 if (!isset($this->workspaceIndex[$pageRecord['t3ver_wsid']])) {
391 $this->recStats['versions_lost_workspace']['pages'][$rootID] = $rootID;
392 }
393 // In case the rootID is a version inside a versioned page
394 if ($rootIsVersion == 2) {
395 $this->recStats['versions_inside_versioned_page']['pages'][$rootID] = $rootID;
396 }
397 }
398 if ($echoLevel > 0) {
399 echo LF . $accumulatedPath . ' [' . $rootID . ']' . ($pageRecord['deleted'] ? ' (DELETED)' : '') . ($this->recStats['versions_published']['pages'][$rootID] ? ' (PUBLISHED)' : '');
400 }
401 if ($echoLevel > 1 && $this->recStats['versions_lost_workspace']['pages'][$rootID]) {
402 echo LF . ' ERROR! This version belongs to non-existing workspace (' . $pageRecord['t3ver_wsid'] . ')!';
403 }
404 if ($echoLevel > 1 && $this->recStats['versions_inside_versioned_page']['pages'][$rootID]) {
405 echo LF . ' WARNING! This version is inside an already versioned page or branch!';
406 }
407 // Call back:
408 if ($callBack) {
409 $this->{$callBack}('pages', $rootID, $echoLevel, $versionSwapmode, $rootIsVersion);
410 }
411 // Traverse tables of records that belongs to page:
412 foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
413 if ($tableName !== 'pages') {
414 // Select all records belonging to page:
415 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
416 ->getQueryBuilderForTable($tableName);
417
418 $queryBuilder->getRestrictions()->removeAll();
419
420 $queryBuilder
421 ->select('uid')
422 ->from($tableName)
423 ->where(
424 $queryBuilder->expr()->eq(
425 'pid',
426 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
427 )
428 );
429
430 if ($GLOBALS['TCA'][$tableName]['ctrl']['delete']) {
431 $queryBuilder->addSelect($GLOBALS['TCA'][$tableName]['ctrl']['delete']);
432 }
433
434 if (!$this->genTree_traverseDeleted) {
435 $queryBuilder->getRestrictions()->add(DeletedRestriction::class);
436 }
437
438 $result = $queryBuilder->execute();
439
440 $count = $result->rowCount();
441 if ($count) {
442 if ($echoLevel == 2) {
443 echo LF . ' \\-' . $tableName . ' (' . $count . ')';
444 }
445 }
446 while ($rowSub = $result->fetch()) {
447 if ($echoLevel == 3) {
448 echo LF . ' \\-' . $tableName . ':' . $rowSub['uid'];
449 }
450 // If the rootID represents an "element" or "page" version type, we must check if the record from this table is allowed to belong to this:
451 if ($versionSwapmode) {
452 // This is illegal records under a versioned page - therefore not registered in $this->recStats['all'] so they should be orphaned:
453 $this->recStats['illegal_record_under_versioned_page'][$tableName][$rowSub['uid']] = $rowSub['uid'];
454 if ($echoLevel > 1) {
455 echo LF . ' ERROR! Illegal record (' . $tableName . ':' . $rowSub['uid'] . ') under versioned page!';
456 }
457 } else {
458 $this->recStats['all'][$tableName][$rowSub['uid']] = $rowSub['uid'];
459 // Register deleted:
460 if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $rowSub[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
461 $this->recStats['deleted'][$tableName][$rowSub['uid']] = $rowSub['uid'];
462 if ($echoLevel == 3) {
463 echo ' (DELETED)';
464 }
465 }
466 // Check location of records regarding tree root:
467 if (!$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] && $rootID == 0) {
468 $this->recStats['misplaced_at_rootlevel'][$tableName][$rowSub['uid']] = $rowSub['uid'];
469 if ($echoLevel > 1) {
470 echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') on rootlevel!';
471 }
472 }
473 if ($GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] == 1 && $rootID > 0) {
474 $this->recStats['misplaced_inside_tree'][$tableName][$rowSub['uid']] = $rowSub['uid'];
475 if ($echoLevel > 1) {
476 echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') inside page tree!';
477 }
478 }
479 // Traverse plugins:
480 if ($callBack) {
481 $this->{$callBack}($tableName, $rowSub['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
482 }
483 // Add any versions of those records:
484 if ($this->genTree_traverseVersions) {
485 $versions = BackendUtility::selectVersionsOfRecord($tableName, $rowSub['uid'], 'uid,t3ver_wsid,t3ver_count' . ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ? ',' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] : ''), null, true);
486 if (is_array($versions)) {
487 foreach ($versions as $verRec) {
488 if (!$verRec['_CURRENT_VERSION']) {
489 if ($echoLevel == 3) {
490 echo LF . ' \\-[#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . '] ' . $tableName . ':' . $verRec['uid'] . ')';
491 }
492 $this->recStats['all'][$tableName][$verRec['uid']] = $verRec['uid'];
493 // Register deleted:
494 if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $verRec[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
495 $this->recStats['deleted'][$tableName][$verRec['uid']] = $verRec['uid'];
496 if ($echoLevel == 3) {
497 echo ' (DELETED)';
498 }
499 }
500 // Register version:
501 $this->recStats['versions'][$tableName][$verRec['uid']] = $verRec['uid'];
502 if ($verRec['t3ver_count'] >= 1 && $verRec['t3ver_wsid'] == 0) {
503 // Only register published versions in LIVE workspace (published versions in draft workspaces are allowed)
504 $this->recStats['versions_published'][$tableName][$verRec['uid']] = $verRec['uid'];
505 if ($echoLevel == 3) {
506 echo ' (PUBLISHED)';
507 }
508 }
509 if ($verRec['t3ver_wsid'] == 0) {
510 $this->recStats['versions_liveWS'][$tableName][$verRec['uid']] = $verRec['uid'];
511 }
512 if (!isset($this->workspaceIndex[$verRec['t3ver_wsid']])) {
513 $this->recStats['versions_lost_workspace'][$tableName][$verRec['uid']] = $verRec['uid'];
514 if ($echoLevel > 1) {
515 echo LF . ' ERROR! Version (' . $tableName . ':' . $verRec['uid'] . ') belongs to non-existing workspace (' . $verRec['t3ver_wsid'] . ')!';
516 }
517 }
518 // In case we are inside a versioned branch, there should not exists versions inside that "branch".
519 if ($versionSwapmode) {
520 $this->recStats['versions_inside_versioned_page'][$tableName][$verRec['uid']] = $verRec['uid'];
521 if ($echoLevel > 1) {
522 echo LF . ' ERROR! This version (' . $tableName . ':' . $verRec['uid'] . ') is inside an already versioned page or branch!';
523 }
524 }
525 // Traverse plugins:
526 if ($callBack) {
527 $this->{$callBack}($tableName, $verRec['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
528 }
529 }
530 }
531 }
532 unset($versions);
533 }
534 }
535 }
536 }
537 }
538 unset($resSub);
539 unset($rowSub);
540 // Find subpages to root ID and traverse (only when rootID is not a version or is a branch-version):
541 if ($depth > 0) {
542 $depth--;
543 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
544 ->getQueryBuilderForTable('pages');
545
546 $queryBuilder->getRestrictions()->removeAll();
547 if (!$this->genTree_traverseDeleted) {
548 $queryBuilder->getRestrictions()->add(DeletedRestriction::class);
549 }
550
551 $queryBuilder
552 ->select('uid')
553 ->from('pages')
554 ->where(
555 $queryBuilder->expr()->eq(
556 'pid',
557 $queryBuilder->createNamedParameter($rootID, \PDO::PARAM_INT)
558 )
559 )
560 ->orderBy('sorting');
561
562 $result = $queryBuilder->execute();
563 while ($row = $result->fetch()) {
564 $this->genTree_traverse($row['uid'], $depth, $echoLevel, $callBack, $versionSwapmode, 0, $accumulatedPath);
565 }
566 }
567 // Add any versions of pages
568 if ($rootID > 0 && $this->genTree_traverseVersions) {
569 $versions = BackendUtility::selectVersionsOfRecord('pages', $rootID, 'uid,t3ver_oid,t3ver_wsid,t3ver_count', null, true);
570 if (is_array($versions)) {
571 foreach ($versions as $verRec) {
572 if (!$verRec['_CURRENT_VERSION']) {
573 $this->genTree_traverse($verRec['uid'], $depth, $echoLevel, $callBack, true, $versionSwapmode ? 2 : 1, $accumulatedPath . ' [#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . ']');
574 }
575 }
576 }
577 }
578 }
579
580 /**************************
581 *
582 * Helper functions
583 *
584 *************************/
585 /**
586 * Compile info-string
587 *
588 * @param array $rec Input record from sys_refindex
589 * @return string String identifying the main record of the reference
590 */
591 public function infoStr($rec)
592 {
593 return $rec['tablename'] . ':' . $rec['recuid'] . ':' . $rec['field'] . ':' . $rec['flexpointer'] . ':' . $rec['softref_key'] . ($rec['deleted'] ? ' (DELETED)' : '');
594 }
595 }