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 16namespace TYPO3\CMS\Core\Database; 17 18use Doctrine\DBAL\Exception as DBALException; 19use Psr\EventDispatcher\EventDispatcherInterface; 20use Psr\Log\LoggerAwareInterface; 21use Psr\Log\LoggerAwareTrait; 22use Psr\Log\LogLevel; 23use TYPO3\CMS\Backend\Utility\BackendUtility; 24use TYPO3\CMS\Backend\View\ProgressListenerInterface; 25use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; 26use TYPO3\CMS\Core\Database\Platform\PlatformInformation; 27use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 28use TYPO3\CMS\Core\DataHandling\DataHandler; 29use TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent; 30use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory; 31use TYPO3\CMS\Core\Registry; 32use TYPO3\CMS\Core\Utility\ArrayUtility; 33use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 34use TYPO3\CMS\Core\Utility\GeneralUtility; 35 36/** 37 * Reference index processing and relation extraction 38 * 39 * @internal Extensions shouldn't fiddle with the reference index themselves, it's task of DataHandler to do this. 40 */ 41class ReferenceIndex implements LoggerAwareInterface 42{ 43 use LoggerAwareTrait; 44 45 /** 46 * Definition of tables to exclude from the ReferenceIndex 47 * 48 * Only tables which do not contain any relations and never did so far since references also won't be deleted for 49 * these. Since only tables with an entry in $GLOBALS['TCA] are handled by ReferenceIndex there is no need to add 50 * *_mm-tables. 51 * 52 * Implemented as array with fields as keys and booleans as values for fast isset() lookup instead of slow in_array() 53 * 54 * @var array 55 * @see updateRefIndexTable() 56 * @see shouldExcludeTableFromReferenceIndex() 57 */ 58 protected array $excludedTables = [ 59 'sys_log' => true, 60 'tx_extensionmanager_domain_model_extension' => true, 61 ]; 62 63 /** 64 * Definition of fields to exclude from ReferenceIndex in *every* table 65 * 66 * Implemented as array with fields as keys and booleans as values for fast isset() lookup instead of slow in_array() 67 * 68 * @var array 69 * @see getRelations() 70 * @see fetchTableRelationFields() 71 * @see shouldExcludeTableColumnFromReferenceIndex() 72 */ 73 protected array $excludedColumns = [ 74 'uid' => true, 75 'perms_userid' => true, 76 'perms_groupid' => true, 77 'perms_user' => true, 78 'perms_group' => true, 79 'perms_everybody' => true, 80 'pid' => true, 81 ]; 82 83 /** 84 * This array holds the FlexForm references of a record 85 * 86 * @var array 87 * @see getRelations() 88 * @see FlexFormTools::traverseFlexFormXMLData() 89 * @see getRelations_flexFormCallBack() 90 */ 91 protected $temp_flexRelations = []; 92 93 /** 94 * An index of all found references of a single record 95 * 96 * @var array 97 */ 98 protected $relations = []; 99 100 /** 101 * Number which we can increase if a change in the code means we will have to force a re-generation of the index. 102 * 103 * @var int 104 * @see updateRefIndexTable() 105 */ 106 protected $hashVersion = 1; 107 108 /** 109 * Current workspace id 110 */ 111 protected int $workspaceId = 0; 112 113 /** 114 * A list of fields that may contain relations per TCA table. 115 * This is either ['*'] or an array of single field names. The list 116 * depends on TCA and is built when a first table row is handled. 117 */ 118 protected array $tableRelationFieldCache = []; 119 120 protected EventDispatcherInterface $eventDispatcher; 121 protected SoftReferenceParserFactory $softReferenceParserFactory; 122 123 public function __construct(EventDispatcherInterface $eventDispatcher = null, SoftReferenceParserFactory $softReferenceParserFactory = null) 124 { 125 $this->eventDispatcher = $eventDispatcher ?? GeneralUtility::makeInstance(EventDispatcherInterface::class); 126 $this->softReferenceParserFactory = $softReferenceParserFactory ?? GeneralUtility::makeInstance(SoftReferenceParserFactory::class); 127 } 128 129 /** 130 * Sets the current workspace id 131 * 132 * @param int $workspaceId 133 * @see updateIndex() 134 */ 135 public function setWorkspaceId($workspaceId) 136 { 137 $this->workspaceId = (int)$workspaceId; 138 } 139 140 /** 141 * Gets the current workspace id 142 * 143 * @return int 144 * @see updateRefIndexTable() 145 */ 146 protected function getWorkspaceId() 147 { 148 return $this->workspaceId; 149 } 150 151 /** 152 * Call this function to update the sys_refindex table for a record (even one just deleted) 153 * NOTICE: Currently, references updated for a deleted-flagged record will not include those from within FlexForm 154 * fields in some cases where the data structure is defined by another record since the resolving process ignores 155 * deleted records! This will also result in bad cleaning up in DataHandler I think... Anyway, that's the story of 156 * FlexForms; as long as the DS can change, lots of references can get lost in no time. 157 * 158 * @param string $tableName Table name 159 * @param int $uid UID of record 160 * @param bool $testOnly If set, nothing will be written to the index but the result value will still report statistics on what is added, deleted and kept. Can be used for mere analysis. 161 * @return array Array with statistics about how many index records were added, deleted and not altered plus the complete reference set for the record. 162 */ 163 public function updateRefIndexTable($tableName, $uid, $testOnly = false) 164 { 165 $result = [ 166 'keptNodes' => 0, 167 'deletedNodes' => 0, 168 'addedNodes' => 0, 169 ]; 170 171 $uid = $uid ? (int)$uid : 0; 172 if (!$uid) { 173 return $result; 174 } 175 176 // If this table cannot contain relations, skip it 177 if ($this->shouldExcludeTableFromReferenceIndex($tableName)) { 178 return $result; 179 } 180 181 $tableRelationFields = $this->fetchTableRelationFields($tableName); 182 183 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 184 $connection = $connectionPool->getConnectionForTable('sys_refindex'); 185 186 // Get current index from Database with hash as index using $uidIndexField 187 // no restrictions are needed, since sys_refindex is not a TCA table 188 $queryBuilder = $connection->createQueryBuilder(); 189 $queryBuilder->getRestrictions()->removeAll(); 190 $queryResult = $queryBuilder->select('hash')->from('sys_refindex')->where( 191 $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)), 192 $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)), 193 $queryBuilder->expr()->eq( 194 'workspace', 195 $queryBuilder->createNamedParameter($this->getWorkspaceId(), \PDO::PARAM_INT) 196 ) 197 )->executeQuery(); 198 $currentRelationHashes = []; 199 while ($relation = $queryResult->fetchAssociative()) { 200 $currentRelationHashes[$relation['hash']] = true; 201 } 202 203 // If the table has fields which could contain relations and the record does exist 204 if ($tableRelationFields !== []) { 205 $existingRecord = $this->getRecord($tableName, $uid); 206 if ($existingRecord) { 207 // Table has relation fields and record exists - get relations 208 $this->relations = []; 209 $relations = $this->generateDataUsingRecord($tableName, $existingRecord); 210 if (!is_array($relations)) { 211 return $result; 212 } 213 // Traverse the generated index: 214 foreach ($relations as &$relation) { 215 if (!is_array($relation)) { 216 continue; 217 } 218 // Exclude any relations TO a specific table 219 if (($relation['ref_table'] ?? '') && $this->shouldExcludeTableFromReferenceIndex($relation['ref_table'])) { 220 continue; 221 } 222 $relation['hash'] = md5(implode('///', $relation) . '///' . $this->hashVersion); 223 // First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!) 224 if (isset($currentRelationHashes[$relation['hash']])) { 225 unset($currentRelationHashes[$relation['hash']]); 226 $result['keptNodes']++; 227 $relation['_ACTION'] = 'KEPT'; 228 } else { 229 // If new, add it: 230 if (!$testOnly) { 231 $connection->insert('sys_refindex', $relation); 232 } 233 $result['addedNodes']++; 234 $relation['_ACTION'] = 'ADDED'; 235 } 236 } 237 $result['relations'] = $relations; 238 } 239 } 240 241 // If any old are left, remove them: 242 if (!empty($currentRelationHashes)) { 243 $hashList = array_keys($currentRelationHashes); 244 if (!empty($hashList)) { 245 $result['deletedNodes'] = count($hashList); 246 $result['deletedNodes_hashList'] = implode(',', $hashList); 247 if (!$testOnly) { 248 $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform()); 249 foreach (array_chunk($hashList, $maxBindParameters - 10, true) as $chunk) { 250 if (empty($chunk)) { 251 continue; 252 } 253 $queryBuilder = $connection->createQueryBuilder(); 254 $queryBuilder 255 ->delete('sys_refindex') 256 ->where( 257 $queryBuilder->expr()->in( 258 'hash', 259 $queryBuilder->createNamedParameter($chunk, Connection::PARAM_STR_ARRAY) 260 ) 261 ) 262 ->executeStatement(); 263 } 264 } 265 } 266 } 267 268 return $result; 269 } 270 271 /** 272 * Returns the amount of references for the given record 273 * 274 * @param string $tableName 275 * @param int $uid 276 * @return int 277 */ 278 public function getNumberOfReferencedRecords(string $tableName, int $uid): int 279 { 280 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); 281 return (int)$queryBuilder 282 ->count('*')->from('sys_refindex') 283 ->where( 284 $queryBuilder->expr()->eq( 285 'ref_table', 286 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR) 287 ), 288 $queryBuilder->expr()->eq( 289 'ref_uid', 290 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 291 ) 292 )->executeQuery()->fetchOne(); 293 } 294 295 /** 296 * Calculate the relations for a record of a given table 297 * 298 * @param string $tableName Table being processed 299 * @param array $record Record from $tableName 300 * @return array 301 */ 302 protected function generateDataUsingRecord(string $tableName, array $record): array 303 { 304 $this->relations = []; 305 // Get all relations from record: 306 $recordRelations = $this->getRelations($tableName, $record); 307 // Traverse those relations, compile records to insert in table: 308 foreach ($recordRelations as $fieldName => $fieldRelations) { 309 // Based on type 310 switch ($fieldRelations['type'] ?? '') { 311 case 'db': 312 $this->createEntryDataForDatabaseRelationsUsingRecord($tableName, $record, $fieldName, '', $fieldRelations['itemArray']); 313 break; 314 case 'flex': 315 // DB references in FlexForms 316 if (is_array($fieldRelations['flexFormRels']['db'])) { 317 foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) { 318 $this->createEntryDataForDatabaseRelationsUsingRecord($tableName, $record, $fieldName, $flexPointer, $subList); 319 } 320 } 321 // Soft references in FlexForms 322 // @todo #65464 Test correct handling of soft references in FlexForms 323 if (is_array($fieldRelations['flexFormRels']['softrefs'])) { 324 foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) { 325 $this->createEntryDataForSoftReferencesUsingRecord($tableName, $record, $fieldName, $flexPointer, $subList['keys']); 326 } 327 } 328 break; 329 } 330 // Soft references in the field 331 if (is_array($fieldRelations['softrefs']['keys'] ?? false)) { 332 $this->createEntryDataForSoftReferencesUsingRecord($tableName, $record, $fieldName, '', $fieldRelations['softrefs']['keys']); 333 } 334 } 335 336 return array_filter($this->relations); 337 } 338 339 /** 340 * Create array with field/value pairs ready to insert in database 341 * 342 * @param string $tableName Tablename of source record (where reference is located) 343 * @param array $record Record from $table 344 * @param string $fieldName Fieldname of source record (where reference is located) 345 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [$field] 346 * @param string $referencedTable In database references the tablename the reference points to. Keyword "_STRING" indicates special usage (typ. SoftReference) in $referenceString 347 * @param int $referencedUid In database references the UID of the record (zero $referencedTable is "_STRING") 348 * @param string $referenceString For "_STRING" references: The string. 349 * @param int $sort The sorting order of references if many (the "group" or "select" TCA types). -1 if no sorting order is specified. 350 * @param string $softReferenceKey If the reference is a soft reference, this is the soft reference parser key. Otherwise empty. 351 * @param string $softReferenceId Soft reference ID for key. Might be useful for replace operations. 352 * @return array|bool Array to insert in DB or false if record should not be processed 353 */ 354 protected function createEntryDataUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, string $referencedTable, int $referencedUid, string $referenceString = '', int $sort = -1, string $softReferenceKey = '', string $softReferenceId = '') 355 { 356 $currentWorkspace = $this->getWorkspaceId(); 357 if (BackendUtility::isTableWorkspaceEnabled($tableName)) { 358 $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; 359 if (isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $currentWorkspace && empty($fieldConfig['MM'])) { 360 // The given record is workspace-enabled but doesn't live in the selected workspace. Don't add index, it's not actually there. 361 // We still add those rows if the record is a local side live record of an MM relation and can be a target of a workspace record. 362 // See workspaces ManyToMany Modify addCategoryRelation for details on this case. 363 return false; 364 } 365 } 366 return [ 367 'tablename' => $tableName, 368 'recuid' => $record['uid'], 369 'field' => $fieldName, 370 'flexpointer' => $flexPointer, 371 'softref_key' => $softReferenceKey, 372 'softref_id' => $softReferenceId, 373 'sorting' => $sort, 374 'workspace' => $currentWorkspace, 375 'ref_table' => $referencedTable, 376 'ref_uid' => $referencedUid, 377 'ref_string' => mb_substr($referenceString, 0, 1024), 378 ]; 379 } 380 381 /** 382 * Add database references to ->relations array based on fetched record 383 * 384 * @param string $tableName Tablename of source record (where reference is located) 385 * @param array $record Record from $tableName 386 * @param string $fieldName Fieldname of source record (where reference is located) 387 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in $fieldName 388 * @param array $items Data array with database relations (table/id) 389 */ 390 protected function createEntryDataForDatabaseRelationsUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, array $items) 391 { 392 foreach ($items as $sort => $i) { 393 $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, $i['table'], (int)$i['id'], '', $sort); 394 } 395 } 396 397 /** 398 * Add SoftReference references to ->relations array based on fetched record 399 * 400 * @param string $tableName Tablename of source record (where reference is located) 401 * @param array $record Record from $tableName 402 * @param string $fieldName Fieldname of source record (where reference is located) 403 * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in $fieldName 404 * @param array $keys Data array with soft reference keys 405 */ 406 protected function createEntryDataForSoftReferencesUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, array $keys) 407 { 408 foreach ($keys as $spKey => $elements) { 409 if (is_array($elements)) { 410 foreach ($elements as $subKey => $el) { 411 if (is_array($el['subst'] ?? false)) { 412 switch ((string)$el['subst']['type']) { 413 case 'db': 414 [$referencedTable, $referencedUid] = explode(':', $el['subst']['recordRef']); 415 $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, $referencedTable, (int)$referencedUid, '', -1, $spKey, $subKey); 416 break; 417 case 'string': 418 $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, '_STRING', 0, $el['subst']['tokenValue'], -1, $spKey, $subKey); 419 break; 420 } 421 } 422 } 423 } 424 } 425 } 426 427 /******************************* 428 * 429 * Get relations from table row 430 * 431 *******************************/ 432 433 /** 434 * Returns relation information for a $table/$row-array 435 * Traverses all fields in input row which are configured in TCA/columns 436 * It looks for hard relations to records in the TCA types "select" and "group" 437 * 438 * @param string $table Table name 439 * @param array $row Row from table 440 * @param string $onlyField Specific field to fetch for. 441 * @return array Array with information about relations 442 * @see export_addRecord() 443 */ 444 public function getRelations($table, $row, $onlyField = '') 445 { 446 // Initialize: 447 $uid = $row['uid']; 448 $outRow = []; 449 foreach ($row as $field => $value) { 450 if ($this->shouldExcludeTableColumnFromReferenceIndex($table, $field, $onlyField) === false) { 451 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config']; 452 // Add a softref definition for link fields if the TCA does not specify one already 453 if ($conf['type'] === 'input' && isset($conf['renderType']) && $conf['renderType'] === 'inputLink' && empty($conf['softref'])) { 454 $conf['softref'] = 'typolink'; 455 } 456 // Add DB: 457 $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table, $row); 458 if (!empty($resultsFromDatabase)) { 459 // Create an entry for the field with all DB relations: 460 $outRow[$field] = [ 461 'type' => 'db', 462 'itemArray' => $resultsFromDatabase, 463 ]; 464 } 465 // For "flex" fieldtypes we need to traverse the structure looking for db references of course! 466 if ($conf['type'] === 'flex') { 467 // Get current value array: 468 // NOTICE: failure to resolve Data Structures can lead to integrity problems with the reference index. Please look up 469 // the note in the JavaDoc documentation for the function FlexFormTools->getDataStructureIdentifier() 470 $currentValueArray = GeneralUtility::xml2array($value); 471 // Traversing the XML structure, processing: 472 if (is_array($currentValueArray)) { 473 $this->temp_flexRelations = [ 474 'db' => [], 475 'softrefs' => [], 476 ]; 477 // Create and call iterator object: 478 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class); 479 $flexFormTools->traverseFlexFormXMLData($table, $field, $row, $this, 'getRelations_flexFormCallBack'); 480 // Create an entry for the field: 481 $outRow[$field] = [ 482 'type' => 'flex', 483 'flexFormRels' => $this->temp_flexRelations, 484 ]; 485 } 486 } 487 // Soft References: 488 if ((string)$value !== '') { 489 $softRefValue = $value; 490 if (!empty($conf['softref'])) { 491 foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($conf['softref']) as $softReferenceParser) { 492 $parserResult = $softReferenceParser->parse($table, $field, $uid, $softRefValue); 493 if ($parserResult->hasMatched()) { 494 $outRow[$field]['softrefs']['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements(); 495 if ($parserResult->hasContent()) { 496 $softRefValue = $parserResult->getContent(); 497 } 498 } 499 } 500 } 501 if (!empty($outRow[$field]['softrefs']) && (string)$value !== (string)$softRefValue && str_contains($softRefValue, '{softref:')) { 502 $outRow[$field]['softrefs']['tokenizedContent'] = $softRefValue; 503 } 504 } 505 } 506 } 507 return $outRow; 508 } 509 510 /** 511 * Callback function for traversing the FlexForm structure in relation to finding DB references! 512 * 513 * @param array $dsArr Data structure for the current value 514 * @param mixed $dataValue Current value 515 * @param array $PA Additional configuration used in calling function 516 * @param string $structurePath Path of value in DS structure 517 * @see DataHandler::checkValue_flex_procInData_travDS() 518 * @see FlexFormTools::traverseFlexFormXMLData() 519 */ 520 public function getRelations_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath) 521 { 522 // Removing "data/" in the beginning of path (which points to location in data array) 523 $structurePath = substr($structurePath, 5) . '/'; 524 $dsConf = $dsArr['TCEforms']['config']; 525 // Implode parameter values: 526 [$table, $uid, $field] = [ 527 $PA['table'], 528 $PA['uid'], 529 $PA['field'], 530 ]; 531 // Add a softref definition for link fields if the TCA does not specify one already 532 if (($dsConf['type'] ?? '') === 'input' && ($dsConf['renderType'] ?? '') === 'inputLink' && empty($dsConf['softref'])) { 533 $dsConf['softref'] = 'typolink'; 534 } 535 // Add DB: 536 $resultsFromDatabase = $this->getRelations_procDB($dataValue, $dsConf, $uid, $table); 537 if (!empty($resultsFromDatabase)) { 538 // Create an entry for the field with all DB relations: 539 $this->temp_flexRelations['db'][$structurePath] = $resultsFromDatabase; 540 } 541 // Soft References: 542 if (is_array($dataValue) || (string)$dataValue !== '') { 543 $softRefValue = $dataValue; 544 foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($dsConf['softref'] ?? '') as $softReferenceParser) { 545 $parserResult = $softReferenceParser->parse($table, $field, $uid, $softRefValue, $structurePath); 546 if ($parserResult->hasMatched()) { 547 $this->temp_flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements(); 548 if ($parserResult->hasContent()) { 549 $softRefValue = $parserResult->getContent(); 550 } 551 } 552 } 553 if (!empty($this->temp_flexRelations['softrefs']) && (string)$dataValue !== (string)$softRefValue) { 554 $this->temp_flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $softRefValue; 555 } 556 } 557 } 558 559 /** 560 * Check field configuration if it is a DB relation field and extract DB relations if any 561 * 562 * @param string $value Field value 563 * @param array $conf Field configuration array of type "TCA/columns 564 * @param int $uid Field uid 565 * @param string $table Table name 566 * @param array $row 567 * @return array|bool If field type is OK it will return an array with the database relations. Else FALSE 568 */ 569 protected function getRelations_procDB($value, $conf, $uid, $table = '', array $row = []) 570 { 571 // Get IRRE relations 572 if (empty($conf)) { 573 return false; 574 } 575 if ($conf['type'] === 'inline' && !empty($conf['foreign_table']) && empty($conf['MM'])) { 576 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); 577 $dbAnalysis->setUseLiveReferenceIds(false); 578 $dbAnalysis->setWorkspaceId($this->getWorkspaceId()); 579 $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf); 580 return $dbAnalysis->itemArray; 581 // DB record lists: 582 } 583 if ($this->isDbReferenceField($conf)) { 584 $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table']; 585 if ($conf['MM_opposite_field'] ?? false) { 586 // Never handle sys_refindex when looking at MM from foreign side 587 return []; 588 } 589 590 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); 591 $dbAnalysis->setWorkspaceId($this->getWorkspaceId()); 592 $dbAnalysis->start($value, $allowedTables, $conf['MM'] ?? '', $uid, $table, $conf); 593 $itemArray = $dbAnalysis->itemArray; 594 595 if (ExtensionManagementUtility::isLoaded('workspaces') 596 && $this->getWorkspaceId() > 0 597 && !empty($conf['MM'] ?? '') 598 && !empty($conf['allowed'] ?? '') 599 && empty($conf['MM_opposite_field'] ?? '') 600 && (int)($row['t3ver_wsid'] ?? 0) === 0 601 ) { 602 // When dealing with local side mm relations in workspace 0, there may be workspace records on the foreign 603 // side, for instance when those got an additional category. See ManyToMany Modify addCategoryRelations test. 604 // In those cases, the full set of relations must be written to sys_refindex as workspace rows. 605 // But, if the relations in this workspace and live are identical, no sys_refindex workspace rows 606 // have to be added. 607 $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class); 608 $dbAnalysis->setWorkspaceId(0); 609 $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf); 610 $itemArrayLive = $dbAnalysis->itemArray; 611 if ($itemArrayLive === $itemArray) { 612 $itemArray = false; 613 } 614 } 615 return $itemArray; 616 } 617 return false; 618 } 619 620 /******************************* 621 * 622 * Setting values 623 * 624 *******************************/ 625 626 /** 627 * Setting the value of a reference or removing it completely. 628 * Usage: For lowlevel clean up operations! 629 * WARNING: With this you can set values that are not allowed in the database since it will bypass all checks for validity! 630 * Hence it is targeted at clean-up operations. Please use DataHandler in the usual ways if you wish to manipulate references. 631 * Since this interface allows updates to soft reference values (which DataHandler does not directly) you may like to use it 632 * for that as an exception to the warning above. 633 * Notice; If you want to remove multiple references from the same field, you MUST start with the one having the highest 634 * sorting number. If you don't the removal of a reference with a lower number will recreate an index in which the remaining 635 * references in that field has new hash-keys due to new sorting numbers - and you will get errors for the remaining operations 636 * which cannot find the hash you feed it! 637 * To ensure proper working only admin-BE_USERS in live workspace should use this function 638 * 639 * @param string $hash 32-byte hash string identifying the record from sys_refindex which you wish to change the value for 640 * @param mixed $newValue Value you wish to set for reference. If NULL, the reference is removed (unless a soft-reference in which case it can only be set to a blank string). If you wish to set a database reference, use the format "[table]:[uid]". Any other case, the input value is set as-is 641 * @param bool $returnDataArray Return $dataArray only, do not submit it to database. 642 * @param bool $bypassWorkspaceAdminCheck If set, it will bypass check for workspace-zero and admin user 643 * @return string|bool|array FALSE (=OK), error message string or array (if $returnDataArray is set!) 644 */ 645 public function setReferenceValue($hash, $newValue, $returnDataArray = false, $bypassWorkspaceAdminCheck = false) 646 { 647 $backendUser = $this->getBackendUser(); 648 if ($backendUser->workspace === 0 && $backendUser->isAdmin() || $bypassWorkspaceAdminCheck) { 649 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); 650 $queryBuilder->getRestrictions()->removeAll(); 651 652 // Get current index from Database 653 $referenceRecord = $queryBuilder 654 ->select('*') 655 ->from('sys_refindex') 656 ->where( 657 $queryBuilder->expr()->eq('hash', $queryBuilder->createNamedParameter($hash, \PDO::PARAM_STR)) 658 ) 659 ->setMaxResults(1) 660 ->executeQuery() 661 ->fetchAssociative(); 662 663 // Check if reference existed. 664 if (!is_array($referenceRecord)) { 665 return 'ERROR: No reference record with hash="' . $hash . '" was found!'; 666 } 667 668 if (empty($GLOBALS['TCA'][$referenceRecord['tablename']])) { 669 return 'ERROR: Table "' . $referenceRecord['tablename'] . '" was not in TCA!'; 670 } 671 672 // Get that record from database 673 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 674 ->getQueryBuilderForTable($referenceRecord['tablename']); 675 $queryBuilder->getRestrictions()->removeAll(); 676 $record = $queryBuilder 677 ->select('*') 678 ->from($referenceRecord['tablename']) 679 ->where( 680 $queryBuilder->expr()->eq( 681 'uid', 682 $queryBuilder->createNamedParameter($referenceRecord['recuid'], \PDO::PARAM_INT) 683 ) 684 ) 685 ->setMaxResults(1) 686 ->executeQuery() 687 ->fetchAssociative(); 688 689 if (is_array($record)) { 690 // Get relation for single field from record 691 $recordRelations = $this->getRelations($referenceRecord['tablename'], $record, $referenceRecord['field']); 692 if ($fieldRelation = $recordRelations[$referenceRecord['field']]) { 693 // Initialize data array that is to be sent to DataHandler afterwards: 694 $dataArray = []; 695 // Based on type 696 switch ((string)$fieldRelation['type']) { 697 case 'db': 698 $error = $this->setReferenceValue_dbRels($referenceRecord, $fieldRelation['itemArray'], $newValue, $dataArray); 699 if ($error) { 700 return $error; 701 } 702 break; 703 case 'flex': 704 // DB references in FlexForms 705 if (is_array($fieldRelation['flexFormRels']['db'][$referenceRecord['flexpointer']])) { 706 $error = $this->setReferenceValue_dbRels($referenceRecord, $fieldRelation['flexFormRels']['db'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']); 707 if ($error) { 708 return $error; 709 } 710 } 711 // Soft references in FlexForms 712 if ($referenceRecord['softref_key'] && is_array($fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']]['keys'][$referenceRecord['softref_key']])) { 713 $error = $this->setReferenceValue_softreferences($referenceRecord, $fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']); 714 if ($error) { 715 return $error; 716 } 717 } 718 break; 719 } 720 // Soft references in the field: 721 if ($referenceRecord['softref_key'] && is_array($fieldRelation['softrefs']['keys'][$referenceRecord['softref_key']])) { 722 $error = $this->setReferenceValue_softreferences($referenceRecord, $fieldRelation['softrefs'], $newValue, $dataArray); 723 if ($error) { 724 return $error; 725 } 726 } 727 // Data Array, now ready to be sent to DataHandler 728 if ($returnDataArray) { 729 return $dataArray; 730 } 731 // Execute CMD array: 732 $dataHandler = GeneralUtility::makeInstance(DataHandler::class); 733 $dataHandler->dontProcessTransformations = true; 734 $dataHandler->bypassWorkspaceRestrictions = true; 735 // Otherwise this may lead to permission issues if user is not admin 736 $dataHandler->bypassAccessCheckForRecords = true; 737 // Check has been done previously that there is a backend user which is Admin and also in live workspace 738 $dataHandler->start($dataArray, []); 739 $dataHandler->process_datamap(); 740 // Return errors if any: 741 if (!empty($dataHandler->errorLog)) { 742 return LF . 'DataHandler:' . implode(LF . 'DataHandler:', $dataHandler->errorLog); 743 } 744 } 745 } 746 } else { 747 return 'ERROR: BE_USER object is not admin OR not in workspace 0 (Live)'; 748 } 749 750 return false; 751 } 752 753 /** 754 * Setting a value for a reference for a DB field: 755 * 756 * @param array $refRec sys_refindex record 757 * @param array $itemArray Array of references from that field 758 * @param string $newValue Value to substitute current value with (or NULL to unset it) 759 * @param array $dataArray Data array in which the new value is set (passed by reference) 760 * @param string $flexPointer Flexform pointer, if in a flex form field. 761 * @return string Error message if any, otherwise FALSE = OK 762 */ 763 protected function setReferenceValue_dbRels($refRec, $itemArray, $newValue, &$dataArray, $flexPointer = '') 764 { 765 if ((int)$itemArray[$refRec['sorting']]['id'] === (int)$refRec['ref_uid'] && (string)$itemArray[$refRec['sorting']]['table'] === (string)$refRec['ref_table']) { 766 // Setting or removing value: 767 // Remove value: 768 if ($newValue === null) { 769 unset($itemArray[$refRec['sorting']]); 770 } else { 771 [$itemArray[$refRec['sorting']]['table'], $itemArray[$refRec['sorting']]['id']] = explode(':', $newValue); 772 } 773 // Traverse and compile new list of records: 774 $saveValue = []; 775 foreach ($itemArray as $pair) { 776 $saveValue[] = $pair['table'] . '_' . $pair['id']; 777 } 778 // Set in data array: 779 if ($flexPointer) { 780 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = ArrayUtility::setValueByPath( 781 [], 782 substr($flexPointer, 0, -1), 783 implode(',', $saveValue) 784 ); 785 } else { 786 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = implode(',', $saveValue); 787 } 788 } else { 789 return 'ERROR: table:id pair "' . $refRec['ref_table'] . ':' . $refRec['ref_uid'] . '" did not match that of the record ("' . $itemArray[$refRec['sorting']]['table'] . ':' . $itemArray[$refRec['sorting']]['id'] . '") in sorting index "' . $refRec['sorting'] . '"'; 790 } 791 792 return false; 793 } 794 795 /** 796 * Setting a value for a soft reference token 797 * 798 * @param array $refRec sys_refindex record 799 * @param array $softref Array of soft reference occurrences 800 * @param string $newValue Value to substitute current value with 801 * @param array $dataArray Data array in which the new value is set (passed by reference) 802 * @param string $flexPointer Flexform pointer, if in a flex form field. 803 * @return string Error message if any, otherwise FALSE = OK 804 */ 805 protected function setReferenceValue_softreferences($refRec, $softref, $newValue, &$dataArray, $flexPointer = '') 806 { 807 if (!is_array($softref['keys'][$refRec['softref_key']][$refRec['softref_id']])) { 808 return 'ERROR: Soft reference parser key "' . $refRec['softref_key'] . '" or the index "' . $refRec['softref_id'] . '" was not found.'; 809 } 810 811 // Set new value: 812 $softref['keys'][$refRec['softref_key']][$refRec['softref_id']]['subst']['tokenValue'] = '' . $newValue; 813 // Traverse softreferences and replace in tokenized content to rebuild it with new value inside: 814 foreach ($softref['keys'] as $sfIndexes) { 815 foreach ($sfIndexes as $data) { 816 $softref['tokenizedContent'] = str_replace('{softref:' . $data['subst']['tokenID'] . '}', $data['subst']['tokenValue'], $softref['tokenizedContent']); 817 } 818 } 819 // Set in data array: 820 if (!str_contains($softref['tokenizedContent'], '{softref:')) { 821 if ($flexPointer) { 822 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = ArrayUtility::setValueByPath( 823 [], 824 substr($flexPointer, 0, -1), 825 $softref['tokenizedContent'] 826 ); 827 } else { 828 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = $softref['tokenizedContent']; 829 } 830 } else { 831 return 'ERROR: After substituting all found soft references there were still soft reference tokens in the text. (theoretically this does not have to be an error if the string "{softref:" happens to be in the field for another reason.)'; 832 } 833 834 return false; 835 } 836 837 /******************************* 838 * 839 * Helper functions 840 * 841 *******************************/ 842 843 /** 844 * Returns TRUE if the TCA/columns field type is a DB reference field 845 * 846 * @param array $configuration Config array for TCA/columns field 847 * @return bool TRUE if DB reference field (group/db or select with foreign-table) 848 */ 849 protected function isDbReferenceField(array $configuration): bool 850 { 851 return 852 ($configuration['type'] === 'group' && ($configuration['internal_type'] ?? '') !== 'folder') 853 || ( 854 in_array($configuration['type'], ['select', 'category', 'inline'], true) 855 && !empty($configuration['foreign_table']) 856 ); 857 } 858 859 /** 860 * Returns TRUE if the TCA/columns field type is a reference field 861 * 862 * @param array $configuration Config array for TCA/columns field 863 * @return bool TRUE if reference field 864 */ 865 protected function isReferenceField(array $configuration): bool 866 { 867 return 868 $this->isDbReferenceField($configuration) 869 || ($configuration['type'] === 'input' && isset($configuration['renderType']) && $configuration['renderType'] === 'inputLink') 870 || $configuration['type'] === 'flex' 871 || isset($configuration['softref']) 872 ; 873 } 874 875 /** 876 * Returns all fields of a table which could contain a relation 877 * 878 * @param string $tableName Name of the table 879 * @return array Fields which may contain relations 880 */ 881 protected function fetchTableRelationFields(string $tableName): array 882 { 883 if (!empty($this->tableRelationFieldCache[$tableName])) { 884 return $this->tableRelationFieldCache[$tableName]; 885 } 886 if (!isset($GLOBALS['TCA'][$tableName]['columns'])) { 887 return []; 888 } 889 $fields = []; 890 foreach ($GLOBALS['TCA'][$tableName]['columns'] as $field => $fieldDefinition) { 891 if (is_array($fieldDefinition['config'])) { 892 // Check for flex field 893 if (isset($fieldDefinition['config']['type']) && $fieldDefinition['config']['type'] === 'flex') { 894 // Fetch all fields if the is a field of type flex in the table definition because the complete row is passed to 895 // FlexFormTools->getDataStructureIdentifier() in the end and might be needed in ds_pointerField or a hook 896 $this->tableRelationFieldCache[$tableName] = ['*']; 897 return ['*']; 898 } 899 // Only fetch this field if it can contain a reference 900 if ($this->isReferenceField($fieldDefinition['config'])) { 901 $fields[] = $field; 902 } 903 } 904 } 905 $this->tableRelationFieldCache[$tableName] = $fields; 906 return $fields; 907 } 908 909 /** 910 * Updating Index (External API) 911 * 912 * @param bool $testOnly If set, only a test 913 * @param ProgressListenerInterface|null $progressListener If set, the current progress is added to the listener 914 * @return array Header and body status content 915 * @todo: Consider moving this together with the helper methods to a dedicated class. 916 */ 917 public function updateIndex($testOnly, ?ProgressListenerInterface $progressListener = null) 918 { 919 $errors = []; 920 $tableNames = []; 921 $recCount = 0; 922 $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces'); 923 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 924 $refIndexConnectionName = empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']['sys_refindex']) 925 ? ConnectionPool::DEFAULT_CONNECTION_NAME 926 : $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']['sys_refindex']; 927 928 // Drop sys_refindex rows from deleted workspaces 929 $listOfActiveWorkspaces = $this->getListOfActiveWorkspaces(); 930 $unusedWorkspaceRows = $this->getAmountOfUnusedWorkspaceRowsInReferenceIndex($listOfActiveWorkspaces); 931 if ($unusedWorkspaceRows > 0) { 932 $error = 'Index table hosted ' . $unusedWorkspaceRows . ' indexes for non-existing or deleted workspaces, now removed.'; 933 $errors[] = $error; 934 if ($progressListener) { 935 $progressListener->log($error, LogLevel::WARNING); 936 } 937 if (!$testOnly) { 938 $this->removeUnusedWorkspaceRowsFromReferenceIndex($listOfActiveWorkspaces); 939 } 940 } 941 942 // Main loop traverses all records of all TCA tables 943 foreach ($GLOBALS['TCA'] as $tableName => $cfg) { 944 if ($this->shouldExcludeTableFromReferenceIndex($tableName)) { 945 continue; 946 } 947 $tableConnectionName = empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]) 948 ? ConnectionPool::DEFAULT_CONNECTION_NAME 949 : $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]; 950 951 // Some additional magic is needed if the table has a field that is the local side of 952 // a mm relation. See the variable usage below for details. 953 $tableHasLocalSideMmRelation = false; 954 foreach (($cfg['columns'] ?? []) as $fieldConfig) { 955 if (!empty($fieldConfig['config']['MM'] ?? '') 956 && !empty($fieldConfig['config']['allowed'] ?? '') 957 && empty($fieldConfig['config']['MM_opposite_field'] ?? '') 958 ) { 959 $tableHasLocalSideMmRelation = true; 960 } 961 } 962 963 $fields = ['uid']; 964 if (BackendUtility::isTableWorkspaceEnabled($tableName)) { 965 $fields[] = 't3ver_wsid'; 966 } 967 // Traverse all records in tables, including deleted records 968 $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName); 969 $queryBuilder->getRestrictions()->removeAll(); 970 try { 971 $queryResult = $queryBuilder 972 ->select(...$fields) 973 ->from($tableName) 974 ->orderBy('uid') 975 ->executeQuery(); 976 } catch (DBALException $e) { 977 // Table exists in TCA but does not exist in the database 978 $this->logger->error('Table {table_name} exists in TCA but does not exist in the database. You should run the Database Analyzer in the Install Tool to fix this.', [ 979 'table_name' => $tableName, 980 'exception' => $e, 981 ]); 982 continue; 983 } 984 985 if ($progressListener) { 986 $progressListener->start($queryResult->rowCount(), $tableName); 987 } 988 $tableNames[] = $tableName; 989 while ($record = $queryResult->fetchAssociative()) { 990 if ($progressListener) { 991 $progressListener->advance(); 992 } 993 994 if ($isWorkspacesLoaded && $tableHasLocalSideMmRelation && (int)($record['t3ver_wsid'] ?? 0) === 0) { 995 // If we have record that can be the local side of a workspace relation, workspace records 996 // may point to it, even though the record has no workspace overlay. See workspace ManyToMany 997 // Modify addCategoryRelation as example. In those cases, we need to iterate all active workspaces 998 // and update refindex for all foreign workspace records that point to it. 999 foreach ($listOfActiveWorkspaces as $workspaceId) { 1000 $refIndexObj = GeneralUtility::makeInstance(self::class); 1001 $refIndexObj->setWorkspaceId($workspaceId); 1002 $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly); 1003 $recCount++; 1004 if ($result['addedNodes'] || $result['deletedNodes']) { 1005 $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes'; 1006 $errors[] = $error; 1007 if ($progressListener) { 1008 $progressListener->log($error, LogLevel::WARNING); 1009 } 1010 } 1011 } 1012 } else { 1013 $refIndexObj = GeneralUtility::makeInstance(self::class); 1014 if (isset($record['t3ver_wsid'])) { 1015 $refIndexObj->setWorkspaceId($record['t3ver_wsid']); 1016 } 1017 $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly); 1018 $recCount++; 1019 if ($result['addedNodes'] || $result['deletedNodes']) { 1020 $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes'; 1021 $errors[] = $error; 1022 if ($progressListener) { 1023 $progressListener->log($error, LogLevel::WARNING); 1024 } 1025 } 1026 } 1027 } 1028 if ($progressListener) { 1029 $progressListener->finish(); 1030 } 1031 1032 // Subselect based queries only work on the same connection 1033 // @todo: Consider dropping this in v12 and always use sub select: The base set of tables should 1034 // be in exactly one DB and only tables like caches should be "extractable" to a different DB?! 1035 // Even though sys_refindex is a "cache-like" table since it only holds secondary information that 1036 // can always be re-created by analyzing the entire data set, it shouldn't be possible to run it 1037 // on a different database since that prevents quick joins between sys_refindex and target relations. 1038 // We should probably have some report and/or install tool check to make sure all main tables 1039 // are on the same connection in v12. 1040 if ($refIndexConnectionName !== $tableConnectionName) { 1041 $this->logger->error('Not checking table {table_name} for lost indexes, "sys_refindex" table uses a different connection', ['table_name' => $tableName]); 1042 continue; 1043 } 1044 1045 // Searching for lost indexes for this table 1046 // Build sub-query to find lost records 1047 $subQueryBuilder = $connectionPool->getQueryBuilderForTable($tableName); 1048 $subQueryBuilder->getRestrictions()->removeAll(); 1049 $subQueryBuilder 1050 ->select('uid') 1051 ->from($tableName, 'sub_' . $tableName) 1052 ->where( 1053 $subQueryBuilder->expr()->eq( 1054 'sub_' . $tableName . '.uid', 1055 $queryBuilder->quoteIdentifier('sys_refindex.recuid') 1056 ) 1057 ); 1058 1059 // Main query to find lost records 1060 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex'); 1061 $queryBuilder->getRestrictions()->removeAll(); 1062 $lostIndexes = $queryBuilder 1063 ->count('hash') 1064 ->from('sys_refindex') 1065 ->where( 1066 $queryBuilder->expr()->eq( 1067 'tablename', 1068 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR) 1069 ), 1070 'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')' 1071 ) 1072 ->executeQuery() 1073 ->fetchOne(); 1074 1075 if ($lostIndexes > 0) { 1076 $error = 'Table ' . $tableName . ' has ' . $lostIndexes . ' lost indexes which are now deleted'; 1077 $errors[] = $error; 1078 if ($progressListener) { 1079 $progressListener->log($error, LogLevel::WARNING); 1080 } 1081 if (!$testOnly) { 1082 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex'); 1083 $queryBuilder->delete('sys_refindex') 1084 ->where( 1085 $queryBuilder->expr()->eq( 1086 'tablename', 1087 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR) 1088 ), 1089 'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')' 1090 ) 1091 ->executeStatement(); 1092 } 1093 } 1094 } 1095 1096 // Searching lost indexes for non-existing tables 1097 // @todo: Consider moving this *before* the main re-index logic to have a smaller 1098 // dataset when starting with heavy lifting. 1099 $lostTables = $this->getAmountOfUnusedTablesInReferenceIndex($tableNames); 1100 if ($lostTables > 0) { 1101 $error = 'Index table hosted ' . $lostTables . ' indexes for non-existing tables, now removed'; 1102 $errors[] = $error; 1103 if ($progressListener) { 1104 $progressListener->log($error, LogLevel::WARNING); 1105 } 1106 if (!$testOnly) { 1107 $this->removeReferenceIndexDataFromUnusedDatabaseTables($tableNames); 1108 } 1109 } 1110 $errorCount = count($errors); 1111 $recordsCheckedString = $recCount . ' records from ' . count($tableNames) . ' tables were checked/updated.'; 1112 if ($progressListener) { 1113 if ($errorCount) { 1114 $progressListener->log($recordsCheckedString . 'Updates: ' . $errorCount, LogLevel::WARNING); 1115 } else { 1116 $progressListener->log($recordsCheckedString . 'Index Integrity was perfect!', LogLevel::INFO); 1117 } 1118 } 1119 if (!$testOnly) { 1120 $registry = GeneralUtility::makeInstance(Registry::class); 1121 $registry->set('core', 'sys_refindex_lastUpdate', $GLOBALS['EXEC_TIME']); 1122 } 1123 return ['resultText' => trim($recordsCheckedString), 'errors' => $errors]; 1124 } 1125 1126 /** 1127 * Helper method of updateIndex(). 1128 * Create list of non-deleted "active" workspace uid's. This contains at least 0 "live workspace". 1129 * 1130 * @return int[] 1131 */ 1132 private function getListOfActiveWorkspaces(): array 1133 { 1134 if (!ExtensionManagementUtility::isLoaded('workspaces')) { 1135 // If ext:workspaces is not loaded, "0" is the only valid one. 1136 return [0]; 1137 } 1138 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace'); 1139 // There are no "hidden" workspaces, which wouldn't make much sense anyways. 1140 $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1141 $result = $queryBuilder->select('uid')->from('sys_workspace')->orderBy('uid')->executeQuery(); 1142 // "0", plus non-deleted workspaces are active 1143 $activeWorkspaces = [0]; 1144 while ($row = $result->fetchFirstColumn()) { 1145 $activeWorkspaces[] = (int)$row[0]; 1146 } 1147 return $activeWorkspaces; 1148 } 1149 1150 /** 1151 * Helper method of updateIndex() to find number of rows in sys_refindex that 1152 * relate to a non-existing or deleted workspace record, even if workspaces is 1153 * not loaded at all, but has been loaded somewhere in the past and sys_refindex 1154 * rows have been created. 1155 */ 1156 private function getAmountOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces): int 1157 { 1158 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); 1159 $numberOfInvalidWorkspaceRecords = $queryBuilder->count('hash') 1160 ->from('sys_refindex') 1161 ->where( 1162 $queryBuilder->expr()->notIn( 1163 'workspace', 1164 $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY) 1165 ) 1166 ) 1167 ->executeQuery() 1168 ->fetchOne(); 1169 return (int)$numberOfInvalidWorkspaceRecords; 1170 } 1171 1172 /** 1173 * Pair method of getAmountOfUnusedWorkspaceRowsInReferenceIndex() to actually delete 1174 * sys_refindex rows of deleted workspace records, or all if ext:workspace is not loaded. 1175 */ 1176 private function removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces): void 1177 { 1178 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex'); 1179 $queryBuilder->delete('sys_refindex') 1180 ->where( 1181 $queryBuilder->expr()->notIn( 1182 'workspace', 1183 $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY) 1184 ) 1185 ) 1186 ->executeStatement(); 1187 } 1188 1189 protected function getAmountOfUnusedTablesInReferenceIndex(array $tableNames): int 1190 { 1191 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 1192 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex'); 1193 $queryBuilder->getRestrictions()->removeAll(); 1194 $lostTables = $queryBuilder 1195 ->count('hash') 1196 ->from('sys_refindex') 1197 ->where( 1198 $queryBuilder->expr()->notIn( 1199 'tablename', 1200 $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY) 1201 ) 1202 )->executeQuery() 1203 ->fetchOne(); 1204 return (int)$lostTables; 1205 } 1206 1207 protected function removeReferenceIndexDataFromUnusedDatabaseTables(array $tableNames): void 1208 { 1209 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 1210 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex'); 1211 $queryBuilder->delete('sys_refindex') 1212 ->where( 1213 $queryBuilder->expr()->notIn( 1214 'tablename', 1215 $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY) 1216 ) 1217 )->executeStatement(); 1218 } 1219 1220 /** 1221 * Get one record from database. 1222 * 1223 * @param string $tableName 1224 * @param int $uid 1225 * @return array|false 1226 */ 1227 protected function getRecord(string $tableName, int $uid) 1228 { 1229 // Fetch fields of the table which might contain relations 1230 $tableRelationFields = $this->fetchTableRelationFields($tableName); 1231 1232 if ($tableRelationFields === []) { 1233 // Return if there are no fields which could contain relations 1234 return $this->relations; 1235 } 1236 if ($tableRelationFields !== ['*']) { 1237 // Only fields that might contain relations are fetched 1238 $tableRelationFields[] = 'uid'; 1239 if (BackendUtility::isTableWorkspaceEnabled($tableName)) { 1240 $tableRelationFields = array_merge($tableRelationFields, ['t3ver_wsid', 't3ver_state']); 1241 } 1242 } 1243 1244 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1245 ->getQueryBuilderForTable($tableName); 1246 $queryBuilder->getRestrictions()->removeAll(); 1247 $queryBuilder 1248 ->select(...array_unique($tableRelationFields)) 1249 ->from($tableName) 1250 ->where( 1251 $queryBuilder->expr()->eq( 1252 'uid', 1253 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 1254 ) 1255 ); 1256 // Do not fetch soft deleted records 1257 $deleteField = (string)($GLOBALS['TCA'][$tableName]['ctrl']['delete'] ?? ''); 1258 if ($deleteField !== '') { 1259 $queryBuilder->andWhere( 1260 $queryBuilder->expr()->eq( 1261 $deleteField, 1262 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 1263 ) 1264 ); 1265 } 1266 return $queryBuilder->executeQuery()->fetchAssociative(); 1267 } 1268 1269 /** 1270 * Checks if a given table should be excluded from ReferenceIndex 1271 * 1272 * @param string $tableName Name of the table 1273 * @return bool true if it should be excluded 1274 */ 1275 protected function shouldExcludeTableFromReferenceIndex(string $tableName): bool 1276 { 1277 if (isset($this->excludedTables[$tableName])) { 1278 return $this->excludedTables[$tableName]; 1279 } 1280 // Only exclude tables from ReferenceIndex which do not contain any relations and never 1281 // did since existing references won't be deleted! 1282 $event = new IsTableExcludedFromReferenceIndexEvent($tableName); 1283 $event = $this->eventDispatcher->dispatch($event); 1284 $this->excludedTables[$tableName] = $event->isTableExcluded(); 1285 return $this->excludedTables[$tableName]; 1286 } 1287 1288 /** 1289 * Checks if a given column in a given table should be excluded in the ReferenceIndex process 1290 * 1291 * @param string $tableName Name of the table 1292 * @param string $column Name of the column 1293 * @param string $onlyColumn Name of a specific column to fetch 1294 * @return bool true if it should be excluded 1295 */ 1296 protected function shouldExcludeTableColumnFromReferenceIndex( 1297 string $tableName, 1298 string $column, 1299 string $onlyColumn 1300 ): bool { 1301 if (isset($this->excludedColumns[$column])) { 1302 return true; 1303 } 1304 if (is_array($GLOBALS['TCA'][$tableName]['columns'][$column] ?? false) 1305 && (!$onlyColumn || $onlyColumn === $column) 1306 ) { 1307 return false; 1308 } 1309 return true; 1310 } 1311 1312 /** 1313 * Enables the runtime-based caches 1314 * Could lead to side effects, depending if the reference index instance is run multiple times 1315 * while records would be changed. 1316 * 1317 * @deprecated since v11, will be removed in v12. 1318 */ 1319 public function enableRuntimeCache() 1320 { 1321 trigger_error('Calling ReferenceIndex->enableRuntimeCache() is obsolete and should be dropped.', E_USER_DEPRECATED); 1322 } 1323 1324 /** 1325 * Disables the runtime-based cache 1326 * 1327 * @deprecated since v11, will be removed in v12. 1328 */ 1329 public function disableRuntimeCache() 1330 { 1331 trigger_error('Calling ReferenceIndex->disableRuntimeCache() is obsolete and should be dropped.', E_USER_DEPRECATED); 1332 } 1333 1334 /** 1335 * Returns the current BE user. 1336 * 1337 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication 1338 */ 1339 protected function getBackendUser() 1340 { 1341 return $GLOBALS['BE_USER']; 1342 } 1343} 1344