TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
+385
View File
@@ -0,0 +1,385 @@
<?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\Backend\Form\FormDataProvider;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Resolve and prepare files data.
*/
class TcaFiles extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
private const string FOREIGN_SELECTOR = 'uid_local';
public function __construct(
private readonly InlineStackProcessor $inlineStackProcessor,
private readonly LoggerInterface $logger,
) {}
public function addData(array $result): array
{
// inlineFirstPid is currently resolved by TcaInline
// @todo check if duplicating the functionality makes sense to resolve dependencies
foreach ($result['processedTca']['columns'] as $fieldName => $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'];
}
}