Passed
Push — master ( b1175c...652bae )
by
unknown
24:43 queued 11:39
created

DatabaseIntegrityCheck::genTree()   B

Complexity

Conditions 11
Paths 132

Size

Total Lines 51
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 34
c 1
b 0
f 0
dl 0
loc 51
rs 7.05
cc 11
nc 132
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
            if ($row['hidden']) {
151
                $this->recStats['hidden']++;
152
            }
153
            $this->recStats['doktype'][$row['doktype']]++;
154
            // If all records should be shown, do so:
155
            if ($this->genTreeIncludeRecords) {
156
                foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
157
                    if ($tableName !== 'pages') {
158
                        $this->genTree_records($newID, $tableName);
159
                    }
160
                }
161
            }
162
            // Add sub pages:
163
            $this->genTree($newID);
164
            // If versions are included in the tree, add those now:
165
            if ($this->genTreeIncludeVersions) {
166
                $this->genTree($newID, true);
167
            }
168
        }
169
    }
170
171
    /**
172
     * @param int $theID a pid (page-record id) from which to start making the tree
173
     * @param string $table Table to get the records from
174
     * @param bool $versions Internal variable, don't set from outside!
175
     */
176
    public function genTree_records($theID, $table, $versions = false): void
177
    {
178
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
179
        $queryBuilder->getRestrictions()->removeAll();
180
        if (!$this->genTreeIncludeDeleted) {
181
            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
182
        }
183
        $queryBuilder
184
            ->select(...explode(',', BackendUtility::getCommonSelectFields($table)))
185
            ->from($table);
186
187
        // Select all records from table pointing to this page
188
        if ($versions) {
189
            $queryBuilder->where(
190
                $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
191
            );
192
        } else {
193
            $queryBuilder->where(
194
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
195
            );
196
        }
197
        $queryResult = $queryBuilder->execute();
198
        // Traverse selected
199
        while ($row = $queryResult->fetch()) {
200
            $newID = $row['uid'];
201
            // Register various data for this item:
202
            $this->recIdArray[$table][$newID] = $row;
203
            $this->recStats['all_valid'][$table][$newID] = $newID;
204
            if ($row['deleted']) {
205
                $this->recStats['deleted'][$table][$newID] = $newID;
206
            }
207
            // Select all versions of this record:
208
            if ($this->genTreeIncludeVersions && BackendUtility::isTableWorkspaceEnabled($table)) {
209
                $this->genTree_records($newID, $table, true);
210
            }
211
        }
212
    }
213
214
    /**
215
     * Fills $this->lRecords with the records from all tc-tables that are not attached to a PID in the pid-list.
216
     *
217
     * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
218
     */
219
    public function lostRecords($pid_list): void
220
    {
221
        $this->lostPagesList = '';
222
        $pageIds = GeneralUtility::intExplode(',', $pid_list);
223
        if (is_array($pageIds)) {
0 ignored issues
show
introduced by
The condition is_array($pageIds) is always true.
Loading history...
224
            foreach ($GLOBALS['TCA'] as $table => $tableConf) {
225
                $pageIdsForTable = $pageIds;
226
                // Remove preceding "-1," for non-versioned tables
227
                if (!BackendUtility::isTableWorkspaceEnabled($table)) {
228
                    $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
229
                    unset($pageIdsForTable[-1]);
230
                }
231
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
232
                $queryBuilder->getRestrictions()->removeAll();
233
                $selectFields = ['uid', 'pid'];
234
                if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
235
                    $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['label'];
236
                }
237
                $queryResult = $queryBuilder->select(...$selectFields)
238
                    ->from($table)
239
                    ->where(
240
                        $queryBuilder->expr()->notIn(
241
                            'pid',
242
                            $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
243
                        )
244
                    )
245
                    ->execute();
246
                $lostIdList = [];
247
                while ($row = $queryResult->fetch()) {
248
                    $this->lRecords[$table][$row['uid']] = [
249
                        'uid' => $row['uid'],
250
                        'pid' => $row['pid'],
251
                        'title' => strip_tags(BackendUtility::getRecordTitle($table, $row))
252
                    ];
253
                    $lostIdList[] = $row['uid'];
254
                }
255
                if ($table === 'pages') {
256
                    $this->lostPagesList = implode(',', $lostIdList);
257
                }
258
            }
259
        }
260
    }
261
262
    /**
263
     * Fixes lost record from $table with uid $uid by setting the PID to zero.
264
     * If there is a disabled column for the record that will be set as well.
265
     *
266
     * @param string $table Database tablename
267
     * @param int $uid The uid of the record which will have the PID value set to 0 (zero)
268
     * @return bool TRUE if done.
269
     */
270
    public function fixLostRecord($table, $uid): bool
271
    {
272
        if ($table && $GLOBALS['TCA'][$table] && $uid && is_array($this->lRecords[$table][$uid]) && $GLOBALS['BE_USER']->isAdmin()) {
273
            $updateFields = [
274
                'pid' => 0
275
            ];
276
            // If possible a lost record restored is hidden as default
277
            if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']) {
278
                $updateFields[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']] = 1;
279
            }
280
            GeneralUtility::makeInstance(ConnectionPool::class)
281
                ->getConnectionForTable($table)
282
                ->update($table, $updateFields, ['uid' => (int)$uid]);
283
            return true;
284
        }
285
        return false;
286
    }
287
288
    /**
289
     * Counts records from $GLOBALS['TCA']-tables that ARE attached to an existing page.
290
     *
291
     * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
292
     * @return array an array with the number of records from all $GLOBALS['TCA']-tables that are attached to a PID in the pid-list.
293
     */
294
    public function countRecords($pid_list): array
295
    {
296
        $list = [];
297
        $list_n = [];
298
        $pageIds = GeneralUtility::intExplode(',', $pid_list);
299
        if (!empty($pageIds)) {
300
            foreach ($GLOBALS['TCA'] as $table => $tableConf) {
301
                $pageIdsForTable = $pageIds;
302
                // Remove preceding "-1," for non-versioned tables
303
                if (!BackendUtility::isTableWorkspaceEnabled($table)) {
304
                    $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
305
                    unset($pageIdsForTable[-1]);
306
                }
307
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
308
                $queryBuilder->getRestrictions()->removeAll();
309
                $count = $queryBuilder->count('uid')
310
                    ->from($table)
311
                    ->where(
312
                        $queryBuilder->expr()->in(
313
                            'pid',
314
                            $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
315
                        )
316
                    )
317
                    ->execute()
318
                    ->fetchColumn(0);
319
                if ($count) {
320
                    $list[$table] = $count;
321
                }
322
323
                // same query excluding all deleted records
324
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
325
                $queryBuilder->getRestrictions()
326
                    ->removeAll()
327
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
328
                $count = $queryBuilder->count('uid')
329
                    ->from($table)
330
                    ->where(
331
                        $queryBuilder->expr()->in(
332
                            'pid',
333
                            $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
334
                        )
335
                    )
336
                    ->execute()
337
                    ->fetchColumn(0);
338
                if ($count) {
339
                    $list_n[$table] = $count;
340
                }
341
            }
342
        }
343
        return ['all' => $list, 'non_deleted' => $list_n];
344
    }
345
346
    /**
347
     * Finding relations in database based on type 'group' (database-uid's in a list)
348
     *
349
     * @return array An array with all fields listed that somehow are references to other records (foreign-keys)
350
     */
351
    public function getGroupFields(): array
352
    {
353
        $result = [];
354
        foreach ($GLOBALS['TCA'] as $table => $tableConf) {
355
            $cols = $GLOBALS['TCA'][$table]['columns'];
356
            foreach ($cols as $field => $config) {
357
                if (($config['config']['type'] ?? '') === 'group' && ($config['config']['internal_type'] ?? false) === 'db') {
358
                    $result[$table][] = $field;
359
                }
360
                if (($config['config']['type'] ?? '') === 'select' && ($config['config']['foreign_table'] ?? false)) {
361
                    $result[$table][] = $field;
362
                }
363
            }
364
        }
365
        return $result;
366
    }
367
368
    /**
369
     * This selects non-empty-records from the tables/fields in the fkey_array generated by getGroupFields()
370
     *
371
     * @see getGroupFields()
372
     */
373
    public function selectNonEmptyRecordsWithFkeys(): void
374
    {
375
        $fkey_arrays = $this->getGroupFields();
376
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
377
        foreach ($fkey_arrays as $table => $fields) {
378
            $connection = $connectionPool->getConnectionForTable($table);
379
            $schemaManager = $connection->getSchemaManager();
380
            $tableColumns = $schemaManager->listTableColumns($table);
381
382
            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
383
            $queryBuilder->getRestrictions()->removeAll();
384
385
            $queryBuilder->select('uid')
386
                ->from($table);
387
            $whereClause = [];
388
389
            foreach ($fields as $fieldName) {
390
                // The array index of $tableColumns is the lowercased column name!
391
                // It is quoted for keywords
392
                $column = $tableColumns[strtolower($fieldName)]
393
                    ?? $tableColumns[$connection->quoteIdentifier(strtolower($fieldName))];
394
                if (!$column) {
395
                    // Throw meaningful exception if field does not exist in DB - 'none' is not filtered here since the
396
                    // method is only called with type=group fields
397
                    throw new \RuntimeException(
398
                        'Field ' . $fieldName . ' for table ' . $table . ' has been defined in TCA, but does not exist in DB',
399
                        1536248937
400
                    );
401
                }
402
                $fieldType = $column->getType()->getName();
403
                if (in_array(
404
                    $fieldType,
405
                    [Types::BIGINT, Types::INTEGER, Types::SMALLINT, Types::DECIMAL, Types::FLOAT],
406
                    true
407
                )) {
408
                    $whereClause[] = $queryBuilder->expr()->andX(
409
                        $queryBuilder->expr()->isNotNull($fieldName),
410
                        $queryBuilder->expr()->neq(
411
                            $fieldName,
412
                            $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
413
                        )
414
                    );
415
                } elseif (in_array($fieldType, [Types::STRING, Types::TEXT], true)) {
416
                    $whereClause[] = $queryBuilder->expr()->andX(
417
                        $queryBuilder->expr()->isNotNull($fieldName),
418
                        $queryBuilder->expr()->neq(
419
                            $fieldName,
420
                            $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
421
                        )
422
                    );
423
                } elseif ($fieldType === Types::BLOB) {
424
                    $whereClause[] = $queryBuilder->expr()->andX(
425
                        $queryBuilder->expr()->isNotNull($fieldName),
426
                        $queryBuilder->expr()
427
                            ->comparison(
428
                                $queryBuilder->expr()->length($fieldName),
429
                                ExpressionBuilder::GT,
430
                                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
431
                            )
432
                    );
433
                }
434
            }
435
            $queryResult = $queryBuilder->orWhere(...$whereClause)->execute();
436
437
            while ($row = $queryResult->fetch()) {
438
                foreach ($fields as $field) {
439
                    if (trim($row[$field])) {
440
                        $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
441
                        if ($fieldConf['type'] === 'group' && $fieldConf['internal_type'] === 'db') {
442
                            $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
443
                            $dbAnalysis->start(
444
                                $row[$field],
445
                                $fieldConf['allowed'],
446
                                $fieldConf['MM'],
447
                                $row['uid'],
448
                                $table,
449
                                $fieldConf
450
                            );
451
                            foreach ($dbAnalysis->itemArray as $tempArr) {
452
                                $this->checkGroupDBRefs[$tempArr['table']][$tempArr['id']] += 1;
453
                            }
454
                        }
455
                        if ($fieldConf['type'] === 'select' && $fieldConf['foreign_table']) {
456
                            $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
457
                            $dbAnalysis->start(
458
                                $row[$field],
459
                                $fieldConf['foreign_table'],
460
                                $fieldConf['MM'],
461
                                $row['uid'],
462
                                $table,
463
                                $fieldConf
464
                            );
465
                            foreach ($dbAnalysis->itemArray as $tempArr) {
466
                                if ($tempArr['id'] > 0) {
467
                                    $this->checkSelectDBRefs[$fieldConf['foreign_table']][$tempArr['id']] += 1;
468
                                }
469
                            }
470
                        }
471
                    }
472
                }
473
            }
474
        }
475
    }
476
477
    /**
478
     * Depends on selectNonEmpty.... to be executed first!!
479
     *
480
     * @param array $theArray Table with key/value pairs being table names and arrays with uid numbers
481
     * @return string HTML Error message
482
     */
483
    public function testDBRefs($theArray): string
484
    {
485
        $result = '';
486
        foreach ($theArray as $table => $dbArr) {
487
            if ($GLOBALS['TCA'][$table]) {
488
                $ids = array_keys($dbArr);
489
                if (!empty($ids)) {
490
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
491
                        ->getQueryBuilderForTable($table);
492
                    $queryBuilder->getRestrictions()
493
                        ->removeAll()
494
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
495
                    $queryResult = $queryBuilder
496
                        ->select('uid')
497
                        ->from($table)
498
                        ->where(
499
                            $queryBuilder->expr()->in(
500
                                'uid',
501
                                $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
502
                            )
503
                        )
504
                        ->execute();
505
                    while ($row = $queryResult->fetch()) {
506
                        if (isset($dbArr[$row['uid']])) {
507
                            unset($dbArr[$row['uid']]);
508
                        } else {
509
                            $result .= 'Strange Error. ...<br />';
510
                        }
511
                    }
512
                    foreach ($dbArr as $theId => $theC) {
513
                        $result .= 'There are ' . $theC . ' records pointing to this missing or deleted record; [' . $table . '][' . $theId . ']<br />';
514
                    }
515
                }
516
            } else {
517
                $result .= 'Codeerror. Table is not a table...<br />';
518
            }
519
        }
520
        return $result;
521
    }
522
523
    /**
524
     * @return array
525
     */
526
    public function getPageIdArray(): array
527
    {
528
        return $this->pageIdArray;
529
    }
530
531
    /**
532
     * @return array
533
     */
534
    public function getCheckGroupDBRefs(): array
535
    {
536
        return $this->checkGroupDBRefs;
537
    }
538
539
    /**
540
     * @return array
541
     */
542
    public function getCheckSelectDBRefs(): array
543
    {
544
        return $this->checkSelectDBRefs;
545
    }
546
547
    /**
548
     * @return array
549
     */
550
    public function getRecStats(): array
551
    {
552
        return $this->recStats;
553
    }
554
555
    /**
556
     * @return array
557
     */
558
    public function getLRecords(): array
559
    {
560
        return $this->lRecords;
561
    }
562
563
    /**
564
     * @return string
565
     */
566
    public function getLostPagesList(): string
567
    {
568
        return $this->lostPagesList;
569
    }
570
}
571