DatabaseIntegrityCheck::genTree()   F
last analyzed

Complexity

Conditions 13
Paths 516

Size

Total Lines 61
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 38
c 2
b 0
f 0
dl 0
loc 61
rs 3.1222
cc 13
nc 516
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Lowlevel\Integrity;
17
18
use Doctrine\DBAL\Types\Types;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Database\Connection;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
23
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24
use TYPO3\CMS\Core\Database\RelationHandler;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
27
/**
28
 * This class holds functions used by the TYPO3 backend to check the integrity
29
 * of the database (The DBint module, 'lowlevel' extension)
30
 *
31
 * Depends on \TYPO3\CMS\Core\Database\RelationHandler
32
 *
33
 * @TODO: Need to really extend this class when the DataHandler library has been
34
 * @TODO: updated and the whole API is better defined. There are some known bugs
35
 * @TODO: in this library. Further it would be nice with a facility to not only
36
 * @TODO: analyze but also clean up!
37
 * @see \TYPO3\CMS\Lowlevel\Controller\DatabaseIntegrityController::func_relations()
38
 * @see \TYPO3\CMS\Lowlevel\Controller\DatabaseIntegrityController::func_records()
39
 */
40
class DatabaseIntegrityCheck
41
{
42
    /**
43
     * @var bool If set, genTree() includes deleted pages. This is default.
44
     */
45
    protected $genTreeIncludeDeleted = true;
46
47
    /**
48
     * @var bool If set, genTree() includes versionized pages/records. This is default.
49
     */
50
    protected $genTreeIncludeVersions = true;
51
52
    /**
53
     * @var bool If set, genTree() includes records from pages.
54
     */
55
    protected $genTreeIncludeRecords = false;
56
57
    /**
58
     * @var array Will hold id/rec pairs from genTree()
59
     */
60
    protected $pageIdArray = [];
61
62
    /**
63
     * @var array Will hold id/rec pairs from genTree() that are not default language
64
     */
65
    protected $pageTranslatedPageIDArray = [];
66
67
    /**
68
     * @var array
69
     */
70
    protected $recIdArray = [];
71
72
    /**
73
     * @var array From the select-fields
74
     */
75
    protected $checkSelectDBRefs = [];
76
77
    /**
78
     * @var array From the group-fields
79
     */
80
    protected $checkGroupDBRefs = [];
81
82
    /**
83
     * @var array Statistics
84
     */
85
    protected $recStats = [
86
        'allValid' => [],
87
        'published_versions' => [],
88
        'deleted' => []
89
    ];
90
91
    /**
92
     * @var array
93
     */
94
    protected $lRecords = [];
95
96
    /**
97
     * @var string
98
     */
99
    protected $lostPagesList = '';
100
101
    /**
102
     * @return array
103
     */
104
    public function getPageTranslatedPageIDArray(): array
105
    {
106
        return $this->pageTranslatedPageIDArray;
107
    }
108
109
    /**
110
     * Generates a list of Page-uid's that corresponds to the tables in the tree.
111
     * This list should ideally include all records in the pages-table.
112
     *
113
     * @param int $theID a pid (page-record id) from which to start making the tree
114
     * @param bool $versions Internal variable, don't set from outside!
115
     */
116
    public function genTree($theID, $versions = false)
117
    {
118
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
119
        $queryBuilder->getRestrictions()->removeAll();
120
        if (!$this->genTreeIncludeDeleted) {
121
            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
122
        }
123
        $queryBuilder->select('uid', 'title', 'doktype', 'deleted', 'hidden', 'sys_language_uid')
124
            ->from('pages')
125
            ->orderBy('sorting');
126
        if ($versions) {
127
            $queryBuilder->addSelect('t3ver_wsid');
128
            $queryBuilder->where(
129
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
130
            );
131
        } else {
132
            $queryBuilder->where(
133
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
134
            );
135
        }
136
        $result = $queryBuilder->execute();
137
        // Traverse the records selected
138
        while ($row = $result->fetch()) {
139
            $newID = $row['uid'];
140
            // Register various data for this item:
141
            if ($row['sys_language_uid'] === 0) {
142
                $this->pageIdArray[$newID] = $row;
143
            } else {
144
                $this->pageTranslatedPageIDArray[$newID] = $row;
145
            }
146
            $this->recStats['all_valid']['pages'][$newID] = $newID;
147
            if ($row['deleted']) {
148
                $this->recStats['deleted']['pages'][$newID] = $newID;
149
            }
150
151
            if (!isset($this->recStats['hidden'])) {
152
                $this->recStats['hidden'] = 0;
153
            }
154
155
            if ($row['hidden']) {
156
                $this->recStats['hidden']++;
157
            }
158
159
            if (!is_array($this->recStats['doktype'][$row['doktype']] ?? false)) {
160
                $this->recStats['doktype'][$row['doktype']] = 0;
161
            }
162
163
            $this->recStats['doktype'][$row['doktype']]++;
164
            // If all records should be shown, do so:
165
            if ($this->genTreeIncludeRecords) {
166
                foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
167
                    if ($tableName !== 'pages') {
168
                        $this->genTree_records($newID, $tableName);
169
                    }
170
                }
171
            }
172
            // Add sub pages:
173
            $this->genTree($newID);
174
            // If versions are included in the tree, add those now:
175
            if ($this->genTreeIncludeVersions) {
176
                $this->genTree($newID, true);
177
            }
178
        }
179
    }
180
181
    /**
182
     * @param int $theID a pid (page-record id) from which to start making the tree
183
     * @param string $table Table to get the records from
184
     * @param bool $versions Internal variable, don't set from outside!
185
     */
186
    public function genTree_records($theID, $table, $versions = false): void
187
    {
188
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
189
        $queryBuilder->getRestrictions()->removeAll();
190
        if (!$this->genTreeIncludeDeleted) {
191
            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
192
        }
193
        $queryBuilder
194
            ->select(...explode(',', BackendUtility::getCommonSelectFields($table)))
195
            ->from($table);
196
197
        // Select all records from table pointing to this page
198
        if ($versions) {
199
            $queryBuilder->where(
200
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
201
            );
202
        } else {
203
            $queryBuilder->where(
204
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
205
            );
206
        }
207
        $queryResult = $queryBuilder->execute();
208
        // Traverse selected
209
        while ($row = $queryResult->fetch()) {
210
            $newID = $row['uid'];
211
            // Register various data for this item:
212
            $this->recIdArray[$table][$newID] = $row;
213
            $this->recStats['all_valid'][$table][$newID] = $newID;
214
            if ($row['deleted']) {
215
                $this->recStats['deleted'][$table][$newID] = $newID;
216
            }
217
            // Select all versions of this record:
218
            if ($this->genTreeIncludeVersions && BackendUtility::isTableWorkspaceEnabled($table)) {
219
                $this->genTree_records($newID, $table, true);
220
            }
221
        }
222
    }
223
224
    /**
225
     * Fills $this->lRecords with the records from all tc-tables that are not attached to a PID in the pid-list.
226
     *
227
     * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
228
     */
229
    public function lostRecords($pid_list): void
230
    {
231
        $this->lostPagesList = '';
232
        $pageIds = GeneralUtility::intExplode(',', $pid_list);
233
        if (is_array($pageIds)) {
0 ignored issues
show
introduced by
The condition is_array($pageIds) is always true.
Loading history...
234
            foreach ($GLOBALS['TCA'] as $table => $tableConf) {
235
                $pageIdsForTable = $pageIds;
236
                // Remove preceding "-1," for non-versioned tables
237
                if (!BackendUtility::isTableWorkspaceEnabled($table)) {
238
                    $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
239
                    unset($pageIdsForTable[-1]);
240
                }
241
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
242
                $queryBuilder->getRestrictions()->removeAll();
243
                $selectFields = ['uid', 'pid'];
244
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
245
                    $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['label'];
246
                }
247
                $queryResult = $queryBuilder->select(...$selectFields)
248
                    ->from($table)
249
                    ->where(
250
                        $queryBuilder->expr()->notIn(
251
                            'pid',
252
                            $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
253
                        )
254
                    )
255
                    ->execute();
256
                $lostIdList = [];
257
                while ($row = $queryResult->fetch()) {
258
                    $this->lRecords[$table][$row['uid']] = [
259
                        'uid' => $row['uid'],
260
                        'pid' => $row['pid'],
261
                        'title' => strip_tags(BackendUtility::getRecordTitle($table, $row))
262
                    ];
263
                    $lostIdList[] = $row['uid'];
264
                }
265
                if ($table === 'pages') {
266
                    $this->lostPagesList = implode(',', $lostIdList);
267
                }
268
            }
269
        }
270
    }
271
272
    /**
273
     * Fixes lost record from $table with uid $uid by setting the PID to zero.
274
     * If there is a disabled column for the record that will be set as well.
275
     *
276
     * @param string $table Database tablename
277
     * @param int $uid The uid of the record which will have the PID value set to 0 (zero)
278
     * @return bool TRUE if done.
279
     */
280
    public function fixLostRecord($table, $uid): bool
281
    {
282
        if ($table && $GLOBALS['TCA'][$table] && $uid && is_array($this->lRecords[$table][$uid]) && $GLOBALS['BE_USER']->isAdmin()) {
283
            $updateFields = [
284
                'pid' => 0
285
            ];
286
            // If possible a lost record restored is hidden as default
287
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']) {
288
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']] = 1;
289
            }
290
            GeneralUtility::makeInstance(ConnectionPool::class)
291
                ->getConnectionForTable($table)
292
                ->update($table, $updateFields, ['uid' => (int)$uid]);
293
            return true;
294
        }
295
        return false;
296
    }
297
298
    /**
299
     * Counts records from $GLOBALS['TCA']-tables that ARE attached to an existing page.
300
     *
301
     * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
302
     * @return array an array with the number of records from all $GLOBALS['TCA']-tables that are attached to a PID in the pid-list.
303
     */
304
    public function countRecords($pid_list): array
305
    {
306
        $list = [];
307
        $list_n = [];
308
        $pageIds = GeneralUtility::intExplode(',', $pid_list);
309
        if (!empty($pageIds)) {
310
            foreach ($GLOBALS['TCA'] as $table => $tableConf) {
311
                $pageIdsForTable = $pageIds;
312
                // Remove preceding "-1," for non-versioned tables
313
                if (!BackendUtility::isTableWorkspaceEnabled($table)) {
314
                    $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
315
                    unset($pageIdsForTable[-1]);
316
                }
317
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
318
                $queryBuilder->getRestrictions()->removeAll();
319
                $count = $queryBuilder->count('uid')
320
                    ->from($table)
321
                    ->where(
322
                        $queryBuilder->expr()->in(
323
                            'pid',
324
                            $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
325
                        )
326
                    )
327
                    ->execute()
328
                    ->fetchOne();
329
                if ($count) {
330
                    $list[$table] = $count;
331
                }
332
333
                // same query excluding all deleted records
334
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
335
                $queryBuilder->getRestrictions()
336
                    ->removeAll()
337
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
338
                $count = $queryBuilder->count('uid')
339
                    ->from($table)
340
                    ->where(
341
                        $queryBuilder->expr()->in(
342
                            'pid',
343
                            $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
344
                        )
345
                    )
346
                    ->execute()
347
                    ->fetchOne();
348
                if ($count) {
349
                    $list_n[$table] = $count;
350
                }
351
            }
352
        }
353
        return ['all' => $list, 'non_deleted' => $list_n];
354
    }
355
356
    /**
357
     * Finding relations in database based on type 'group' (database-uid's in a list)
358
     *
359
     * @return array An array with all fields listed that somehow are references to other records (foreign-keys)
360
     */
361
    public function getGroupFields(): array
362
    {
363
        $result = [];
364
        foreach ($GLOBALS['TCA'] as $table => $tableConf) {
365
            $cols = $GLOBALS['TCA'][$table]['columns'];
366
            foreach ($cols as $field => $config) {
367
                if (($config['config']['type'] ?? '') === 'group' && ($config['config']['internal_type'] ?? false) === 'db') {
368
                    $result[$table][] = $field;
369
                }
370
                if (($config['config']['type'] ?? '') === 'select' && ($config['config']['foreign_table'] ?? false)) {
371
                    $result[$table][] = $field;
372
                }
373
            }
374
        }
375
        return $result;
376
    }
377
378
    /**
379
     * This selects non-empty-records from the tables/fields in the fkey_array generated by getGroupFields()
380
     *
381
     * @see getGroupFields()
382
     */
383
    public function selectNonEmptyRecordsWithFkeys(): void
384
    {
385
        $fkey_arrays = $this->getGroupFields();
386
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
387
        foreach ($fkey_arrays as $table => $fields) {
388
            $connection = $connectionPool->getConnectionForTable($table);
389
            $schemaManager = $connection->getSchemaManager();
390
            $tableColumns = $schemaManager->listTableColumns($table);
391
392
            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
393
            $queryBuilder->getRestrictions()->removeAll();
394
395
            $queryBuilder->select('uid')
396
                ->from($table);
397
            $whereClause = [];
398
399
            foreach ($fields as $fieldName) {
400
                // The array index of $tableColumns is the lowercased column name!
401
                // It is quoted for keywords
402
                $column = $tableColumns[strtolower($fieldName)]
403
                    ?? $tableColumns[$connection->quoteIdentifier(strtolower($fieldName))];
404
                if (!$column) {
405
                    // Throw meaningful exception if field does not exist in DB - 'none' is not filtered here since the
406
                    // method is only called with type=group fields
407
                    throw new \RuntimeException(
408
                        'Field ' . $fieldName . ' for table ' . $table . ' has been defined in TCA, but does not exist in DB',
409
                        1536248937
410
                    );
411
                }
412
                $fieldType = $column->getType()->getName();
413
                if (in_array(
414
                    $fieldType,
415
                    [Types::BIGINT, Types::INTEGER, Types::SMALLINT, Types::DECIMAL, Types::FLOAT],
416
                    true
417
                )) {
418
                    $whereClause[] = $queryBuilder->expr()->andX(
419
                        $queryBuilder->expr()->isNotNull($fieldName),
420
                        $queryBuilder->expr()->neq(
421
                            $fieldName,
422
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
423
                        )
424
                    );
425
                } elseif (in_array($fieldType, [Types::STRING, Types::TEXT], true)) {
426
                    $whereClause[] = $queryBuilder->expr()->andX(
427
                        $queryBuilder->expr()->isNotNull($fieldName),
428
                        $queryBuilder->expr()->neq(
429
                            $fieldName,
430
                            $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
431
                        )
432
                    );
433
                } elseif ($fieldType === Types::BLOB) {
434
                    $whereClause[] = $queryBuilder->expr()->andX(
435
                        $queryBuilder->expr()->isNotNull($fieldName),
436
                        $queryBuilder->expr()
437
                            ->comparison(
438
                                $queryBuilder->expr()->length($fieldName),
439
                                ExpressionBuilder::GT,
440
                                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
441
                            )
442
                    );
443
                }
444
            }
445
            $queryResult = $queryBuilder->orWhere(...$whereClause)->execute();
446
447
            while ($row = $queryResult->fetch()) {
448
                foreach ($fields as $field) {
449
                    if (trim($row[$field] ?? '')) {
450
                        $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
451
                        if ($fieldConf['type'] === 'group' && $fieldConf['internal_type'] === 'db') {
452
                            $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
453
                            $dbAnalysis->start(
454
                                $row[$field],
455
                                $fieldConf['allowed'],
456
                                $fieldConf['MM'],
457
                                $row['uid'],
458
                                $table,
459
                                $fieldConf
460
                            );
461
                            foreach ($dbAnalysis->itemArray as $tempArr) {
462
                                $this->checkGroupDBRefs[$tempArr['table']][$tempArr['id']] += 1;
463
                            }
464
                        }
465
                        if ($fieldConf['type'] === 'select' && $fieldConf['foreign_table']) {
466
                            $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
467
                            $dbAnalysis->start(
468
                                $row[$field],
469
                                $fieldConf['foreign_table'],
470
                                $fieldConf['MM'],
471
                                $row['uid'],
472
                                $table,
473
                                $fieldConf
474
                            );
475
                            foreach ($dbAnalysis->itemArray as $tempArr) {
476
                                if ($tempArr['id'] > 0) {
477
                                    $this->checkSelectDBRefs[$fieldConf['foreign_table']][$tempArr['id']] += 1;
478
                                }
479
                            }
480
                        }
481
                    }
482
                }
483
            }
484
        }
485
    }
486
487
    /**
488
     * Depends on selectNonEmpty.... to be executed first!!
489
     *
490
     * @param array $theArray Table with key/value pairs being table names and arrays with uid numbers
491
     * @return string HTML Error message
492
     */
493
    public function testDBRefs($theArray): string
494
    {
495
        $result = '';
496
        foreach ($theArray as $table => $dbArr) {
497
            if ($GLOBALS['TCA'][$table]) {
498
                $ids = array_keys($dbArr);
499
                if (!empty($ids)) {
500
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
501
                        ->getQueryBuilderForTable($table);
502
                    $queryBuilder->getRestrictions()
503
                        ->removeAll()
504
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
505
                    $queryResult = $queryBuilder
506
                        ->select('uid')
507
                        ->from($table)
508
                        ->where(
509
                            $queryBuilder->expr()->in(
510
                                'uid',
511
                                $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
512
                            )
513
                        )
514
                        ->execute();
515
                    while ($row = $queryResult->fetch()) {
516
                        if (isset($dbArr[$row['uid']])) {
517
                            unset($dbArr[$row['uid']]);
518
                        } else {
519
                            $result .= 'Strange Error. ...<br />';
520
                        }
521
                    }
522
                    foreach ($dbArr as $theId => $theC) {
523
                        $result .= 'There are ' . $theC . ' records pointing to this missing or deleted record; [' . $table . '][' . $theId . ']<br />';
524
                    }
525
                }
526
            } else {
527
                $result .= 'Codeerror. Table is not a table...<br />';
528
            }
529
        }
530
        return $result;
531
    }
532
533
    /**
534
     * @return array
535
     */
536
    public function getPageIdArray(): array
537
    {
538
        return $this->pageIdArray;
539
    }
540
541
    /**
542
     * @return array
543
     */
544
    public function getCheckGroupDBRefs(): array
545
    {
546
        return $this->checkGroupDBRefs;
547
    }
548
549
    /**
550
     * @return array
551
     */
552
    public function getCheckSelectDBRefs(): array
553
    {
554
        return $this->checkSelectDBRefs;
555
    }
556
557
    /**
558
     * @return array
559
     */
560
    public function getRecStats(): array
561
    {
562
        return $this->recStats;
563
    }
564
565
    /**
566
     * @return array
567
     */
568
    public function getLRecords(): array
569
    {
570
        return $this->lRecords;
571
    }
572
573
    /**
574
     * @return string
575
     */
576
    public function getLostPagesList(): string
577
    {
578
        return $this->lostPagesList;
579
    }
580
}
581