$fieldConfig) { if (($fieldConfig['config']['type'] ?? '') !== 'file') { continue; } if (!$this->getBackendUser()->check('tables_modify', self::FILE_REFERENCE_TABLE)) { // Early return if user is not allowed to modify the file reference table continue; } if (!$result['tcaSchemata']->has(self::FILE_REFERENCE_TABLE)) { throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' does not exists', 1664364262); } $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE); if (!$fileReferenceSchema->hasField(self::FOREIGN_SELECTOR)) { throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' has no column ' . self::FOREIGN_SELECTOR, 1770975128); } $childField = $fileReferenceSchema->getField(self::FOREIGN_SELECTOR); if ($childField->getType() !== 'group' || !($childField->getConfiguration()['allowed'] ?? false)) { throw new \UnexpectedValueException( 'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field ' . self::FOREIGN_SELECTOR . ' of table ' . self::FILE_REFERENCE_TABLE . ', but this field ' . 'is either not defined, is not of type "group" or does not define the "allowed" option.', 1664364263 ); } $result['processedTca']['columns'][$fieldName]['children'] = []; $result = $this->initializeMinMaxItems($result, $fieldName); $result = $this->initializeParentSysLanguageUid($result, $fieldName); $result = $this->initializeAppearance($result, $fieldName); // If field is set to readOnly, set all fields of the relation to readOnly as well if ($result['inlineParentConfig']['readOnly'] ?? false) { foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) { $result['processedTca']['columns'][$columnName]['config']['readOnly'] = true; } } // Resolve existing file references - this is usually always done except on ajax calls if ($result['inlineResolveExistingChildren']) { $result = $this->resolveFileReferences($result, $fieldName); if (!empty($fieldConfig['config']['selectorOrUniqueConfiguration'])) { throw new \RuntimeException('selectorOrUniqueConfiguration not implemented for TCA type "file"', 1664380909); } } } return $result; } protected function initializeMinMaxItems(array $result, string $fieldName): array { $config = $result['processedTca']['columns'][$fieldName]['config']; $config['minitems'] = isset($config['minitems']) ? MathUtility::forceIntegerInRange($config['minitems'], 0) : 0; $config['maxitems'] = isset($config['maxitems']) ? MathUtility::forceIntegerInRange($config['maxitems'], 1) : 99999; $result['processedTca']['columns'][$fieldName]['config'] = $config; return $result; } protected function initializeParentSysLanguageUid(array $result, string $fieldName): array { if (($parentLanguageFieldName = (string)($result['processedTca']['ctrl']['languageField'] ?? '')) === '' || !$result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE)->hasCapability(TcaSchemaCapability::Language) || isset($result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid']) || !isset($result['databaseRow'][$parentLanguageFieldName]) ) { return $result; } $result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid'] = is_array($result['databaseRow'][$parentLanguageFieldName]) ? (int)($result['databaseRow'][$parentLanguageFieldName][0] ?? 0) : (int)$result['databaseRow'][$parentLanguageFieldName]; return $result; } protected function initializeAppearance(array $result, string $fieldName): array { $result['processedTca']['columns'][$fieldName]['config']['appearance'] = array_replace_recursive( [ 'useSortable' => true, 'headerThumbnail' => [ 'height' => '45m', ], 'enabledControls' => [ 'edit' => true, 'info' => true, 'dragdrop' => true, 'sort' => false, 'hide' => true, 'delete' => true, 'localize' => true, ], ], $result['processedTca']['columns'][$fieldName]['config']['appearance'] ?? [] ); return $result; } /** * Substitute the value in databaseRow of this inline field with an array * that contains the databaseRows of currently connected records and some meta information. */ protected function resolveFileReferences(array $result, string $fieldName): array { if ($result['defaultLanguageRow'] !== null) { return $this->resolveFileReferenceOverlays($result, $fieldName); } $fileReferenceUidsOfDefaultLanguageRecord = $this->resolveFileReferenceUids( $result['processedTca']['columns'][$fieldName]['config'], $result['tableName'], $result['databaseRow']['uid'], $result['databaseRow'][$fieldName] ); $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfDefaultLanguageRecord); if ($result['inlineCompileExistingChildren']) { $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE); foreach ($this->getSubstitutedWorkspacedUids($fileReferenceUidsOfDefaultLanguageRecord, $fileReferenceSchema) as $uid) { try { $compiledFileReference = $this->compileFileReference($result, $fieldName, $uid); $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference; } catch (DatabaseRecordException $e) { // Nothing to do here, missing file reference is just not being rendered. } } } return $result; } /** * Substitute the value in databaseRow of this file field with an array * that contains the databaseRows of currently connected file references * and some meta information. */ protected function resolveFileReferenceOverlays(array $result, string $fieldName): array { $fileReferenceUidsOfLocalizedOverlay = []; $fieldConfig = $result['processedTca']['columns'][$fieldName]['config']; if ($result['command'] === 'edit') { $fileReferenceUidsOfLocalizedOverlay = $this->resolveFileReferenceUids( $fieldConfig, $result['tableName'], $result['databaseRow']['uid'], $result['databaseRow'][$fieldName] ); } $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfLocalizedOverlay); $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE); $fileReferenceUidsOfLocalizedOverlay = $this->getSubstitutedWorkspacedUids($fileReferenceUidsOfLocalizedOverlay, $fileReferenceSchema); if ($result['inlineCompileExistingChildren']) { $tableNameWithDefaultRecords = $result['tableName']; $fileReferenceUidsOfDefaultLanguageRecord = $this->getSubstitutedWorkspacedUids( $this->resolveFileReferenceUids( $fieldConfig, $tableNameWithDefaultRecords, $result['defaultLanguageRow']['uid'], $result['defaultLanguageRow'][$fieldName] ), $fileReferenceSchema ); // Find which records are localized, which records are not localized and which are localized but miss default language record $fieldNameWithDefaultLanguageUid = $fileReferenceSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName() ?? ''; $showPossibleLocalizationRecords = $fieldConfig['appearance']['showPossibleLocalizationRecords'] ?? false; foreach ($fileReferenceUidsOfLocalizedOverlay as $localizedUid) { try { $localizedRecord = $this->getRecordFromDatabase(self::FILE_REFERENCE_TABLE, $localizedUid); } catch (DatabaseRecordException $e) { // The child could not be compiled, probably it was deleted and a dangling mm record exists $this->logger->warning( $e->getMessage(), [ 'table' => self::FILE_REFERENCE_TABLE, 'uid' => $localizedUid, 'exception' => $e, ] ); continue; } // Compile localized record $compiledFileReference = $this->compileFileReference($result, $fieldName, $localizedUid); $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference; // If that relation is configured to "showPossibleLocalizationRecords", this localized record // needs to be removed from the list of records that are pending to be localized. if ($showPossibleLocalizationRecords) { $uidOfDefaultLanguageRecord = (int)$localizedRecord[$fieldNameWithDefaultLanguageUid]; if (in_array($uidOfDefaultLanguageRecord, $fileReferenceUidsOfDefaultLanguageRecord, true)) { // This localized child has a default language record. Remove this record from list of default language records $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]); } $uidOfDefaultLanguageRecordWorkspaceVersionArray = $this->getSubstitutedWorkspacedUids([$uidOfDefaultLanguageRecord], $fileReferenceSchema); if (!empty($uidOfDefaultLanguageRecordWorkspaceVersionArray) && in_array($uidOfDefaultLanguageRecordWorkspaceVersionArray[0], $fileReferenceUidsOfDefaultLanguageRecord, true) ) { // In some situations 'l10n_parent' of a localized workspace record points to the live version // of the default language record, and not to the workspace version, even though it exists. // Filter those as well, since the interface would otherwise show the item as "can be localized/synchronized". $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecordWorkspaceVersionArray[0]]); } } } if ($showPossibleLocalizationRecords) { foreach ($fileReferenceUidsOfDefaultLanguageRecord as $defaultLanguageUid) { // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that // exist in default language, but are not localized yet. Compile and mark those try { $compiledFileReference = $this->compileFileReference($result, $fieldName, $defaultLanguageUid, true); } catch (DatabaseRecordException $e) { // The child could not be compiled, probably it was deleted and a dangling mm record exists $this->logger->warning( $e->getMessage(), [ 'table' => self::FILE_REFERENCE_TABLE, 'uid' => $defaultLanguageUid, 'exception' => $e, ] ); continue; } $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference; } } } return $result; } protected function compileFileReference(array $result, string $parentFieldName, int $childUid, $isInlineDefaultLanguageRecordInLocalizedParentContext = false): array { $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0) ?: []; return GeneralUtility::makeInstance(FormDataCompiler::class) ->compile( [ 'request' => $result['request'], 'command' => 'edit', 'tableName' => self::FILE_REFERENCE_TABLE, 'vanillaUid' => $childUid, 'returnUrl' => $result['returnUrl'], 'isInlineChild' => true, 'isInlineDefaultLanguageRecordInLocalizedParentContext' => $isInlineDefaultLanguageRecordInLocalizedParentContext, 'inlineStructure' => $result['inlineStructure'], 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], 'inlineFirstPid' => $result['inlineFirstPid'], 'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'], 'inlineParentUid' => $result['databaseRow']['uid'], 'inlineParentTableName' => $result['tableName'], 'inlineParentFieldName' => $parentFieldName, 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '', 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '', 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '', // pass through schemata as they are immutable once they are set 'tcaSchemata' => $result['tcaSchemata'], // pass through fullTca as it is immutable once set 'fullTca' => $result['fullTca'], ], GeneralUtility::makeInstance(TcaDatabaseRecord::class) ); } /** * Substitute given list of uids with corresponding workspace uids - if needed * * @param int[] $connectedUids List of file reference uids * @return int[] List of substituted uids */ protected function getSubstitutedWorkspacedUids(array $connectedUids, TcaSchema $fileReferenceSchema): array { $workspace = $this->getBackendUser()->workspace; if ($workspace === 0 || !$fileReferenceSchema->hasCapability(TcaSchemaCapability::Workspace)) { return $connectedUids; } $substitutedUids = []; foreach ($connectedUids as $uid) { $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord( $workspace, self::FILE_REFERENCE_TABLE, $uid, 'uid,t3ver_state' ); if (!empty($workspaceVersion)) { $versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0); if ($versionState === VersionState::DELETE_PLACEHOLDER) { continue; } $uid = $workspaceVersion['uid']; } $substitutedUids[] = (int)$uid; } return $substitutedUids; } /** * Resolve file reference uids using the RelationHandler * * @return int[] */ protected function resolveFileReferenceUids( array $parentConfig, $parentTableName, $parentUid, $parentFieldValue ): array { $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); $relationHandler->start( $parentFieldValue, self::FILE_REFERENCE_TABLE, '', BackendUtility::getLiveVersionIdOfRecord($parentTableName, $parentUid) ?? $parentUid, $parentTableName, $parentConfig ); return array_map(intval(...), $relationHandler->getValueArray()); } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } }