Completed
Push — master ( 037cac...f0eb20 )
by
unknown
46:06 queued 26:31
created

DatabaseIntegrityCheck::getRecStats()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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