TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user