[TASK] Rebuild the calcAge functionality
[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\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\DateTimeUtility;
20
21 /**
22 * Core functions for cleaning and analysing
23 *
24 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
25 */
26 /**
27 * Core functions for cleaning and analysing
28 *
29 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
30 */
31 class CleanerCommand extends \TYPO3\CMS\Core\Controller\CommandLineController {
32
33 /**
34 * @todo Define visibility
35 */
36 public $genTree_traverseDeleted = TRUE;
37
38 /**
39 * @todo Define visibility
40 */
41 public $genTree_traverseVersions = TRUE;
42
43 /**
44 * @todo Define visibility
45 */
46 public $label_infoString = 'The list of records is organized as [table]:[uid]:[field]:[flexpointer]:[softref_key]';
47
48 /**
49 * @todo Define visibility
50 */
51 public $pagetreePlugins = array();
52
53 /**
54 * @todo Define visibility
55 */
56 public $cleanerModules = array();
57
58 /**
59 * @todo Define visibility
60 */
61 public $performanceStatistics = array();
62
63 protected $workspaceIndex = array();
64
65 /**
66 * Constructor
67 *
68 * @todo Define visibility
69 */
70 public function __construct() {
71 // Running parent class constructor
72 parent::__construct();
73 $this->cleanerModules = (array) $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules'];
74 // Adding options to help archive:
75 $this->cli_options[] = array('-r', 'Execute this tool, otherwise help is shown');
76 $this->cli_options[] = array('-v level', 'Verbosity level 0-3', 'The value of level can be:
77 0 = all output
78 1 = info and greater (default)
79 2 = warnings and greater
80 3 = errors');
81 $this->cli_options[] = array('--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")');
82 $this->cli_options[] = array('--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.');
83 $this->cli_options[] = array('--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');
84 $this->cli_options[] = array('--YES', 'Implicit YES to all questions', 'Use this with EXTREME care. The option "-i" is not affected by this option.');
85 $this->cli_options[] = array('-i', 'Interactive', 'Will ask you before running the AUTOFIX on each element.');
86 $this->cli_options[] = array('--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!');
87 $this->cli_options[] = array('--showhowto', 'Displays HOWTO file for cleaner script.');
88 // Setting help texts:
89 $this->cli_help['name'] = 'lowlevel_cleaner -- Analysis and clean-up tools for TYPO3 installations';
90 $this->cli_help['synopsis'] = 'toolkey ###OPTIONS###';
91 $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:
92
93 ' . implode('
94 ', array_keys($this->cleanerModules));
95 $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner missing_files -s -r
96 This will show you missing files in the TYPO3 system and only report back if errors were found.';
97 $this->cli_help['author'] = 'Kasper Skaarhoej, (c) 2006';
98 }
99
100 /**************************
101 *
102 * CLI functionality
103 *
104 *************************/
105 /**
106 * CLI engine
107 *
108 * @param array $argv Command line arguments
109 * @return string
110 * @todo Define visibility
111 */
112 public function cli_main($argv) {
113
114 $this->cli_setArguments($argv);
115
116 // Force user to admin state and set workspace to "Live":
117 $GLOBALS['BE_USER']->user['admin'] = 1;
118 $GLOBALS['BE_USER']->setWorkspace(0);
119 // Print Howto:
120 if ($this->cli_isArg('--showhowto')) {
121 $howto = GeneralUtility::getUrl(\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('lowlevel') . 'HOWTO_clean_up_TYPO3_installations.txt');
122 echo wordwrap($howto, 120) . LF;
123 die;
124 }
125 // Print help
126 $analysisType = (string) $this->cli_args['_DEFAULT'][1];
127 if (!$analysisType) {
128 $this->cli_validateArgs();
129 $this->cli_help();
130 die;
131 }
132
133 if (is_array($this->cleanerModules[$analysisType])) {
134 $cleanerMode = GeneralUtility::getUserObj($this->cleanerModules[$analysisType][0]);
135 $cleanerMode->cli_validateArgs();
136 // Run it...
137 if ($this->cli_isArg('-r')) {
138 if (!$cleanerMode->checkRefIndex || $this->cli_referenceIndexCheck()) {
139 $res = $cleanerMode->main();
140 $this->cli_printInfo($analysisType, $res);
141 // Autofix...
142 if ($this->cli_isArg('--AUTOFIX')) {
143 if ($this->cli_isArg('--YES') || $this->cli_keyboardInput_yes('
144
145 NOW Running --AUTOFIX on result. OK?' . ($this->cli_isArg('--dryrun') ? ' (--dryrun simulation)' : ''))) {
146 $cleanerMode->main_autofix($res);
147 } else {
148 $this->cli_echo('ABORTING AutoFix...
149 ', 1);
150 }
151 }
152 }
153 } else {
154 // Help only...
155 $cleanerMode->cli_help();
156 die;
157 }
158 } else {
159 $this->cli_echo('ERROR: Analysis Type \'' . $analysisType . '\' is unknown.
160 ', 1);
161 die;
162 }
163 }
164
165 /**
166 * Checks reference index
167 *
168 * @return boolean TRUE if reference index was OK (either OK, updated or ignored)
169 * @todo Define visibility
170 */
171 public function cli_referenceIndexCheck() {
172 // Reference index option:
173 $refIndexMode = isset($this->cli_args['--refindex']) ? $this->cli_args['--refindex'][0] : 'check';
174 if (!GeneralUtility::inList('update,ignore,check', $refIndexMode)) {
175 $this->cli_echo('ERROR: Wrong value for --refindex argument.
176 ', 1);
177 die;
178 }
179 switch ($refIndexMode) {
180 case 'check':
181
182 case 'update':
183 $refIndexObj = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Database\\ReferenceIndex');
184 list($headerContent, $bodyContent, $errorCount) = $refIndexObj->updateIndex($refIndexMode == 'check', $this->cli_echo());
185 if ($errorCount && $refIndexMode == 'check') {
186 $ok = FALSE;
187 $this->cli_echo('ERROR: Reference Index Check failed! (run with \'--refindex update\' to fix)
188 ', 1);
189 } else {
190 $ok = TRUE;
191 }
192 break;
193 case 'ignore':
194 $this->cli_echo('Reference Index Check: Bypassing reference index check...
195 ');
196 $ok = TRUE;
197 break;
198 }
199 return $ok;
200 }
201
202 /**
203 * @param string $matchString
204 * @return string If string, it's the reason for not executing. Returning FALSE means it should execute.
205 * @todo Define visibility
206 */
207 public function cli_noExecutionCheck($matchString) {
208 // Check for filter:
209 if ($this->cli_isArg('--filterRegex') && ($regex = $this->cli_argValue('--filterRegex', 0))) {
210 if (!preg_match($regex, $matchString)) {
211 return 'BYPASS: Filter Regex "' . $regex . '" did not match string "' . $matchString . '"';
212 }
213 }
214 // Check for interactive mode
215 if ($this->cli_isArg('-i')) {
216 if (!$this->cli_keyboardInput_yes(' EXECUTE?')) {
217 return 'BYPASS...';
218 }
219 }
220 // Check for
221 if ($this->cli_isArg('--dryrun')) {
222 return 'BYPASS: --dryrun set';
223 }
224 }
225
226 /**
227 * Formats a result array from a test so it fits output in the shell
228 *
229 * @param string $header Name of the test (eg. function name)
230 * @param array $res Result array from an analyze function
231 * @return void Outputs with echo - capture content with output buffer if needed.
232 * @todo Define visibility
233 */
234 public function cli_printInfo($header, $res) {
235 $detailLevel = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->cli_isArg('-v') ? $this->cli_argValue('-v') : 1, 0, 3);
236 $silent = !$this->cli_echo();
237 $severity = array(
238 0 => 'MESSAGE',
239 1 => 'INFO',
240 2 => 'WARNING',
241 3 => 'ERROR'
242 );
243 // Header output:
244 if ($detailLevel <= 1) {
245 $this->cli_echo('*********************************************
246 ' . $header . LF . '*********************************************
247 ');
248 $this->cli_echo(wordwrap(trim($res['message'])) . LF . LF);
249 }
250 // Traverse headers for output:
251 if (is_array($res['headers'])) {
252 foreach ($res['headers'] as $key => $value) {
253 if ($detailLevel <= (int)$value[2]) {
254 if (is_array($res[$key]) && (count($res[$key]) || !$silent)) {
255 // Header and explanaion:
256 $this->cli_echo('---------------------------------------------' . LF, 1);
257 $this->cli_echo('[' . $header . ']' . LF, 1);
258 $this->cli_echo($value[0] . ' [' . $severity[$value[2]] . ']' . LF, 1);
259 $this->cli_echo('---------------------------------------------' . LF, 1);
260 if (trim($value[1])) {
261 $this->cli_echo('Explanation: ' . wordwrap(trim($value[1])) . LF . LF, 1);
262 }
263 }
264 // Content:
265 if (is_array($res[$key])) {
266 if (count($res[$key])) {
267 if ($this->cli_echo('', 1)) {
268 print_r($res[$key]);
269 }
270 } else {
271 $this->cli_echo('(None)' . LF . LF);
272 }
273 } else {
274 $this->cli_echo($res[$key] . LF . LF);
275 }
276 }
277 }
278 }
279 }
280
281 /**************************
282 *
283 * Page tree traversal
284 *
285 *************************/
286 /**
287 * 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.
288 * Output (in $this->recStats) can be useful for multiple purposes.
289 *
290 * @param integer $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)
291 * @param integer $depth Depth to traverse. zero is do not traverse at all. 1 = 1 sublevel, 1000= 1000 sublevels (all...)
292 * @param boolean $echoLevel If >0, will echo information about the traversal process.
293 * @param string $callBack Call back function (from this class or subclass)
294 * @return void
295 * @todo Define visibility
296 */
297 public function genTree($rootID, $depth = 1000, $echoLevel = 0, $callBack = '') {
298 $pt = DateTimeUtility::milliseconds();
299 $this->performanceStatistics['genTree()'] = '';
300 // Initialize:
301 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
302 $this->workspaceIndex = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,title', 'sys_workspace', '1=1' . BackendUtility::deleteClause('sys_workspace'), '', '', '', 'uid');
303 }
304 $this->workspaceIndex[-1] = TRUE;
305 $this->workspaceIndex[0] = TRUE;
306 $this->recStats = array(
307 'all' => array(),
308 // 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)
309 'deleted' => array(),
310 // Subset of "alL" that are deleted-flagged [Info]
311 'versions' => array(),
312 // Subset of "all" which are offline versions (pid=-1). [Info]
313 'versions_published' => array(),
314 // Subset of "versions" that is a count of 1 or more (has been published) [Info]
315 'versions_liveWS' => array(),
316 // Subset of "versions" that exists in live workspace [Info]
317 'versions_lost_workspace' => array(),
318 // Subset of "versions" that doesn't belong to an existing workspace [Warning: Fix by move to live workspace]
319 'versions_inside_versioned_page' => array(),
320 // 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)]
321 'illegal_record_under_versioned_page' => array(),
322 // 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]
323 'misplaced_at_rootlevel' => array(),
324 // Subset of "all": Those that should not be at root level but are. [Warning: Fix by moving record into page tree]
325 'misplaced_inside_tree' => array()
326 );
327 // Start traversal:
328 $pt2 = DateTimeUtility::milliseconds();
329 $this->performanceStatistics['genTree_traverse()'] = '';
330 $this->performanceStatistics['genTree_traverse():TraverseTables'] = '';
331 $this->genTree_traverse($rootID, $depth, $echoLevel, $callBack);
332 $this->performanceStatistics['genTree_traverse()'] = DateTimeUtility::milliseconds() - $pt2;
333 // Sort recStats (for diff'able displays)
334 foreach ($this->recStats as $kk => $vv) {
335 foreach ($this->recStats[$kk] as $tables => $recArrays) {
336 ksort($this->recStats[$kk][$tables]);
337 }
338 ksort($this->recStats[$kk]);
339 }
340 if ($echoLevel > 0) {
341 echo LF . LF;
342 }
343 // Processing performance statistics:
344 $this->performanceStatistics['genTree()'] = DateTimeUtility::milliseconds() - $pt;
345 // Count records:
346 foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
347 // Select all records belonging to page:
348 $resSub = $GLOBALS['TYPO3_DB']->exec_SELECTquery('count(*)', $tableName, '');
349 $countRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($resSub);
350 $this->performanceStatistics['MySQL_count'][$tableName] = $countRow['count(*)'];
351 $this->performanceStatistics['CSV'] .= LF . $tableName . ',' . $this->performanceStatistics['genTree_traverse():TraverseTables:']['MySQL'][$tableName] . ',' . $this->performanceStatistics['genTree_traverse():TraverseTables:']['Proc'][$tableName] . ',' . $this->performanceStatistics['MySQL_count'][$tableName];
352 }
353 $this->performanceStatistics['recStats_size']['(ALL)'] = strlen(serialize($this->recStats));
354 foreach ($this->recStats as $key => $arrcontent) {
355 $this->performanceStatistics['recStats_size'][$key] = strlen(serialize($arrcontent));
356 }
357 }
358
359 /**
360 * Recursive traversal of page tree:
361 *
362 * @param integer $rootID Page root id (must be online, valid page record - or zero for page tree root)
363 * @param integer $depth Depth
364 * @param integer $echoLevel Echo Level
365 * @param string $callBack Call back function (from this class or subclass)
366 * @param string $versionSwapmode DON'T set from outside, internal. (indicates we are inside a version of a page) - will be "SWAPMODE:-1" or empty
367 * @param integer $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!)
368 * @param string $accumulatedPath Internal string that accumulates the path
369 * @return void
370 * @access private
371 * @todo $versionSwapmode needs to be cleaned up, since page and branch version (0, 1) does not exist anymore
372 * @todo Define visibility
373 */
374 public function genTree_traverse($rootID, $depth, $echoLevel = 0, $callBack = '', $versionSwapmode = '', $rootIsVersion = 0, $accumulatedPath = '') {
375 // Register page:
376 $this->recStats['all']['pages'][$rootID] = $rootID;
377 $pageRecord = BackendUtility::getRecordRaw('pages', 'uid=' . (int)$rootID, 'deleted,title,t3ver_count,t3ver_wsid');
378 $accumulatedPath .= '/' . $pageRecord['title'];
379 // Register if page is deleted:
380 if ($pageRecord['deleted']) {
381 $this->recStats['deleted']['pages'][$rootID] = $rootID;
382 }
383 // 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.
384 if ($rootIsVersion) {
385 $this->recStats['versions']['pages'][$rootID] = $rootID;
386 // If it has been published and is in archive now...
387 if ($pageRecord['t3ver_count'] >= 1 && $pageRecord['t3ver_wsid'] == 0) {
388 $this->recStats['versions_published']['pages'][$rootID] = $rootID;
389 }
390 // If it has been published and is in archive now...
391 if ($pageRecord['t3ver_wsid'] == 0) {
392 $this->recStats['versions_liveWS']['pages'][$rootID] = $rootID;
393 }
394 // If it doesn't belong to a workspace...
395 if (!isset($this->workspaceIndex[$pageRecord['t3ver_wsid']])) {
396 $this->recStats['versions_lost_workspace']['pages'][$rootID] = $rootID;
397 }
398 // In case the rootID is a version inside a versioned page
399 if ($rootIsVersion == 2) {
400 $this->recStats['versions_inside_versioned_page']['pages'][$rootID] = $rootID;
401 }
402 }
403 if ($echoLevel > 0) {
404 echo LF . $accumulatedPath . ' [' . $rootID . ']' . ($pageRecord['deleted'] ? ' (DELETED)' : '') . ($this->recStats['versions_published']['pages'][$rootID] ? ' (PUBLISHED)' : '');
405 }
406 if ($echoLevel > 1 && $this->recStats['versions_lost_workspace']['pages'][$rootID]) {
407 echo LF . ' ERROR! This version belongs to non-existing workspace (' . $pageRecord['t3ver_wsid'] . ')!';
408 }
409 if ($echoLevel > 1 && $this->recStats['versions_inside_versioned_page']['pages'][$rootID]) {
410 echo LF . ' WARNING! This version is inside an already versioned page or branch!';
411 }
412 // Call back:
413 if ($callBack) {
414 $this->{$callBack}('pages', $rootID, $echoLevel, $versionSwapmode, $rootIsVersion);
415 }
416 $pt3 = DateTimeUtility::milliseconds();
417 // Traverse tables of records that belongs to page:
418 foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
419 if ($tableName != 'pages') {
420 // Select all records belonging to page:
421 $pt4 = DateTimeUtility::milliseconds();
422 $resSub = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid' . ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ? ',' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] : ''), $tableName, 'pid=' . (int)$rootID . ($this->genTree_traverseDeleted ? '' : BackendUtility::deleteClause($tableName)));
423 $this->performanceStatistics['genTree_traverse():TraverseTables:']['MySQL']['(ALL)'] += DateTimeUtility::milliseconds() - $pt4;
424 $this->performanceStatistics['genTree_traverse():TraverseTables:']['MySQL'][$tableName] += DateTimeUtility::milliseconds() - $pt4;
425 $pt5 = DateTimeUtility::milliseconds();
426 $count = $GLOBALS['TYPO3_DB']->sql_num_rows($resSub);
427 if ($count) {
428 if ($echoLevel == 2) {
429 echo LF . ' \\-' . $tableName . ' (' . $count . ')';
430 }
431 }
432 while ($rowSub = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($resSub)) {
433 if ($echoLevel == 3) {
434 echo LF . ' \\-' . $tableName . ':' . $rowSub['uid'];
435 }
436 // 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:
437 if ($versionSwapmode == 'SWAPMODE:-1' || $versionSwapmode == 'SWAPMODE:0' && !$GLOBALS['TCA'][$tableName]['ctrl']['versioning_followPages']) {
438 // This is illegal records under a versioned page - therefore not registered in $this->recStats['all'] so they should be orphaned:
439 $this->recStats['illegal_record_under_versioned_page'][$tableName][$rowSub['uid']] = $rowSub['uid'];
440 if ($echoLevel > 1) {
441 echo LF . ' ERROR! Illegal record (' . $tableName . ':' . $rowSub['uid'] . ') under versioned page!';
442 }
443 } else {
444 $this->recStats['all'][$tableName][$rowSub['uid']] = $rowSub['uid'];
445 // Register deleted:
446 if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $rowSub[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
447 $this->recStats['deleted'][$tableName][$rowSub['uid']] = $rowSub['uid'];
448 if ($echoLevel == 3) {
449 echo ' (DELETED)';
450 }
451 }
452 // Check location of records regarding tree root:
453 if (!$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] && $rootID == 0) {
454 $this->recStats['misplaced_at_rootlevel'][$tableName][$rowSub['uid']] = $rowSub['uid'];
455 if ($echoLevel > 1) {
456 echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') on rootlevel!';
457 }
458 }
459 if ($GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'] == 1 && $rootID > 0) {
460 $this->recStats['misplaced_inside_tree'][$tableName][$rowSub['uid']] = $rowSub['uid'];
461 if ($echoLevel > 1) {
462 echo LF . ' ERROR! Misplaced record (' . $tableName . ':' . $rowSub['uid'] . ') inside page tree!';
463 }
464 }
465 // Traverse plugins:
466 if ($callBack) {
467 $this->{$callBack}($tableName, $rowSub['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
468 }
469 // Add any versions of those records:
470 if ($this->genTree_traverseVersions) {
471 $versions = BackendUtility::selectVersionsOfRecord($tableName, $rowSub['uid'], 'uid,t3ver_wsid,t3ver_count' . ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ? ',' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] : ''), 0, TRUE);
472 if (is_array($versions)) {
473 foreach ($versions as $verRec) {
474 if (!$verRec['_CURRENT_VERSION']) {
475 if ($echoLevel == 3) {
476 echo LF . ' \\-[#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . '] ' . $tableName . ':' . $verRec['uid'] . ')';
477 }
478 $this->recStats['all'][$tableName][$verRec['uid']] = $verRec['uid'];
479 // Register deleted:
480 if ($GLOBALS['TCA'][$tableName]['ctrl']['delete'] && $verRec[$GLOBALS['TCA'][$tableName]['ctrl']['delete']]) {
481 $this->recStats['deleted'][$tableName][$verRec['uid']] = $verRec['uid'];
482 if ($echoLevel == 3) {
483 echo ' (DELETED)';
484 }
485 }
486 // Register version:
487 $this->recStats['versions'][$tableName][$verRec['uid']] = $verRec['uid'];
488 if ($verRec['t3ver_count'] >= 1 && $verRec['t3ver_wsid'] == 0) {
489 // Only register published versions in LIVE workspace (published versions in draft workspaces are allowed)
490 $this->recStats['versions_published'][$tableName][$verRec['uid']] = $verRec['uid'];
491 if ($echoLevel == 3) {
492 echo ' (PUBLISHED)';
493 }
494 }
495 if ($verRec['t3ver_wsid'] == 0) {
496 $this->recStats['versions_liveWS'][$tableName][$verRec['uid']] = $verRec['uid'];
497 }
498 if (!isset($this->workspaceIndex[$verRec['t3ver_wsid']])) {
499 $this->recStats['versions_lost_workspace'][$tableName][$verRec['uid']] = $verRec['uid'];
500 if ($echoLevel > 1) {
501 echo LF . ' ERROR! Version (' . $tableName . ':' . $verRec['uid'] . ') belongs to non-existing workspace (' . $verRec['t3ver_wsid'] . ')!';
502 }
503 }
504 // In case we are inside a versioned branch, there should not exists versions inside that "branch".
505 if ($versionSwapmode) {
506 $this->recStats['versions_inside_versioned_page'][$tableName][$verRec['uid']] = $verRec['uid'];
507 if ($echoLevel > 1) {
508 echo LF . ' ERROR! This version (' . $tableName . ':' . $verRec['uid'] . ') is inside an already versioned page or branch!';
509 }
510 }
511 // Traverse plugins:
512 if ($callBack) {
513 $this->{$callBack}($tableName, $verRec['uid'], $echoLevel, $versionSwapmode, $rootIsVersion);
514 }
515 }
516 }
517 }
518 unset($versions);
519 }
520 }
521 }
522 $this->performanceStatistics['genTree_traverse():TraverseTables:']['Proc']['(ALL)'] += DateTimeUtility::milliseconds() - $pt5;
523 $this->performanceStatistics['genTree_traverse():TraverseTables:']['Proc'][$tableName] += DateTimeUtility::milliseconds() - $pt5;
524 }
525 }
526 unset($resSub);
527 unset($rowSub);
528 $this->performanceStatistics['genTree_traverse():TraverseTables'] += DateTimeUtility::milliseconds() - $pt3;
529 // Find subpages to root ID and traverse (only when rootID is not a version or is a branch-version):
530 if (!$versionSwapmode || $versionSwapmode == 'SWAPMODE:1') {
531 if ($depth > 0) {
532 $depth--;
533 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', 'pages', 'pid=' . (int)$rootID . ($this->genTree_traverseDeleted ? '' : BackendUtility::deleteClause('pages')), '', 'sorting');
534 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
535 $this->genTree_traverse($row['uid'], $depth, $echoLevel, $callBack, $versionSwapmode, 0, $accumulatedPath);
536 }
537 }
538 // Add any versions of pages
539 if ($rootID > 0 && $this->genTree_traverseVersions) {
540 $versions = BackendUtility::selectVersionsOfRecord('pages', $rootID, 'uid,t3ver_oid,t3ver_wsid,t3ver_count', 0, TRUE);
541 if (is_array($versions)) {
542 foreach ($versions as $verRec) {
543 if (!$verRec['_CURRENT_VERSION']) {
544 $this->genTree_traverse($verRec['uid'], $depth, $echoLevel, $callBack, 'SWAPMODE:-1', $versionSwapmode ? 2 : 1, $accumulatedPath . ' [#OFFLINE VERSION: WS#' . $verRec['t3ver_wsid'] . '/Cnt:' . $verRec['t3ver_count'] . ']');
545 }
546 }
547 }
548 }
549 }
550 }
551
552 /**************************
553 *
554 * Helper functions
555 *
556 *************************/
557 /**
558 * Compile info-string
559 *
560 * @param array $rec Input record from sys_refindex
561 * @return string String identifying the main record of the reference
562 * @todo Define visibility
563 */
564 public function infoStr($rec) {
565 return $rec['tablename'] . ':' . $rec['recuid'] . ':' . $rec['field'] . ':' . $rec['flexpointer'] . ':' . $rec['softref_key'] . ($rec['deleted'] ? ' (DELETED)' : '');
566 }
567
568 }