301e72c61efd00e5f1aad3eb43f64eeaf5f42525
[Packages/TYPO3.CMS.git] / typo3 / sysext / lowlevel / Classes / Integrity / DatabaseIntegrityCheck.php
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\Type;
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
19 use TYPO3\CMS\Core\Core\Environment;
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 use TYPO3\CMS\Core\Utility\PathUtility;
27
28 /**
29 * This class holds functions used by the TYPO3 backend to check the integrity
30 * of the database (The DBint module, 'lowlevel' extension)
31 *
32 * Depends on \TYPO3\CMS\Core\Database\RelationHandler
33 *
34 * @TODO: Need to really extend this class when the DataHandler library has been
35 * @TODO: updated and the whole API is better defined. There are some known bugs
36 * @TODO: in this library. Further it would be nice with a facility to not only
37 * @TODO: analyze but also clean up!
38 * @see \TYPO3\CMS\Lowlevel\Controller\DatabaseIntegrityController::func_relations(), \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
74 */
75 protected $checkFileRefs = [];
76
77 /**
78 * @var array From the select-fields
79 */
80 protected $checkSelectDBRefs = [];
81
82 /**
83 * @var array From the group-fields
84 */
85 protected $checkGroupDBRefs = [];
86
87 /**
88 * @var array Statistics
89 */
90 protected $recStats = [
91 'allValid' => [],
92 'published_versions' => [],
93 'deleted' => []
94 ];
95
96 /**
97 * @var array
98 */
99 protected $lRecords = [];
100
101 /**
102 * @var string
103 */
104 protected $lostPagesList = '';
105
106 /**
107 * @return array
108 */
109 public function getPageTranslatedPageIDArray(): array
110 {
111 return $this->pageTranslatedPageIDArray;
112 }
113
114 /**
115 * Generates a list of Page-uid's that corresponds to the tables in the tree.
116 * This list should ideally include all records in the pages-table.
117 *
118 * @param int $theID a pid (page-record id) from which to start making the tree
119 * @param string $depthData HTML-code (image-tags) used when this function calls itself recursively.
120 * @param bool $versions Internal variable, don't set from outside!
121 */
122 public function genTree($theID, $depthData = '', $versions = false)
123 {
124 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
125 $queryBuilder->getRestrictions()->removeAll();
126 if (!$this->genTreeIncludeDeleted) {
127 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
128 }
129 $queryBuilder->select('uid', 'title', 'doktype', 'deleted', 'hidden', 'sys_language_uid')
130 ->from('pages')
131 ->orderBy('sorting');
132 if ($versions) {
133 $queryBuilder->addSelect('t3ver_wsid', 't3ver_id', 't3ver_count');
134 $queryBuilder->where(
135 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
136 $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
137 );
138 } else {
139 $queryBuilder->where(
140 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
141 );
142 }
143 $result = $queryBuilder->execute();
144 // Traverse the records selected
145 while ($row = $result->fetch()) {
146 $newID = $row['uid'];
147 // Register various data for this item:
148 if ($row['sys_language_uid'] === 0) {
149 $this->pageIdArray[$newID] = $row;
150 } else {
151 $this->pageTranslatedPageIDArray[$newID] = $row;
152 }
153 $this->recStats['all_valid']['pages'][$newID] = $newID;
154 if ($row['deleted']) {
155 $this->recStats['deleted']['pages'][$newID] = $newID;
156 }
157 if ($versions && $row['t3ver_count'] >= 1) {
158 $this->recStats['published_versions']['pages'][$newID] = $newID;
159 }
160 if ($row['deleted']) {
161 $this->recStats['deleted']++;
162 }
163 if ($row['hidden']) {
164 $this->recStats['hidden']++;
165 }
166 $this->recStats['doktype'][$row['doktype']]++;
167 // If all records should be shown, do so:
168 if ($this->genTreeIncludeRecords) {
169 foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
170 if ($tableName !== 'pages') {
171 $this->genTree_records($newID, '', $tableName);
172 }
173 }
174 }
175 // Add sub pages:
176 $this->genTree($newID);
177 // If versions are included in the tree, add those now:
178 if ($this->genTreeIncludeVersions) {
179 $this->genTree($newID, '', true);
180 }
181 }
182 }
183
184 /**
185 * @param int $theID a pid (page-record id) from which to start making the tree
186 * @param string $_ Unused parameter
187 * @param string $table Table to get the records from
188 * @param bool $versions Internal variable, don't set from outside!
189 */
190 public function genTree_records($theID, $_ = '', $table = '', $versions = false): void
191 {
192 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
193 $queryBuilder->getRestrictions()->removeAll();
194 if (!$this->genTreeIncludeDeleted) {
195 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
196 }
197 $queryBuilder
198 ->select(...explode(',', BackendUtility::getCommonSelectFields($table)))
199 ->from($table);
200
201 // Select all records from table pointing to this page
202 if ($versions) {
203 $queryBuilder->where(
204 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
205 $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
206 );
207 } else {
208 $queryBuilder->where(
209 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($theID, \PDO::PARAM_INT))
210 );
211 }
212 $queryResult = $queryBuilder->execute();
213 // Traverse selected
214 while ($row = $queryResult->fetch()) {
215 $newID = $row['uid'];
216 // Register various data for this item:
217 $this->recIdArray[$table][$newID] = $row;
218 $this->recStats['all_valid'][$table][$newID] = $newID;
219 if ($row['deleted']) {
220 $this->recStats['deleted'][$table][$newID] = $newID;
221 }
222 if ($versions && $row['t3ver_count'] >= 1 && $row['t3ver_wsid'] == 0) {
223 $this->recStats['published_versions'][$table][$newID] = $newID;
224 }
225 // Select all versions of this record:
226 if ($this->genTreeIncludeVersions && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
227 $this->genTree_records($newID, '', $table, true);
228 }
229 }
230 }
231
232 /**
233 * Fills $this->lRecords with the records from all tc-tables that are not attached to a PID in the pid-list.
234 *
235 * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
236 */
237 public function lostRecords($pid_list): void
238 {
239 $this->lostPagesList = '';
240 $pageIds = GeneralUtility::intExplode(',', $pid_list);
241 if (is_array($pageIds)) {
242 foreach ($GLOBALS['TCA'] as $table => $tableConf) {
243 $pageIdsForTable = $pageIds;
244 // Remove preceding "-1," for non-versioned tables
245 if (!BackendUtility::isTableWorkspaceEnabled($table)) {
246 $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
247 unset($pageIdsForTable[-1]);
248 }
249 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
250 $queryBuilder->getRestrictions()->removeAll();
251 $selectFields = ['uid', 'pid'];
252 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
253 $selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['label'];
254 }
255 $queryResult = $queryBuilder->select(...$selectFields)
256 ->from($table)
257 ->where(
258 $queryBuilder->expr()->notIn(
259 'pid',
260 $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
261 )
262 )
263 ->execute();
264 $lostIdList = [];
265 while ($row = $queryResult->fetch()) {
266 $this->lRecords[$table][$row['uid']] = [
267 'uid' => $row['uid'],
268 'pid' => $row['pid'],
269 'title' => strip_tags(BackendUtility::getRecordTitle($table, $row))
270 ];
271 $lostIdList[] = $row['uid'];
272 }
273 if ($table === 'pages') {
274 $this->lostPagesList = implode(',', $lostIdList);
275 }
276 }
277 }
278 }
279
280 /**
281 * Fixes lost record from $table with uid $uid by setting the PID to zero.
282 * If there is a disabled column for the record that will be set as well.
283 *
284 * @param string $table Database tablename
285 * @param int $uid The uid of the record which will have the PID value set to 0 (zero)
286 * @return bool TRUE if done.
287 */
288 public function fixLostRecord($table, $uid): bool
289 {
290 if ($table && $GLOBALS['TCA'][$table] && $uid && is_array($this->lRecords[$table][$uid]) && $GLOBALS['BE_USER']->isAdmin()) {
291 $updateFields = [
292 'pid' => 0
293 ];
294 // If possible a lost record restored is hidden as default
295 if ($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']) {
296 $updateFields[$GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']] = 1;
297 }
298 GeneralUtility::makeInstance(ConnectionPool::class)
299 ->getConnectionForTable($table)
300 ->update($table, $updateFields, ['uid' => (int)$uid]);
301 return true;
302 }
303 return false;
304 }
305
306 /**
307 * Counts records from $GLOBALS['TCA']-tables that ARE attached to an existing page.
308 *
309 * @param string $pid_list list of pid's (page-record uid's). This list is probably made by genTree()
310 * @return array an array with the number of records from all $GLOBALS['TCA']-tables that are attached to a PID in the pid-list.
311 */
312 public function countRecords($pid_list): array
313 {
314 $list = [];
315 $list_n = [];
316 $pageIds = GeneralUtility::intExplode(',', $pid_list);
317 if (!empty($pageIds)) {
318 foreach ($GLOBALS['TCA'] as $table => $tableConf) {
319 $pageIdsForTable = $pageIds;
320 // Remove preceding "-1," for non-versioned tables
321 if (!BackendUtility::isTableWorkspaceEnabled($table)) {
322 $pageIdsForTable = array_combine($pageIdsForTable, $pageIdsForTable);
323 unset($pageIdsForTable[-1]);
324 }
325 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
326 $queryBuilder->getRestrictions()->removeAll();
327 $count = $queryBuilder->count('uid')
328 ->from($table)
329 ->where(
330 $queryBuilder->expr()->in(
331 'pid',
332 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
333 )
334 )
335 ->execute()
336 ->fetchColumn(0);
337 if ($count) {
338 $list[$table] = $count;
339 }
340
341 // same query excluding all deleted records
342 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
343 $queryBuilder->getRestrictions()
344 ->removeAll()
345 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
346 $count = $queryBuilder->count('uid')
347 ->from($table)
348 ->where(
349 $queryBuilder->expr()->in(
350 'pid',
351 $queryBuilder->createNamedParameter($pageIdsForTable, Connection::PARAM_INT_ARRAY)
352 )
353 )
354 ->execute()
355 ->fetchColumn(0);
356 if ($count) {
357 $list_n[$table] = $count;
358 }
359 }
360 }
361 return ['all' => $list, 'non_deleted' => $list_n];
362 }
363
364 /**
365 * Finding relations in database based on type 'group' (files or database-uid's in a list)
366 *
367 * @param string $mode $mode = file, $mode = db, $mode = '' (all...)
368 * @return array An array with all fields listed that somehow are references to other records (foreign-keys) or files
369 */
370 public function getGroupFields($mode): array
371 {
372 $result = [];
373 foreach ($GLOBALS['TCA'] as $table => $tableConf) {
374 $cols = $GLOBALS['TCA'][$table]['columns'];
375 foreach ($cols as $field => $config) {
376 if ($config['config']['type'] === 'group') {
377 if ((!$mode || $mode === 'file') && $config['config']['internal_type'] === 'file' || (!$mode || $mode === 'db') && $config['config']['internal_type'] === 'db') {
378 $result[$table][] = $field;
379 }
380 }
381 if ((!$mode || $mode === 'db') && $config['config']['type'] === 'select' && $config['config']['foreign_table']) {
382 $result[$table][] = $field;
383 }
384 }
385 if ($result[$table]) {
386 $result[$table] = implode(',', $result[$table]);
387 }
388 }
389 return $result;
390 }
391
392 /**
393 * Finds all fields that hold filenames from uploadfolder
394 *
395 * @param string $uploadfolder Path to uploadfolder
396 * @return array An array with all fields listed that have references to files in the $uploadfolder
397 */
398 public function getFileFields($uploadfolder): array
399 {
400 $result = [];
401 foreach ($GLOBALS['TCA'] as $table => $tableConf) {
402 $cols = $GLOBALS['TCA'][$table]['columns'];
403 foreach ($cols as $field => $config) {
404 if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file' && $config['config']['uploadfolder'] == $uploadfolder) {
405 $result[] = [$table, $field];
406 }
407 }
408 }
409 return $result;
410 }
411
412 /**
413 * Returns an array with arrays of table/field pairs which are allowed to hold references to the input table name - according to $GLOBALS['TCA']
414 *
415 * @param string $theSearchTable Table name
416 * @return array
417 */
418 public function getDBFields($theSearchTable): array
419 {
420 $result = [];
421 foreach ($GLOBALS['TCA'] as $table => $tableConf) {
422 $cols = $GLOBALS['TCA'][$table]['columns'];
423 foreach ($cols as $field => $config) {
424 if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'db') {
425 if (trim($config['config']['allowed']) === '*' || strstr($config['config']['allowed'], $theSearchTable)) {
426 $result[] = [$table, $field];
427 }
428 } elseif ($config['config']['type'] === 'select' && $config['config']['foreign_table'] == $theSearchTable) {
429 $result[] = [$table, $field];
430 }
431 }
432 }
433 return $result;
434 }
435
436 /**
437 * This selects non-empty-records from the tables/fields in the fkey_array generated by getGroupFields()
438 *
439 * @param array $fkey_arrays Array with tables/fields generated by getGroupFields()
440 * @see getGroupFields()
441 */
442 public function selectNonEmptyRecordsWithFkeys($fkey_arrays): void
443 {
444 if (is_array($fkey_arrays)) {
445 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
446 foreach ($fkey_arrays as $table => $field_list) {
447 if ($GLOBALS['TCA'][$table] && trim($field_list)) {
448 $connection = $connectionPool->getConnectionForTable($table);
449 $schemaManager = $connection->getSchemaManager();
450 $tableColumns = $schemaManager->listTableColumns($table);
451
452 $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
453 $queryBuilder->getRestrictions()->removeAll();
454
455 $fields = GeneralUtility::trimExplode(',', $field_list, true);
456
457 $queryBuilder->select('uid')
458 ->from($table);
459 $whereClause = [];
460
461 foreach ($fields as $fieldName) {
462 // The array index of $tableColumns is the lowercased column name!
463 // It is quoted for keywords
464 $column = $tableColumns[strtolower($fieldName)]
465 ?? $tableColumns[$connection->quoteIdentifier(strtolower($fieldName))];
466 $fieldType = $column->getType()->getName();
467 if (in_array(
468 $fieldType,
469 [Type::BIGINT, Type::INTEGER, Type::SMALLINT, Type::DECIMAL, Type::FLOAT],
470 true
471 )) {
472 $whereClause[] = $queryBuilder->expr()->andX(
473 $queryBuilder->expr()->isNotNull($fieldName),
474 $queryBuilder->expr()->neq(
475 $fieldName,
476 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
477 )
478 );
479 } elseif (in_array($fieldType, [Type::STRING, Type::TEXT], true)) {
480 $whereClause[] = $queryBuilder->expr()->andX(
481 $queryBuilder->expr()->isNotNull($fieldName),
482 $queryBuilder->expr()->neq(
483 $fieldName,
484 $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
485 )
486 );
487 } elseif ($fieldType === Type::BLOB) {
488 $whereClause[] = $queryBuilder->expr()->andX(
489 $queryBuilder->expr()->isNotNull($fieldName),
490 $queryBuilder->expr()
491 ->comparison(
492 $queryBuilder->expr()->length($fieldName),
493 ExpressionBuilder::GT,
494 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
495 )
496 );
497 }
498 }
499 $queryResult = $queryBuilder->orWhere(...$whereClause)->execute();
500
501 while ($row = $queryResult->fetch()) {
502 foreach ($fields as $field) {
503 if (trim($row[$field])) {
504 $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
505 if ($fieldConf['type'] === 'group') {
506 if ($fieldConf['internal_type'] === 'file') {
507 // Files...
508 if ($fieldConf['MM']) {
509 $tempArr = [];
510 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
511 $dbAnalysis->start('', 'files', $fieldConf['MM'], $row['uid']);
512 foreach ($dbAnalysis->itemArray as $somekey => $someval) {
513 if ($someval['id']) {
514 $tempArr[] = $someval['id'];
515 }
516 }
517 } else {
518 $tempArr = explode(',', trim($row[$field]));
519 }
520 foreach ($tempArr as $file) {
521 $file = trim($file);
522 if ($file) {
523 $this->checkFileRefs[$fieldConf['uploadfolder']][$file] += 1;
524 }
525 }
526 }
527 if ($fieldConf['internal_type'] === 'db') {
528 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
529 $dbAnalysis->start(
530 $row[$field],
531 $fieldConf['allowed'],
532 $fieldConf['MM'],
533 $row['uid'],
534 $table,
535 $fieldConf
536 );
537 foreach ($dbAnalysis->itemArray as $tempArr) {
538 $this->checkGroupDBRefs[$tempArr['table']][$tempArr['id']] += 1;
539 }
540 }
541 }
542 if ($fieldConf['type'] === 'select' && $fieldConf['foreign_table']) {
543 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
544 $dbAnalysis->start(
545 $row[$field],
546 $fieldConf['foreign_table'],
547 $fieldConf['MM'],
548 $row['uid'],
549 $table,
550 $fieldConf
551 );
552 foreach ($dbAnalysis->itemArray as $tempArr) {
553 if ($tempArr['id'] > 0) {
554 $this->checkGroupDBRefs[$fieldConf['foreign_table']][$tempArr['id']] += 1;
555 }
556 }
557 }
558 }
559 }
560 }
561 }
562 }
563 }
564 }
565
566 /**
567 * Depends on selectNonEmpty.... to be executed first!!
568 *
569 * @return array Report over files; keys are "moreReferences", "noReferences", "noFile", "error
570 */
571 public function testFileRefs(): array
572 {
573 $output = [];
574 // Handle direct references with upload folder setting (workaround)
575 $newCheckFileRefs = [];
576 foreach ($this->checkFileRefs as $folder => $files) {
577 // Only direct references without a folder setting
578 if ($folder !== '') {
579 $newCheckFileRefs[$folder] = $files;
580 continue;
581 }
582 foreach ($files as $file => $references) {
583 // Direct file references have often many references (removes occurrences in the moreReferences section of the result array)
584 if ($references > 1) {
585 $references = 1;
586 }
587 // The directory must be empty (prevents checking of the root directory)
588 $directory = PathUtility::dirname($file);
589 if ($directory !== '') {
590 $newCheckFileRefs[$directory][PathUtility::basename($file)] = $references;
591 }
592 }
593 }
594 $this->checkFileRefs = $newCheckFileRefs;
595 foreach ($this->checkFileRefs as $folder => $fileArr) {
596 $path = Environment::getPublicPath() . '/' . $folder;
597 if (@is_dir($path) && @is_readable($path)) {
598 $d = dir($path);
599 while ($entry = $d->read()) {
600 if (@is_file($path . '/' . $entry)) {
601 if (isset($fileArr[$entry])) {
602 if ($fileArr[$entry] > 1) {
603 $temp = $this->whereIsFileReferenced($folder, $entry);
604 $tempList = '';
605 foreach ($temp as $inf) {
606 $tempList .= '[' . $inf['table'] . '][' . $inf['uid'] . '][' . $inf['field'] . '] (pid:' . $inf['pid'] . ') - ';
607 }
608 $output['moreReferences'][] = [$path, $entry, $fileArr[$entry], $tempList];
609 }
610 unset($fileArr[$entry]);
611 } else {
612 // Contains workaround for direct references
613 if (!strstr($entry, 'index.htm') && !preg_match('/^' . preg_quote($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/', $folder)) {
614 $output['noReferences'][] = [$path, $entry];
615 }
616 }
617 }
618 }
619 $d->close();
620 $tempCounter = 0;
621 foreach ($fileArr as $file => $value) {
622 // Workaround for direct file references
623 if (preg_match('/^' . preg_quote($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/', $folder)) {
624 $file = $folder . '/' . $file;
625 $folder = '';
626 $path = Environment::getPublicPath();
627 }
628 $temp = $this->whereIsFileReferenced($folder, $file);
629 $tempList = '';
630 foreach ($temp as $inf) {
631 $tempList .= '[' . $inf['table'] . '][' . $inf['uid'] . '][' . $inf['field'] . '] (pid:' . $inf['pid'] . ') - ';
632 }
633 $tempCounter++;
634 $output['noFile'][substr($path, -3) . '_' . substr($file, 0, 3) . '_' . $tempCounter] = [$path, $file, $tempList];
635 }
636 } else {
637 $output['error'][] = [$path];
638 }
639 }
640 return $output;
641 }
642
643 /**
644 * Depends on selectNonEmpty.... to be executed first!!
645 *
646 * @param array $theArray Table with key/value pairs being table names and arrays with uid numbers
647 * @return string HTML Error message
648 */
649 public function testDBRefs($theArray): string
650 {
651 $result = '';
652 foreach ($theArray as $table => $dbArr) {
653 if ($GLOBALS['TCA'][$table]) {
654 $ids = array_keys($dbArr);
655 if (!empty($ids)) {
656 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
657 ->getQueryBuilderForTable($table);
658 $queryBuilder->getRestrictions()
659 ->removeAll()
660 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
661 $queryResult = $queryBuilder
662 ->select('uid')
663 ->from($table)
664 ->where(
665 $queryBuilder->expr()->in(
666 'uid',
667 $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
668 )
669 )
670 ->execute();
671 while ($row = $queryResult->fetch()) {
672 if (isset($dbArr[$row['uid']])) {
673 unset($dbArr[$row['uid']]);
674 } else {
675 $result .= 'Strange Error. ...<br />';
676 }
677 }
678 foreach ($dbArr as $theId => $theC) {
679 $result .= 'There are ' . $theC . ' records pointing to this missing or deleted record; [' . $table . '][' . $theId . ']<br />';
680 }
681 }
682 } else {
683 $result .= 'Codeerror. Table is not a table...<br />';
684 }
685 }
686 return $result;
687 }
688
689 /**
690 * Finding all references to record based on table/uid
691 *
692 * @param string $searchTable Table name
693 * @param int $id Uid of database record
694 * @return array Array with other arrays containing information about where references was found
695 */
696 public function whereIsRecordReferenced($searchTable, $id): array
697 {
698 // Gets tables / Fields that reference to files
699 $fileFields = $this->getDBFields($searchTable);
700 $theRecordList = [];
701 foreach ($fileFields as $info) {
702 list($table, $field) = $info;
703 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
704 $queryBuilder->getRestrictions()->removeAll();
705 $queryResult = $queryBuilder
706 ->select('uid', 'pid', $GLOBALS['TCA'][$table]['ctrl']['label'], $field)
707 ->from($table)
708 ->where(
709 $queryBuilder->expr()->like(
710 $field,
711 $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($id) . '%')
712 )
713 )
714 ->execute();
715
716 while ($row = $queryResult->fetch()) {
717 // Now this is the field, where the reference COULD come from.
718 // But we're not guaranteed, so we must carefully examine the data.
719 $fieldConf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
720 $allowedTables = $fieldConf['type'] === 'group' ? $fieldConf['allowed'] : $fieldConf['foreign_table'];
721 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
722 $dbAnalysis->start($row[$field], $allowedTables, $fieldConf['MM'], $row['uid'], $table, $fieldConf);
723 foreach ($dbAnalysis->itemArray as $tempArr) {
724 if ($tempArr['table'] == $searchTable && $tempArr['id'] == $id) {
725 $theRecordList[] = [
726 'table' => $table,
727 'uid' => $row['uid'],
728 'field' => $field,
729 'pid' => $row['pid']
730 ];
731 }
732 }
733 }
734 }
735 return $theRecordList;
736 }
737
738 /**
739 * Finding all references to file based on uploadfolder / filename
740 *
741 * @param string $uploadFolder Upload folder where file is found
742 * @param string $filename Filename to search for
743 * @return array Array with other arrays containing information about where references was found
744 */
745 public function whereIsFileReferenced($uploadFolder, $filename): array
746 {
747 // Gets tables / Fields that reference to files
748 $fileFields = $this->getFileFields($uploadFolder);
749 $theRecordList = [];
750 foreach ($fileFields as $info) {
751 list($table, $field) = $info;
752 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
753 $queryBuilder->getRestrictions()->removeAll();
754 $queryResult = $queryBuilder
755 ->select('uid', 'pid', $GLOBALS['TCA'][$table]['ctrl']['label'], $field)
756 ->from($table)
757 ->where(
758 $queryBuilder->expr()->like(
759 $field,
760 $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($filename) . '%')
761 )
762 )
763 ->execute();
764 while ($row = $queryResult->fetch()) {
765 // Now this is the field, where the reference COULD come from.
766 // But we're not guaranteed, so we must carefully examine the data.
767 $tempArr = explode(',', trim($row[$field]));
768 foreach ($tempArr as $file) {
769 $file = trim($file);
770 if ($file == $filename) {
771 $theRecordList[] = [
772 'table' => $table,
773 'uid' => $row['uid'],
774 'field' => $field,
775 'pid' => $row['pid']
776 ];
777 }
778 }
779 }
780 }
781 return $theRecordList;
782 }
783
784 /**
785 * @return array
786 */
787 public function getPageIdArray(): array
788 {
789 return $this->pageIdArray;
790 }
791
792 /**
793 * @return array
794 */
795 public function getCheckGroupDBRefs(): array
796 {
797 return $this->checkGroupDBRefs;
798 }
799
800 /**
801 * @return array
802 */
803 public function getCheckSelectDBRefs(): array
804 {
805 return $this->checkSelectDBRefs;
806 }
807
808 /**
809 * @return array
810 */
811 public function getRecStats(): array
812 {
813 return $this->recStats;
814 }
815
816 /**
817 * @return array
818 */
819 public function getLRecords(): array
820 {
821 return $this->lRecords;
822 }
823
824 /**
825 * @return string
826 */
827 public function getLostPagesList(): string
828 {
829 return $this->lostPagesList;
830 }
831 }