‪TYPO3CMS  ‪main
TcaFiles.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
25 use TYPO3\CMS\Backend\Utility\BackendUtility;
32 
37 {
38  private const ‪FILE_REFERENCE_TABLE = 'sys_file_reference';
39  private const ‪FOREIGN_SELECTOR = 'uid_local';
40 
41  public function ‪addData(array $result): array
42  {
43  // inlineFirstPid is currently resolved by TcaInline
44  // @todo check if duplicating the functionality makes sense to resolve dependencies
45 
46  foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
47  if (($fieldConfig['config']['type'] ?? '') !== 'file') {
48  continue;
49  }
50 
51  if (!$this->‪getBackendUser()->check('tables_modify', self::FILE_REFERENCE_TABLE)) {
52  // Early return if user is not allowed to modify the file reference table
53  continue;
54  }
55 
56  if (!(‪$GLOBALS['TCA'][self::FILE_REFERENCE_TABLE] ?? false)) {
57  throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' does not exists', 1664364262);
58  }
59 
60  $childConfiguration = ‪$GLOBALS['TCA'][‪self::FILE_REFERENCE_TABLE]['columns'][‪self::FOREIGN_SELECTOR]['config'] ?? [];
61  if (($childConfiguration['type'] ?? '') !== 'group' || !($childConfiguration['allowed'] ?? false)) {
62  throw new \UnexpectedValueException(
63  'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field '
64  . self::FOREIGN_SELECTOR . ' of table ' . self::FILE_REFERENCE_TABLE . ', but this field '
65  . 'is either not defined, is not of type "group" or does not define the "allowed" option.',
66  1664364263
67  );
68  }
69 
70  $result['processedTca']['columns'][$fieldName]['children'] = [];
71 
72  $result = $this->‪initializeMinMaxItems($result, $fieldName);
73  $result = $this->‪initializeParentSysLanguageUid($result, $fieldName);
74  $result = $this->‪initializeAppearance($result, $fieldName);
75 
76  // If field is set to readOnly, set all fields of the relation to readOnly as well
77  if ($result['inlineParentConfig']['readOnly'] ?? false) {
78  foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
79  $result['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
80  }
81  }
82 
83  // Resolve existing file references - this is usually always done except on ajax calls
84  if ($result['inlineResolveExistingChildren']) {
85  $result = $this->‪resolveFileReferences($result, $fieldName);
86  if (!empty($fieldConfig['config']['selectorOrUniqueConfiguration'])) {
87  throw new \RuntimeException('selectorOrUniqueConfiguration not implemented for TCA type "file"', 1664380909);
88  }
89  }
90  }
91 
92  return $result;
93  }
94 
95  protected function ‪initializeMinMaxItems(array $result, string $fieldName): array
96  {
97  $config = $result['processedTca']['columns'][$fieldName]['config'];
98  $config['minitems'] = isset($config['minitems']) ? ‪MathUtility::forceIntegerInRange($config['minitems'], 0) : 0;
99  $config['maxitems'] = isset($config['maxitems']) ? ‪MathUtility::forceIntegerInRange($config['maxitems'], 1) : 99999;
100  $result['processedTca']['columns'][$fieldName]['config'] = $config;
101 
102  return $result;
103  }
104 
105  protected function ‪initializeParentSysLanguageUid(array $result, string $fieldName): array
106  {
107  if (($parentLanguageFieldName = (string)($result['processedTca']['ctrl']['languageField'] ?? '')) === ''
108  || !(‪$GLOBALS['TCA'][self::FILE_REFERENCE_TABLE]['ctrl']['languageField'] ?? false)
109  || isset($result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid'])
110  || !isset($result['databaseRow'][$parentLanguageFieldName])
111  ) {
112  return $result;
113  }
114 
115  $result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid'] =
116  is_array($result['databaseRow'][$parentLanguageFieldName])
117  ? (int)($result['databaseRow'][$parentLanguageFieldName][0] ?? 0)
118  : (int)$result['databaseRow'][$parentLanguageFieldName];
119 
120  return $result;
121  }
122 
123  protected function ‪initializeAppearance(array $result, string $fieldName): array
124  {
125  $result['processedTca']['columns'][$fieldName]['config']['appearance'] = array_replace_recursive(
126  [
127  'useSortable' => true,
128  'headerThumbnail' => [
129  'height' => '45m',
130  ],
131  'enabledControls' => [
132  'edit' => true,
133  'info' => true,
134  'dragdrop' => true,
135  'sort' => false,
136  'hide' => true,
137  'delete' => true,
138  'localize' => true,
139  ],
140  ],
141  $result['processedTca']['columns'][$fieldName]['config']['appearance'] ?? []
142  );
143 
144  return $result;
145  }
146 
151  protected function ‪resolveFileReferences(array $result, string $fieldName): array
152  {
153  if ($result['defaultLanguageRow'] !== null) {
154  return $this->‪resolveFileReferenceOverlays($result, $fieldName);
155  }
156 
157  $fileReferenceUidsOfDefaultLanguageRecord = $this->‪resolveFileReferenceUids(
158  $result['processedTca']['columns'][$fieldName]['config'],
159  $result['tableName'],
160  $result['databaseRow']['uid'],
161  $result['databaseRow'][$fieldName]
162  );
163  $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfDefaultLanguageRecord);
164 
165  if ($result['inlineCompileExistingChildren']) {
166  foreach ($this->‪getSubstitutedWorkspacedUids($fileReferenceUidsOfDefaultLanguageRecord) as ‪$uid) {
167  try {
168  $compiledFileReference = $this->‪compileFileReference($result, $fieldName, ‪$uid);
169  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
170  } catch (‪DatabaseRecordException $e) {
171  // Nothing to do here, missing file reference is just not being rendered.
172  }
173  }
174  }
175  return $result;
176  }
177 
183  protected function ‪resolveFileReferenceOverlays(array $result, string $fieldName): array
184  {
185  $fileReferenceUidsOfLocalizedOverlay = [];
186  $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
187  if ($result['command'] === 'edit') {
188  $fileReferenceUidsOfLocalizedOverlay = $this->‪resolveFileReferenceUids(
189  $fieldConfig,
190  $result['tableName'],
191  $result['databaseRow']['uid'],
192  $result['databaseRow'][$fieldName]
193  );
194  }
195  $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfLocalizedOverlay);
196  $fileReferenceUidsOfLocalizedOverlay = $this->‪getSubstitutedWorkspacedUids($fileReferenceUidsOfLocalizedOverlay);
197  if ($result['inlineCompileExistingChildren']) {
198  $tableNameWithDefaultRecords = $result['tableName'];
199  $fileReferenceUidsOfDefaultLanguageRecord = $this->‪getSubstitutedWorkspacedUids(
201  $fieldConfig,
202  $tableNameWithDefaultRecords,
203  $result['defaultLanguageRow']['uid'],
204  $result['defaultLanguageRow'][$fieldName]
205  )
206  );
207 
208  // Find which records are localized, which records are not localized and which are localized but miss default language record
209  $fieldNameWithDefaultLanguageUid = (string)(‪$GLOBALS['TCA'][self::FILE_REFERENCE_TABLE]['ctrl']['transOrigPointerField'] ?? '');
210  $showPossibleLocalizationRecords = $fieldConfig['appearance']['showPossibleLocalizationRecords'] ?? false;
211  foreach ($fileReferenceUidsOfLocalizedOverlay as $localizedUid) {
212  try {
213  $localizedRecord = $this->‪getRecordFromDatabase(self::FILE_REFERENCE_TABLE, $localizedUid);
214  } catch (‪DatabaseRecordException $e) {
215  // The child could not be compiled, probably it was deleted and a dangling mm record exists
216  $this->logger->warning(
217  $e->getMessage(),
218  [
219  'table' => self::FILE_REFERENCE_TABLE,
220  'uid' => $localizedUid,
221  'exception' => $e,
222  ]
223  );
224  continue;
225  }
226  // Compile localized record
227  $compiledFileReference = $this->‪compileFileReference($result, $fieldName, $localizedUid);
228  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
229  // If that relation is configured to "showPossibleLocalizationRecords", this localized record
230  // needs to be removed from the list of records that are pending to be localized.
231  if ($showPossibleLocalizationRecords) {
232  $uidOfDefaultLanguageRecord = (int)$localizedRecord[$fieldNameWithDefaultLanguageUid];
233  if (in_array($uidOfDefaultLanguageRecord, $fileReferenceUidsOfDefaultLanguageRecord, true)) {
234  // This localized child has a default language record. Remove this record from list of default language records
235  $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
236  }
237  $uidOfDefaultLanguageRecordWorkspaceVersionArray = $this->‪getSubstitutedWorkspacedUids([$uidOfDefaultLanguageRecord]);
238  if (!empty($uidOfDefaultLanguageRecordWorkspaceVersionArray)
239  && in_array($uidOfDefaultLanguageRecordWorkspaceVersionArray[0], $fileReferenceUidsOfDefaultLanguageRecord, true)
240  ) {
241  // In some situations 'l10n_parent' of a localized workspace record points to the live version
242  // of the default language record, and not to the workspace version, even though it exists.
243  // Filter those as well, since the interface would otherwise show the item as "can be localized/synchronized".
244  $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecordWorkspaceVersionArray[0]]);
245  }
246  }
247  }
248  if ($showPossibleLocalizationRecords) {
249  foreach ($fileReferenceUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
250  // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
251  // exist in default language, but are not localized yet. Compile and mark those
252  try {
253  $compiledFileReference = $this->‪compileFileReference($result, $fieldName, $defaultLanguageUid);
254  } catch (‪DatabaseRecordException $e) {
255  // The child could not be compiled, probably it was deleted and a dangling mm record exists
256  $this->logger->warning(
257  $e->getMessage(),
258  [
259  'table' => self::FILE_REFERENCE_TABLE,
260  'uid' => $defaultLanguageUid,
261  'exception' => $e,
262  ]
263  );
264  continue;
265  }
266  $compiledFileReference['isInlineDefaultLanguageRecordInLocalizedParentContext'] = true;
267  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
268  }
269  }
270  }
271 
272  return $result;
273  }
274 
275  protected function ‪compileFileReference(array $result, string $parentFieldName, int $childUid): array
276  {
277  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
278  $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
279  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0) ?: [];
280 
281  return GeneralUtility::makeInstance(FormDataCompiler::class)
282  ->compile(
283  [
284  'request' => $result['request'],
285  'command' => 'edit',
286  'tableName' => self::FILE_REFERENCE_TABLE,
287  'vanillaUid' => $childUid,
288  'returnUrl' => $result['returnUrl'],
289  'isInlineChild' => true,
290  'inlineStructure' => $result['inlineStructure'],
291  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
292  'inlineFirstPid' => $result['inlineFirstPid'],
293  'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'],
294  'inlineParentUid' => $result['databaseRow']['uid'],
295  'inlineParentTableName' => $result['tableName'],
296  'inlineParentFieldName' => $parentFieldName,
297  'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '',
298  'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '',
299  'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '',
300  ],
301  GeneralUtility::makeInstance(TcaDatabaseRecord::class)
302  );
303  }
304 
311  protected function ‪getSubstitutedWorkspacedUids(array $connectedUids): array
312  {
313  $workspace = $this->‪getBackendUser()->workspace;
314  if ($workspace === 0 || !BackendUtility::isTableWorkspaceEnabled(self::FILE_REFERENCE_TABLE)) {
315  return $connectedUids;
316  }
317  $substitutedUids = [];
318  foreach ($connectedUids as ‪$uid) {
319  $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord(
320  $workspace,
321  self::FILE_REFERENCE_TABLE,
322  ‪$uid,
323  'uid,t3ver_state'
324  );
325  if (!empty($workspaceVersion)) {
326  $versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0);
327  if ($versionState === VersionState::DELETE_PLACEHOLDER) {
328  continue;
329  }
330  ‪$uid = $workspaceVersion['uid'];
331  }
332  $substitutedUids[] = (int)‪$uid;
333  }
334  return $substitutedUids;
335  }
336 
342  protected function ‪resolveFileReferenceUids(
343  array $parentConfig,
344  $parentTableName,
345  $parentUid,
346  $parentFieldValue
347  ): array {
348  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
349  $relationHandler->start(
350  $parentFieldValue,
351  self::FILE_REFERENCE_TABLE,
352  '',
353  BackendUtility::getLiveVersionIdOfRecord($parentTableName, $parentUid) ?? $parentUid,
354  $parentTableName,
355  $parentConfig
356  );
357  return array_map(intval(...), $relationHandler->getValueArray());
358  }
359 
361  {
362  return ‪$GLOBALS['BE_USER'];
363  }
364 
366  {
367  return ‪$GLOBALS['LANG'];
368  }
369 }
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\FOREIGN_SELECTOR
‪const FOREIGN_SELECTOR
Definition: TcaFiles.php:39
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\compileFileReference
‪compileFileReference(array $result, string $parentFieldName, int $childUid)
Definition: TcaFiles.php:275
‪TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider
Definition: AbstractDatabaseRecordProvider.php:31
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:36
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\initializeAppearance
‪initializeAppearance(array $result, string $fieldName)
Definition: TcaFiles.php:123
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\initializeMinMaxItems
‪initializeMinMaxItems(array $result, string $fieldName)
Definition: TcaFiles.php:95
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\getBackendUser
‪getBackendUser()
Definition: TcaFiles.php:360
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\getSubstitutedWorkspacedUids
‪int[] getSubstitutedWorkspacedUids(array $connectedUids)
Definition: TcaFiles.php:311
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\resolveFileReferenceUids
‪int[] resolveFileReferenceUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue)
Definition: TcaFiles.php:342
‪TYPO3\CMS\Backend\Form\FormDataProvider\AbstractDatabaseRecordProvider\getRecordFromDatabase
‪array getRecordFromDatabase($tableName, $uid)
Definition: AbstractDatabaseRecordProvider.php:44
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\resolveFileReferenceOverlays
‪resolveFileReferenceOverlays(array $result, string $fieldName)
Definition: TcaFiles.php:183
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\initializeParentSysLanguageUid
‪initializeParentSysLanguageUid(array $result, string $fieldName)
Definition: TcaFiles.php:105
‪TYPO3\CMS\Backend\Form\FormDataProvider
Definition: AbstractDatabaseRecordProvider.php:16
‪TYPO3\CMS\Backend\Form\FormDataProviderInterface
Definition: FormDataProviderInterface.php:23
‪TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException
Definition: DatabaseRecordException.php:24
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\FILE_REFERENCE_TABLE
‪const FILE_REFERENCE_TABLE
Definition: TcaFiles.php:38
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\getLanguageService
‪getLanguageService()
Definition: TcaFiles.php:365
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles
Definition: TcaFiles.php:37
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Backend\Form\InlineStackProcessor
Definition: InlineStackProcessor.php:32
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange(mixed $theInt, int $min, int $max=2000000000, int $defaultValue=0)
Definition: MathUtility.php:34
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Form\FormDataCompiler
Definition: FormDataCompiler.php:26
‪TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord
Definition: TcaDatabaseRecord.php:25
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\resolveFileReferences
‪resolveFileReferences(array $result, string $fieldName)
Definition: TcaFiles.php:151
‪TYPO3\CMS\Backend\Form\FormDataProvider\TcaFiles\addData
‪addData(array $result)
Definition: TcaFiles.php:41