Completed
Push — master ( 097023...80a38d )
by
unknown
14:57
created

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