Files
cms-core/Classes/Database/ReferenceIndex.php
T

1205 lines
68 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LogLevel;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\ProgressListenerInterface;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent;
use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface;
use TYPO3\CMS\Core\Schema\RelationshipType;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Reference index processing and relation extraction.
*
* @internal Extensions shouldn't fiddle with the reference index themselves, it's task of DataHandler to do this.
*/
#[Autoconfigure(public: true)]
class ReferenceIndex
{
/**
* Increase if a change in the code means we will have to force a re-generation of the index.
* MUST be int since xxhash ignores non-int seeds, see https://github.com/php/php-src/issues/10305
*
* @var positive-int
*/
private const int HASH_VERSION = 1;
/**
* Key list of tables to exclude from ReferenceIndex. Only $GLOBALS['TCA'] need to be listed here, if at all.
* This is a performance improvement to skip tables "irrelevant" in refindex scope.
* An event may alter this.
* The list depends on TCA and an event, entries are managed once per class instance.
* Array with fields as keys and booleans as values for fast isset() lookup instead of slower in_array().
*/
private array $excludedTables = [];
/**
* A list of fields that may contain relations per TCA table.
* The list depends on TCA, entries are created once per table, per class instance.
*/
private array $tableRelationFieldCache = [];
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly SoftReferenceParserFactory $softReferenceParserFactory,
private readonly ConnectionPool $connectionPool,
private readonly Registry $registry,
private readonly FlexFormTools $flexFormTools,
private readonly TcaSchemaFactory $tcaSchemaFactory,
) {}
/**
* Returns the amount of references for the given record.
*/
public function getNumberOfReferencedRecords(string $tableName, int $uid): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
return (int)$queryBuilder
->count('*')->from('sys_refindex')
->where(
$queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($tableName)),
$queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))
)->executeQuery()->fetchOne();
}
/**
* Update full refindex. Used by 'referenceindex:update' CLI and ext:lowlevel BE UI.
*
* @param ProgressListenerInterface|null $progressListener If set, the current progress is added to the listener
* @return array Header and body status content
* @internal
*/
public function updateIndex(bool $testOnly, ?ProgressListenerInterface $progressListener = null): array
{
$errors = [];
$numberOfHandledRecords = 0;
$isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
$tcaTableNames = $this->tcaSchemaFactory->all()->getNames();
// @todo: Ensure tcaSchemaFactory->all() always sorts alphabetically (or add test to verify), then remove this sort()
sort($tcaTableNames);
$progressListener?->log('Remember to create missing tables and columns before running this.', LogLevel::WARNING);
// Remove dangling workspace sys_refindex rows
$listOfActiveWorkspaces = $this->getListOfActiveWorkspaces();
$numberOfUnusedWorkspaceRows = $testOnly
? $this->getNumberOfUnusedWorkspaceRowsInReferenceIndex($listOfActiveWorkspaces)
: $this->removeUnusedWorkspaceRowsFromReferenceIndex($listOfActiveWorkspaces);
if ($numberOfUnusedWorkspaceRows > 0) {
$error = 'Index table hosted ' . $numberOfUnusedWorkspaceRows . ' indexes for non-existing or deleted workspaces, now removed.';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
// Remove sys_refindex rows of tables no longer defined in TCA
$numberOfRowsOfOldTables = $testOnly
? $this->getNumberOfUnusedTablesInReferenceIndex($tcaTableNames)
: $this->removeReferenceIndexDataFromUnusedDatabaseTables($tcaTableNames);
if ($numberOfRowsOfOldTables > 0) {
$error = 'Index table hosted ' . $numberOfRowsOfOldTables . ' indexes for non-existing tables, now removed';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
// Main loop traverses all records of all TCA tables
foreach ($tcaTableNames as $tableName) {
$tableTcaSchema = $this->tcaSchemaFactory->get($tableName);
// Count number of records in table to have a correct $numberOfHandledRecords in the end
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$numberOfRecordsInTargetTable = $queryBuilder
->count('uid')
->from($tableName)
->executeQuery()
->fetchOne();
$progressListener?->start($numberOfRecordsInTargetTable, $tableName);
if ($numberOfRecordsInTargetTable === 0 || $this->shouldExcludeTableFromReferenceIndex($tableName) || empty($this->getTableRelationFields($tableName))) {
// Table is empty, should be excluded, or can not have relations. Blindly remove any existing sys_refindex rows.
$numberOfHandledRecords += $numberOfRecordsInTargetTable;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
if ($testOnly) {
$countDeleted = $queryBuilder
->count('hash')
->from('sys_refindex')
->where($queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)))
->executeQuery()
->fetchOne();
} else {
$countDeleted = $queryBuilder
->delete('sys_refindex')
->where($queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)))
->executeStatement();
}
if ($countDeleted > 0) {
$error = 'Index table hosted ' . $countDeleted . ' ignored or outdated indexed, now removed.';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
$progressListener?->finish();
continue;
}
// Delete lost indexes of table: sys_refindex rows where the uid no longer exists in target table.
$subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$subQueryBuilder->getRestrictions()->removeAll();
$subQueryBuilder
->select('uid')
->from($tableName, 'sub_' . $tableName)
->where(
$subQueryBuilder->expr()->eq('sub_' . $tableName . '.uid', $subQueryBuilder->quoteIdentifier('sys_refindex.recuid'))
);
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
if ($testOnly) {
$numberOfRefindexRowsWithoutExistingTableRow = $queryBuilder
->count('hash')
->from('sys_refindex')
->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)),
'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
)
->executeQuery()
->fetchOne();
} else {
$numberOfRefindexRowsWithoutExistingTableRow = $queryBuilder
->delete('sys_refindex')
->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)),
'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
)
->executeStatement();
}
if ($numberOfRefindexRowsWithoutExistingTableRow > 0) {
$error = 'Table ' . $tableName . ' hosted ' . $numberOfRefindexRowsWithoutExistingTableRow . ' lost indexes, now removed.';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
// Delete rows in sys_refindex related to this table where the record is soft-deleted=1.
if ($tableTcaSchema->hasCapability(TcaSchemaCapability::SoftDelete)) {
$softDeleteFieldName = $tableTcaSchema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$numberOfDeletedRecordsInTargetTable = $queryBuilder
->count('uid')
->from($tableName)
->where($queryBuilder->expr()->eq($softDeleteFieldName, 1))
->executeQuery()
->fetchOne();
if ($numberOfDeletedRecordsInTargetTable > 0) {
$numberOfHandledRecords += $numberOfDeletedRecordsInTargetTable;
// List of deleted=0 records in target table that have records in sys_refindex.
if ($testOnly) {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
// $subQueryBuilder actually fills parameter placeholders for the main $queryBuilder.
// The subQuery is never meant to be executed on its own, only used to be filled-in
// via $subQueryBuilder->getSQL().
$subQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$subQueryBuilder->getRestrictions()->removeAll();
$subQueryBuilder
->select('sub_' . $tableName . '.uid')
->distinct()
->from($tableName, 'sub_' . $tableName)
->join(
'sub_' . $tableName,
'sys_refindex',
'sub_refindex',
$queryBuilder->expr()->eq('sub_refindex.recuid', $queryBuilder->quoteIdentifier('sub_' . $tableName . '.uid'))
)
->where(
$queryBuilder->expr()->eq('sub_refindex.tablename', $queryBuilder->createNamedParameter($tableName)),
$queryBuilder->expr()->eq('sub_' . $tableName . '.' . $softDeleteFieldName, 1),
);
$numberOfRemovedIndexes = $queryBuilder
->count('hash')
->from('sys_refindex')
->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)),
$queryBuilder->quoteIdentifier('recuid') . ' IN ( ' . $subQueryBuilder->getSQL() . ' )'
)
->executeQuery()
->fetchOne();
} else {
// MySQL is picky when using the same table in a sub-query and an outer delete query, if
// it is not materialized into a temporary table. Enforcing a temporary table would mitigate this
// MySQL limit, but we simply fetch the affected uid list instead and fire a chunked delete query.
// In contrast to $testOnly above, we execute the subQuery, named parameter placeholders need
// to be relative to its QueryBuilder.
$uidListQueryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$uidListQueryBuilder->getRestrictions()->removeAll();
$uidListQueryBuilder
->select('sub_' . $tableName . '.uid')
->distinct()
->from($tableName, 'sub_' . $tableName)
->join(
'sub_' . $tableName,
'sys_refindex',
'sub_refindex',
$uidListQueryBuilder->expr()->eq('sub_refindex.recuid', $uidListQueryBuilder->quoteIdentifier('sub_' . $tableName . '.uid'))
)
->where(
$uidListQueryBuilder->expr()->eq('sub_refindex.tablename', $uidListQueryBuilder->createNamedParameter($tableName)),
$uidListQueryBuilder->expr()->eq('sub_' . $tableName . '.' . $softDeleteFieldName, 1),
);
$uidListOfRemovableIndexes = $uidListQueryBuilder->executeQuery()->fetchFirstColumn();
$numberOfRemovedIndexes = 0;
// Another variant to solve this would be a limit/offset query for the upper query, feeding delete.
// This would be more memory efficient. We however think there shouldn't be *that* many affected
// rows to delete in casual scenarios, so we skip that optimization for now since chunking isn't
// needed in most cases anyway.
// 10k is an arbitrary number. Reasoning: 1MB max query length with 10-char uids (9mio uid-range with comma)
// would allow ~10k uids. Combi tablename/recuid is indexed, so delete should be relatively quick even with
// larger sets, so delete-hard-locking on for instance innodb shouldn't be a huge issue here.
foreach (array_chunk($uidListOfRemovableIndexes, 10000) as $uidChunkOfRemovableIndexes) {
$chunkQueryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$chunkQueryBuilder->getRestrictions()->removeAll();
$chunkedNumberOfRemovedIndexes = $chunkQueryBuilder
->delete('sys_refindex')
->where(
$chunkQueryBuilder->expr()->eq('tablename', $chunkQueryBuilder->createNamedParameter($tableName)),
$chunkQueryBuilder->expr()->in('recuid', $uidChunkOfRemovableIndexes)
)
->executeStatement();
$numberOfRemovedIndexes += $chunkedNumberOfRemovedIndexes;
}
}
if ($numberOfRemovedIndexes > 0) {
$error = 'Table ' . $tableName . ' hosted ' . $numberOfRemovedIndexes . ' indexes from soft-deleted records, now removed.';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
$progressListener?->advance($numberOfDeletedRecordsInTargetTable);
}
}
// Some additional magic is needed if the table has a field that is the local side of
// a mm relation. See the variable usage below for details.
$tableHasLocalSideMmRelation = false;
foreach ($tableTcaSchema->getFields() as $field) {
$fieldConfig = $field->getConfiguration();
if (!empty($fieldConfig['MM'] ?? '')
// Catch type=group 'allowed' and type=select 'foreign_table' MM scenarios
&& (!empty($fieldConfig['allowed'] ?? '') || !empty($fieldConfig['foreign_table'] ?? ''))
&& empty($fieldConfig['MM_opposite_field'] ?? '')
) {
$tableHasLocalSideMmRelation = true;
}
}
// Traverse all records in table, not including soft-deleted records
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryResult = $queryBuilder
->select('*')
->from($tableName)
->orderBy('uid')
->executeQuery();
while ($record = $queryResult->fetchAssociative()) {
$progressListener?->advance();
if ($isWorkspacesLoaded && $tableHasLocalSideMmRelation && (int)($record['t3ver_wsid'] ?? 0) === 0) {
// If we have a record that can be the local side of a workspace relation, workspace records
// may point to it, even though the record has no workspace overlay. See workspace ManyToMany
// Modify addCategoryRelation as example. In those cases, we need to iterate all active workspaces
// and update refindex for all foreign workspace records that point to it.
foreach ($listOfActiveWorkspaces as $workspaceId) {
$result = $this->updateRefIndexTable($tableName, (int)$record['uid'], $testOnly, $workspaceId, $record);
$numberOfHandledRecords++;
if ($result['addedNodes'] || $result['deletedNodes']) {
$error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
}
} else {
$result = $this->updateRefIndexTable($tableName, (int)$record['uid'], $testOnly, (int)($record['t3ver_wsid'] ?? 0), $record);
$numberOfHandledRecords++;
if ($result['addedNodes'] || $result['deletedNodes']) {
$error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
$errors[] = $error;
$progressListener?->log($error, LogLevel::WARNING);
}
}
}
$progressListener?->finish();
}
$errorCount = count($errors);
$recordsCheckedString = $numberOfHandledRecords . ' records from ' . count($tcaTableNames) . ' tables were checked/updated.';
if ($errorCount) {
$progressListener?->log($recordsCheckedString . ' Updates: ' . $errorCount, LogLevel::WARNING);
} else {
$progressListener?->log($recordsCheckedString . ' Index Integrity was perfect.');
}
if (!$testOnly) {
$this->registry->set('core', 'sys_refindex_lastUpdate', $GLOBALS['EXEC_TIME']);
}
return ['resultText' => trim($recordsCheckedString), 'errors' => $errors];
}
/**
* Update the sys_refindex table for a record, even one just deleted.
* This is used by DataHandler ReferenceIndexUpdater as entry method to take care of single records.
* It is also used internally via updateIndex() by CLI "referenceindex:update" and lowlevel BE module.
*
* @param array|null $currentRecord Current full (select *) record from DB. Optimization for updateIndex().
* @return array Statistics about how many index records were added, deleted and not altered.
* @internal
*/
public function updateRefIndexTable(string $tableName, int $uid, bool $testOnly = false, int $workspaceUid = 0, ?array $currentRecord = null): array
{
$result = [
'keptNodes' => 0,
'deletedNodes' => 0,
'addedNodes' => 0,
];
if ($uid < 1 || $this->shouldExcludeTableFromReferenceIndex($tableName) || empty($this->getTableRelationFields($tableName))) {
// Not a valid uid, the table is excluded, or can not contain relations.
return $result;
}
if ($currentRecord === null) {
// Fetch record if not provided.
$currentRecord = BackendUtility::getRecord($tableName, $uid);
}
$currentRelationHashes = $this->getCurrentRelationHashes($tableName, $uid, $workspaceUid);
if ($currentRecord === null) {
// If there is no record because it was hard or soft-deleted, remove any existing sys_refindex rows of it.
$numberOfLeftOverRelationHashes = count($currentRelationHashes);
$result['deletedNodes'] = $numberOfLeftOverRelationHashes;
if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) {
$this->removeRelationHashes($currentRelationHashes);
}
return $result;
}
$relations = $this->compileReferenceIndexRowsForRecord($tableName, $currentRecord, $workspaceUid);
$connection = $this->connectionPool->getConnectionForTable('sys_refindex');
$relationsToInsert = [];
foreach ($relations as $relation) {
if (!is_array($relation)) {
continue;
}
// Exclude any relations TO a specific table
if (($relation['ref_table'] ?? '') && $this->shouldExcludeTableFromReferenceIndex($relation['ref_table'])) {
continue;
}
$relation['hash'] = hash(algo: 'xxh128', data: implode(',', $relation), options: ['seed' => self::HASH_VERSION]);
// First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!)
if (isset($currentRelationHashes[$relation['hash']])) {
unset($currentRelationHashes[$relation['hash']]);
$result['keptNodes']++;
} else {
// If new, register for bulk add:
if (!$testOnly) {
$relationsToInsert[] = $relation;
}
$result['addedNodes']++;
}
}
if (!$testOnly && !empty($relationsToInsert)) {
try {
$connection->bulkInsert('sys_refindex', $relationsToInsert, array_keys(current($relationsToInsert)));
} catch (\Exception $e) {
// Do nothing for the time being
}
}
// If any existing are left, they are not in the current set anymore. Remove them.
$numberOfLeftOverRelationHashes = count($currentRelationHashes);
$result['deletedNodes'] = $numberOfLeftOverRelationHashes;
if ($numberOfLeftOverRelationHashes > 0 && !$testOnly) {
$this->removeRelationHashes($currentRelationHashes);
}
return $result;
}
/**
* Returns relation information for a record from a TCA table.
*
* @return array Array with information about relations
* @internal
*/
public function getRelations(string $tableName, array $record, int $workspaceUid): array
{
$result = [];
$relationFields = $this->getTableRelationFields($tableName);
$tableTcaSchema = $this->tcaSchemaFactory->get($tableName);
foreach ($relationFields as $fieldName) {
$value = $record[$fieldName] ?? null;
if (!$tableTcaSchema->hasField($fieldName)) {
continue;
}
$field = $tableTcaSchema->getField($fieldName);
$fieldConfig = $field->getConfiguration();
$resultsFromDatabase = $this->getRelationsFromRelationField($tableName, $value, $fieldConfig, (int)$record['uid'], $workspaceUid, $record);
if (!empty($resultsFromDatabase)) {
// Create an entry for the field with all DB relations:
$result[$fieldName] = [
'type' => 'db',
'itemArray' => $resultsFromDatabase,
];
}
if ($field->isType(TableColumnType::FLEX) && is_string($value) && $value !== '') {
// Traverse the flex data structure looking for db references for flex fields.
$flexFormRelations = $this->getRelationsFromFlexData($tableName, $fieldName, $record, $workspaceUid);
if (!empty($flexFormRelations)) {
$result[$fieldName] = [
'type' => 'flex',
'flexFormRels' => $flexFormRelations,
];
}
}
if ((string)$value !== '') {
// Soft References
$softRefValue = $value;
$softReferenceKeys = $field->getSoftReferenceKeys();
if ($softReferenceKeys !== false) {
foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList(implode(',', $softReferenceKeys)) as $softReferenceParser) {
$parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$record['uid'], $softRefValue);
if ($parserResult->hasMatched()) {
$result[$fieldName]['softrefs']['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
if ($parserResult->hasContent()) {
$softRefValue = $parserResult->getContent();
}
}
}
}
if (!empty($result[$fieldName]['softrefs']) && (string)$value !== (string)$softRefValue && str_contains($softRefValue, '{softref:')) {
$result[$fieldName]['softrefs']['tokenizedContent'] = $softRefValue;
}
}
}
return $result;
}
/**
* Get current sys_refindex rows of table:uid from database with hash as index.
*
* @return array<string, true>
*/
private function getCurrentRelationHashes(string $tableName, int $uid, int $workspaceUid): array
{
$connection = $this->connectionPool->getConnectionForTable('sys_refindex');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryResult = $queryBuilder->select('hash')->from('sys_refindex')->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName)),
$queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($workspaceUid, Connection::PARAM_INT))
)->executeQuery();
$currentRelationHashes = [];
while ($relation = $queryResult->fetchAssociative()) {
$currentRelationHashes[$relation['hash']] = true;
}
return $currentRelationHashes;
}
/**
* Remove sys_refindex rows by hash.
*
* @param array<string, true> $currentRelationHashes
*/
private function removeRelationHashes(array $currentRelationHashes): void
{
$connection = $this->connectionPool->getConnectionForTable('sys_refindex');
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
$chunks = array_chunk(array_keys($currentRelationHashes), $maxBindParameters - 10, true);
foreach ($chunks as $chunk) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder
->delete('sys_refindex')
->where(
$queryBuilder->expr()->in('hash', $queryBuilder->createNamedParameter($chunk, Connection::PARAM_STR_ARRAY))
)
->executeStatement();
}
}
private function compileReferenceIndexRowsForRecord(string $tableName, array $record, int $workspaceUid): array
{
$relations = [];
$tableTcaSchema = $this->tcaSchemaFactory->get($tableName);
$hiddenFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
? (int)$record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()]
: 0;
$starttimeFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)
? (int)$record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()]
: 0;
$endtimeFieldValue = $tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)
? (int)($record[$tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?: 2147483647)
: 2147483647; // @todo: 2^31-1 (year 2038) and not 2^32-1 since postgres 32-bit int is always signed
$recordRelations = $this->getRelations($tableName, $record, $workspaceUid);
foreach ($recordRelations as $fieldName => $fieldRelations) {
$field = $tableTcaSchema->getField($fieldName);
if ($tableTcaSchema->isWorkspaceAware()
&& isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $workspaceUid
&& $field instanceof RelationalFieldTypeInterface
&& $field->getRelationshipType() !== RelationshipType::ManyToMany
) {
// The given record is workspace-enabled but doesn't live in the selected workspace. Don't add index, it's not actually there.
// 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.
// See workspaces ManyToMany Modify addCategoryRelation for details on this case.
continue;
}
if (is_array($fieldRelations['itemArray'] ?? false) && !empty($fieldRelations['itemArray'])) {
// DB relations in a db field
$itemArray = $fieldRelations['itemArray'];
if ($field->isType(TableColumnType::INLINE, TableColumnType::FILE)
&& $field instanceof RelationalFieldTypeInterface
&& $field->getRelationshipType()->isSingularRelationship()
) {
// RelationHandler does not return info on hidden, starttime, endtime for inline non-MM, yet. Add this now.
// @todo: Refactor RelationHandler / PlainDataResolver to (optionally?) return full child row record
$itemArray = $this->enrichInlineRelations(current($itemArray)['table'], $fieldRelations['itemArray']);
}
if ($field->isType(TableColumnType::INLINE, TableColumnType::GROUP, TableColumnType::SELECT)
&& $field instanceof RelationalFieldTypeInterface
&& $field->getRelationshipType() === RelationshipType::ManyToMany
) {
foreach ($itemArray as $itemKey => $item) {
// Get rid of soft-deleted foreign records, those should not create refindex entries
// @todo: It would be better if RelationHandler would not return soft-deleted MM rows. Unsure
// how to do that since RH only works on the MM table, but it could then also return
// the full foreign row along the way.
// @todo: Expensive. This code could be optimized to fetch multiple records at once per foreign
// table, or make RH return full foreign rows in some hopefully efficient way.
$foreignSideRecord = BackendUtility::getRecord($item['table'], (int)$item['id']);
if ($foreignSideRecord === null) {
// @todo: This mixes up ref_sorting when rows are removed here. Shouldn't be
// very problematic, though.
unset($itemArray[$itemKey]);
continue;
}
$itemTableSchema = $this->tcaSchemaFactory->get((string)$item['table']);
if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
$disabledFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
$itemArray[$itemKey][$disabledFieldName] = $foreignSideRecord[$disabledFieldName];
}
if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
$starttimeFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
$itemArray[$itemKey][$starttimeFieldName] = $foreignSideRecord[$starttimeFieldName];
}
if ($itemTableSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
$endtimeFieldName = $itemTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
$itemArray[$itemKey][$endtimeFieldName] = $foreignSideRecord[$endtimeFieldName];
}
if ($itemTableSchema->hasCapability(TcaSchemaCapability::Workspace)) {
$itemArray[$itemKey]['t3ver_state'] = $foreignSideRecord['t3ver_state'];
}
}
}
$sorting = 0;
foreach ($itemArray as $refRecord) {
$refTable = (string)$refRecord['table'];
$refTcaTableSchema = $this->tcaSchemaFactory->get($refTable);
$relations[] = [
'tablename' => $tableName,
'recuid' => (int)$record['uid'],
'field' => $fieldName,
'hidden' => $hiddenFieldValue,
'starttime' => $starttimeFieldValue,
'endtime' => $endtimeFieldValue,
't3ver_state' => (int)($record['t3ver_state'] ?? 0),
'flexpointer' => '',
'softref_key' => '',
'softref_id' => '',
'sorting' => $sorting,
'workspace' => $workspaceUid,
'ref_table' => $refTable,
'ref_uid' => (int)$refRecord['id'],
'ref_field' => (string)($refRecord['fieldname'] ?? ''),
'ref_hidden' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0)
: 0,
'ref_starttime' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0)
: 0,
'ref_endtime' => $refTcaTableSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)
? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647)
: 2147483647,
'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0),
'ref_sorting' => (int)($refRecord['sorting_foreign'] ?? 0),
'ref_string' => '',
];
$sorting++;
}
}
if (is_array($fieldRelations['softrefs']['keys'] ?? false)) {
// Soft reference relations in a db field
foreach ($fieldRelations['softrefs']['keys'] as $softrefKey => $elements) {
if (!is_array($elements)) {
continue;
}
foreach ($elements as $softrefId => $element) {
if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) {
continue;
}
$refTable = '_STRING';
$refUid = 0;
$refString = '';
$refRecord = [];
$refTcaTableSchema = null;
if ($element['subst']['type'] === 'db') {
$explodedRefTableUid = explode(':', $element['subst']['recordRef']);
$refTable = $explodedRefTableUid[0];
$refUid = (int)$explodedRefTableUid[1];
if ($refRecord = BackendUtility::getRecord($refTable, $refUid)) {
// @todo: It would be great to refactor the softref parser mess "data structure"
// and let it return the reference record along the way - "db" type softrefs
// fetch those already.
$refTcaTableSchema = $this->tcaSchemaFactory->get($refTable);
} else {
// Sanitize the target record: If it does not exist, we should not create a softref
// entry to it. Also ref_uid is the uid of the target record. If a softref parser
// goes rogue and misinterprets for instance a huge number ("telephone") as uid,
// creating this as softref would try to insert a "bigger than 2^31 int" for ref_uid,
// leading to an out of bound insert.
// @todo: This late validation of softref stuff should probably be relocated to the
// parsers directly. Refactor this together with the above comment.
// @todo: It is fishy that DataHandler does not sanitize fields with attached softref
// parsers, either. Example: tt_content header_link with manual editor edit
// "+49 123456789" which looks like a phone number, but the internal phone
// syntax is "tel:49123456789". DH still happily writes the invalid thing into DB.
// This should be rethought.
continue;
}
} else {
$refString = mb_substr($element['subst']['tokenValue'], 0, 1024);
}
$relations[] = [
'tablename' => $tableName,
'recuid' => (int)$record['uid'],
'field' => $fieldName,
'hidden' => $hiddenFieldValue,
'starttime' => $starttimeFieldValue,
'endtime' => $endtimeFieldValue,
't3ver_state' => (int)($record['t3ver_state'] ?? 0),
'flexpointer' => '',
'softref_key' => (string)$softrefKey,
'softref_id' => (string)$softrefId,
'sorting' => 0,
'workspace' => $workspaceUid,
'ref_table' => $refTable,
'ref_uid' => $refUid,
'ref_field' => '',
'ref_hidden' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0)
: 0,
'ref_starttime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionStartTime)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0)
: 0,
'ref_endtime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionEndTime)
? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647)
: 2147483647,
'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0),
'ref_sorting' => 0,
'ref_string' => $refString,
];
}
}
}
if (is_array($fieldRelations['flexFormRels']['db'] ?? false)) {
// DB relations in a flex field
foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) {
$sorting = 0;
foreach ($subList as $refRecord) {
// @todo: This has no proper test setup in ReferenceIndexTest and ReferenceIndexWorkspaceLoadedTest.
// We probably need to fetch the target record for inline relations here, as done with
// $fieldRelations['itemArray'] enrichInlineRelations() above, to set ref_ fields hidden, starttime,
// endtime and t3ver_state. Additionally, a test based on categories should verify MM details.
$relations[] = [
'tablename' => $tableName,
'recuid' => (int)$record['uid'],
'field' => $fieldName,
'hidden' => $hiddenFieldValue,
'starttime' => $starttimeFieldValue,
'endtime' => $endtimeFieldValue,
't3ver_state' => (int)($record['t3ver_state'] ?? 0),
'flexpointer' => (string)$flexPointer,
'softref_key' => '',
'softref_id' => '',
'sorting' => $sorting,
'workspace' => $workspaceUid,
'ref_table' => $refRecord['table'],
'ref_uid' => (int)$refRecord['id'],
'ref_field' => (string)($refRecord['fieldname'] ?? ''),
// @todo: ref_hidden, ref_starttime, ref_endtime, ref_t3ver_state, ref_t3ver_state and ref_sorting need coverage and handling.
'ref_hidden' => 0,
'ref_starttime' => 0,
'ref_endtime' => 2147483647,
'ref_t3ver_state' => 0,
'ref_sorting' => 0,
'ref_string' => '',
];
$sorting++;
}
}
}
if (is_array($fieldRelations['flexFormRels']['softrefs'] ?? false)) {
// Soft reference relations in a flex field
foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) {
foreach ($subList['keys'] as $softrefKey => $elements) {
if (!is_array($elements)) {
continue;
}
foreach ($elements as $softrefId => $element) {
if (!in_array($element['subst']['type'] ?? '', ['db', 'string'], true)) {
continue;
}
$refTable = '_STRING';
$refUid = 0;
$refString = '';
$refRecord = [];
$refTcaTableSchema = null;
if ($element['subst']['type'] === 'db') {
$explodedRefTableUid = explode(':', $element['subst']['recordRef']);
$refTable = $explodedRefTableUid[0];
$refUid = (int)$explodedRefTableUid[1];
if ($refRecord = BackendUtility::getRecord($refTable, $refUid)) {
// @todo: It would be great to refactor the softref parser mess "data structure"
// and let it return the reference record along the way - "db" type softrefs
// fetch those already.
$refTcaTableSchema = $this->tcaSchemaFactory->get($refTable);
}
} else {
$refString = mb_substr($element['subst']['tokenValue'], 0, 1024);
}
$relations[] = [
'tablename' => $tableName,
'recuid' => (int)$record['uid'],
'field' => $fieldName,
'hidden' => $hiddenFieldValue,
'starttime' => $starttimeFieldValue,
'endtime' => $endtimeFieldValue,
't3ver_state' => (int)($record['t3ver_state'] ?? 0),
'flexpointer' => $flexPointer,
'softref_key' => (string)$softrefKey,
'softref_id' => (string)$softrefId,
'sorting' => 0,
'workspace' => $workspaceUid,
'ref_table' => $refTable,
'ref_uid' => $refUid,
'ref_field' => '',
'ref_hidden' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName()] ?? 0)
: 0,
'ref_starttime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionStartTime)
? (int)($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName()] ?? 0)
: 0,
'ref_endtime' => $refTcaTableSchema?->hasCapability(TcaSchemaCapability::RestrictionEndTime)
? (int)(($refRecord[$refTcaTableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName()] ?? 0) ?: 2147483647)
: 2147483647,
'ref_t3ver_state' => (int)($refRecord['t3ver_state'] ?? 0),
'ref_sorting' => 0,
'ref_string' => $refString,
];
}
}
}
}
}
return $relations;
}
/**
* RelationHandler does not return relation record details when dealing with
* inline foreign_table relations. We need fields like hidden and starrtime,
* though. Fetch them now.
*/
private function enrichInlineRelations(string $tableName, array $itemArray): array
{
$selectFields = ['uid'];
$tableTcaSchema = $this->tcaSchemaFactory->get($tableName);
if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
$selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
}
if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
$selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
}
if ($tableTcaSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
$selectFields[] = $tableTcaSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
}
if ($tableTcaSchema->isWorkspaceAware()) {
$selectFields[] = 't3ver_state';
}
if (count($selectFields) === 1) {
return $itemArray;
}
$connection = $this->connectionPool->getConnectionForTable($tableName);
$maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$rows = [];
$uidList = array_column($itemArray, 'id');
foreach (array_chunk($uidList, $maxBindParameters - 10, true) as $chunk) {
$result = $queryBuilder->select(...$selectFields)->from($tableName)
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
)
)
->orderBy('uid', 'ASC')->executeQuery();
while ($row = $result->fetchAssociative()) {
$rows[(int)$row['uid']] = $row;
}
}
foreach ($itemArray as &$item) {
if (isset($rows[$item['id']])) {
// @todo: The isset() prevents a PHP array access warning here. It seems this can happen with
// inline CSV since RelationHandler->realList() does not verify if attached records
// really exist. There is probably a deeper issue with CSV lists here, see #106428 for
// more information and reproduce. This area should have a closer look, it looks as if
// "count" value instead of uid fields are hand over here - at least with tx_styleguide_inline_11.
$item = array_merge($item, $rows[$item['id']]);
}
}
return $itemArray;
}
private function getRelationsFromFlexData(string $tableName, string $fieldName, array $row, int $workspaceUid): array
{
$valueArray = GeneralUtility::xml2array($row[$fieldName] ?? '');
if (!is_array($valueArray)) {
// Current flex form values can not be parsed to an array. No relations.
return [];
}
try {
$tableTcaSchema = $this->tcaSchemaFactory->get($tableName);
$fieldConfig['config'] = $tableTcaSchema->getField($fieldName)->getConfiguration();
$dataStructureArray = $this->flexFormTools->parseDataStructureByIdentifier(
$this->flexFormTools->getDataStructureIdentifier($fieldConfig, $tableName, $fieldName, $row, $tableTcaSchema),
$tableTcaSchema
);
} catch (InvalidIdentifierException) {
// Data structure can not be resolved or parsed. No relations.
return [];
}
if (!is_array($dataStructureArray['sheets'] ?? false)) {
// No sheet in DS. Shouldn't happen, though.
return [];
}
$flexRelations = [];
foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementTca) {
// For all elements allowed in Data Structure.
if (($sheetElementTca['type'] ?? '') === 'array') {
// This is a section.
if (!is_array($sheetElementTca['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) {
// No possible containers defined for this section in DS, or no values set for this section.
continue;
}
foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) {
// We have containers for this section in values.
if (!is_array($valueSectionContainers ?? false)) {
// Values don't validate to an array, skip.
continue;
}
foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) {
// For all value containers in this section.
if (!is_array($sheetElementTca['el'][$valueContainerType]['el'] ?? false)) {
// There is no DS for this container type, skip.
continue;
}
foreach ($sheetElementTca['el'][$valueContainerType]['el'] as $containerElement => $containerElementTca) {
// Container type of this value container exists in DS. Iterate DS container to find value relations.
if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) {
$fieldValue = $valueContainerElements['el'][$containerElement]['vDEF'];
$structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/el/' . $valueSectionContainerKey . '/' . $valueContainerType . '/el/' . $containerElement . '/vDEF/';
if ($fieldValue !== '' && ($containerElementTca['config']['softref'] ?? '') !== '') {
$tokenizedContent = $fieldValue;
foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($containerElementTca['config']['softref']) as $softReferenceParser) {
$parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath);
if ($parserResult->hasMatched()) {
$flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
if ($parserResult->hasContent()) {
$tokenizedContent = $parserResult->getContent();
}
}
}
if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) {
$flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent;
}
}
}
}
}
}
} elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) {
// Not a section but a simple field. Get its relations.
$fieldValue = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'];
$structurePath = $sheetKey . '/lDEF/' . $sheetElementKey . '/vDEF/';
$databaseRelations = $this->getRelationsFromRelationField($tableName, $fieldValue, $sheetElementTca['config'] ?? [], (int)$row['uid'], $workspaceUid, $row);
if (!empty($databaseRelations)) {
$flexRelations['db'][$structurePath] = $databaseRelations;
}
if ($fieldValue !== '' && ($sheetElementTca['config']['softref'] ?? '') !== '') {
$tokenizedContent = $fieldValue;
foreach ($this->softReferenceParserFactory->getParsersBySoftRefParserList($sheetElementTca['config']['softref']) as $softReferenceParser) {
$parserResult = $softReferenceParser->parse($tableName, $fieldName, (int)$row['uid'], $fieldValue, $structurePath);
if ($parserResult->hasMatched()) {
$flexRelations['softrefs'][$structurePath]['keys'][$softReferenceParser->getParserKey()] = $parserResult->getMatchedElements();
if ($parserResult->hasContent()) {
$tokenizedContent = $parserResult->getContent();
}
}
}
if (!empty($flexRelations['softrefs'][$structurePath]) && $fieldValue !== $tokenizedContent) {
$flexRelations['softrefs'][$structurePath]['tokenizedContent'] = $tokenizedContent;
}
}
}
}
}
return $flexRelations;
}
/**
* Check field configuration if it is a DB relation field and extract DB relations if any
*/
private function getRelationsFromRelationField(string $tableName, mixed $fieldValue, array $conf, int $uid, int $workspaceUid, array $row): array
{
if (empty($conf)) {
return [];
}
if (($conf['type'] === 'inline' || $conf['type'] === 'file') && !empty($conf['foreign_table']) && empty($conf['MM'])) {
$dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
$dbAnalysis->setUseLiveReferenceIds(false);
$dbAnalysis->setWorkspaceId($workspaceUid);
$dbAnalysis->start($fieldValue, $conf['foreign_table'], '', $uid, $tableName, $conf);
return $dbAnalysis->itemArray;
}
if ($this->isDbReferenceField($conf)) {
$allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
if ($conf['MM_opposite_field'] ?? false) {
// Never handle sys_refindex when looking at MM from foreign side
return [];
}
$dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
$dbAnalysis->setWorkspaceId($workspaceUid);
$dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'] ?? '', $uid, $tableName, $conf);
$itemArray = $dbAnalysis->itemArray;
if (ExtensionManagementUtility::isLoaded('workspaces')
&& $workspaceUid > 0
&& !empty($conf['MM'] ?? '')
// Catch type=group 'allowed' and type=select 'foreign_table' MM scenarios
&& (!empty($conf['allowed'] ?? '') || !empty($conf['foreign_table'] ?? ''))
&& empty($conf['MM_opposite_field'] ?? '')
&& (int)($row['t3ver_wsid'] ?? 0) === 0
) {
// When dealing with local side mm relations in workspace 0, there may be workspace records on the foreign
// side, for instance when those got an additional category. See ManyToMany Modify addCategoryRelations test.
// In those cases, the full set of relations must be written to sys_refindex as workspace rows.
// But, if the relations in this workspace and live are identical, no sys_refindex workspace rows
// have to be added.
$dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
$dbAnalysis->setWorkspaceId(0);
$dbAnalysis->start($fieldValue, $allowedTables, $conf['MM'], $uid, $tableName, $conf);
$itemArrayLive = $dbAnalysis->itemArray;
if ($itemArrayLive === $itemArray) {
$itemArray = [];
}
}
return $itemArray;
}
return [];
}
/**
* Returns true if the TCA/columns field type is a DB reference field
*
* @param array $configuration Config array for TCA/columns field
* @return bool TRUE if DB reference field (group/db or select with foreign-table)
*/
private function isDbReferenceField(array $configuration): bool
{
return
$configuration['type'] === 'group'
|| (
in_array($configuration['type'], ['select', 'category', 'inline', 'file'], true)
&& !empty($configuration['foreign_table'])
);
}
/**
* Returns true if the TCA/columns field may carry references. True for
* group, inline and friends, for flex, and if there is a 'softref' definition.
*/
private function isReferenceField(FieldTypeInterface $field): bool
{
return
$this->isDbReferenceField($field->getConfiguration())
|| $field->isType(TableColumnType::FLEX)
|| $field->getSoftReferenceKeys() !== false
;
}
/**
* List of TCA columns that can have relations. Typically inline, group
* and friends, as well as flex fields and fields with 'softref' config.
* If empty, the table can not have relations.
* Uses a class cache to be quick for multiple calls on same table.
*/
private function getTableRelationFields(string $tableName): array
{
if (isset($this->tableRelationFieldCache[$tableName])) {
return $this->tableRelationFieldCache[$tableName];
}
if (!$this->tcaSchemaFactory->has($tableName)) {
$this->tableRelationFieldCache[$tableName] = [];
return [];
}
$tableTcaFields = $this->tcaSchemaFactory->get($tableName)->getFields();
$relationFields = [];
foreach ($tableTcaFields as $field) {
if ($this->isReferenceField($field)) {
$relationFields[] = $field->getName();
}
}
$this->tableRelationFieldCache[$tableName] = $relationFields;
return $relationFields;
}
/**
* Create list of non-deleted "active" workspace uids. This contains at least 0 "live workspace".
*
* @return int[]
*/
private function getListOfActiveWorkspaces(): array
{
if (!ExtensionManagementUtility::isLoaded('workspaces')) {
// If ext:workspaces is not loaded, "0" is the only valid one.
return [0];
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_workspace');
// Workspaces can't be 'hidden', so we only use deleted restriction here.
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder->select('uid')->from('sys_workspace')->orderBy('uid')->executeQuery();
// "0", plus non-deleted workspaces are active
return array_merge([0 => 0], $result->fetchFirstColumn());
}
/**
* Helper method of updateIndex() to find number of rows in sys_refindex that
* relate to a non-existing or deleted workspace record, even if workspaces is
* not loaded at all, but has been loaded somewhere in the past and sys_refindex
* rows have been created.
*/
private function getNumberOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
$numberOfInvalidWorkspaceRecords = $queryBuilder
->count('hash')
->from('sys_refindex')
->where(
$queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY))
)
->executeQuery()
->fetchOne();
return (int)$numberOfInvalidWorkspaceRecords;
}
/**
* Delete sys_refindex rows of deleted / not existing workspace records, or all if ext:workspace is not loaded.
*/
private function removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
return $queryBuilder
->delete('sys_refindex')
->where(
$queryBuilder->expr()->notIn('workspace', $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY))
)
->executeStatement();
}
/**
* When a TCA table with references has been removed, there may be old sys_refindex
* rows for it. The query finds the number of affected rows.
*/
private function getNumberOfUnusedTablesInReferenceIndex(array $tableNames): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
$numberOfRowsOfUnusedTables = $queryBuilder
->count('hash')
->from('sys_refindex')
->where(
$queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY))
)
->executeQuery()
->fetchOne();
return (int)$numberOfRowsOfUnusedTables;
}
/**
* When a TCA table with references has been removed, there may be old sys_refindex
* rows for it. The query deletes those.
*/
private function removeReferenceIndexDataFromUnusedDatabaseTables(array $tableNames): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$queryBuilder->getRestrictions()->removeAll();
return $queryBuilder
->delete('sys_refindex')
->where(
$queryBuilder->expr()->notIn('tablename', $queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY))
)
->executeStatement();
}
/**
* Checks if a given table should be excluded from ReferenceIndex
*/
private function shouldExcludeTableFromReferenceIndex(string $tableName): bool
{
if (isset($this->excludedTables[$tableName])) {
return $this->excludedTables[$tableName];
}
$event = new IsTableExcludedFromReferenceIndexEvent($tableName);
$event = $this->eventDispatcher->dispatch($event);
$this->excludedTables[$tableName] = $event->isTableExcluded();
return $this->excludedTables[$tableName];
}
}