TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
TcaInline.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
31 
36 {
43  public function addData(array $result)
44  {
45  $result = $this->addInlineFirstPid($result);
46 
47  foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
48  if (!$this->isInlineField($fieldConfig) || !$this->isUserAllowedToModify($fieldConfig)) {
49  continue;
50  }
51  $result['processedTca']['columns'][$fieldName]['children'] = [];
52  if ($result['inlineResolveExistingChildren']) {
53  $result = $this->resolveRelatedRecords($result, $fieldName);
54  $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
55  }
56  }
57 
58  return $result;
59  }
60 
67  protected function isInlineField($fieldConfig)
68  {
69  return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline';
70  }
71 
78  protected function isUserAllowedToModify($fieldConfig)
79  {
80  return $this->getBackendUser()->check('tables_modify', $fieldConfig['config']['foreign_table']);
81  }
82 
91  protected function addInlineFirstPid(array $result)
92  {
93  if (is_null($result['inlineFirstPid'])) {
94  $table = $result['tableName'];
95  $row = $result['databaseRow'];
96  // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
97  if ($table == 'pages') {
98  $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
99  $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
100  } elseif ($row['pid'] < 0) {
101  $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
102  $pid = $prevRec['pid'];
103  } else {
104  $pid = $row['pid'];
105  }
106  $result['inlineFirstPid'] = (int)$pid;
107  }
108  return $result;
109  }
110 
119  protected function resolveRelatedRecords(array $result, $fieldName)
120  {
121  $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
122 
123  // localizationMode is either "none", "keep" or "select":
124  // * none: Handled parent row is not a localized record, or if it is a localized row, this is ignored.
125  // Default language records and overlays have distinct children that are not connected to each other.
126  // * keep: Handled parent row is a localized record, but child table is either not localizable, or
127  // "keep" is explicitly set. A localized parent and its default language row share the same
128  // children records. Editing a child from a localized record will change this record for the
129  // default language row, too.
130  // * select: Handled parent row is a localized record, child table is localizable. Children records are
131  // localized overlays of a default language record. Three scenarios can happen:
132  // ** Localized child overlay and its default language row exist - show localized overlay record
133  // ** Default child language row exists but child overlay doesn't - show a "synchronize this record" button
134  // ** Localized child overlay exists but default language row does not - this dangling child is a data inconsistency
135 
136  // Mode was prepared by TcaInlineConfiguration provider
137  $mode = $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'];
138  if ($mode === 'none') {
139  $connectedUids = [];
140  // A new record that has distinct children can not have children yet, fetch connected uids for existing only
141  if ($result['command'] === 'edit') {
142  $connectedUids = $this->resolveConnectedRecordUids(
143  $result['processedTca']['columns'][$fieldName]['config'],
144  $result['tableName'],
145  $result['databaseRow']['uid'],
146  $result['databaseRow'][$fieldName]
147  );
148  }
149  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
150  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
151  // @todo: If inlineCompileExistingChildren must be kept, it might be better to change the data
152  // @todo: format of databaseRow for this field and separate the child compilation to an own provider?
153  if ($result['inlineCompileExistingChildren']) {
154  foreach ($connectedUids as $childUid) {
155  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
156  }
157  }
158  } elseif ($mode === 'keep') {
159  // Fetch connected uids of default language record
160  $connectedUids = $this->resolveConnectedRecordUids(
161  $result['processedTca']['columns'][$fieldName]['config'],
162  $result['tableName'],
163  $result['defaultLanguageRow']['uid'],
164  $result['defaultLanguageRow'][$fieldName]
165  );
166  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
167  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
168  if ($result['inlineCompileExistingChildren']) {
169  foreach ($connectedUids as $childUid) {
170  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
171  }
172  }
173  } else {
174  $connectedUidsOfLocalizedOverlay = [];
175  if ($result['command'] === 'edit') {
176  $connectedUidsOfLocalizedOverlay = $this->resolveConnectedRecordUids(
177  $result['processedTca']['columns'][$fieldName]['config'],
178  $result['tableName'],
179  $result['databaseRow']['uid'],
180  $result['databaseRow'][$fieldName]
181  );
182  }
183  $result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfLocalizedOverlay);
184  if ($result['inlineCompileExistingChildren']) {
185  $tableNameWithDefaultRecords = $result['tableName'];
186  if ($tableNameWithDefaultRecords === 'pages_language_overlay') {
187  $tableNameWithDefaultRecords = 'pages';
188  }
189  $connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
190  $result['processedTca']['columns'][$fieldName]['config'],
191  $tableNameWithDefaultRecords,
192  $result['defaultLanguageRow']['uid'],
193  $result['defaultLanguageRow'][$fieldName]
194  );
195 
196  $showPossible = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'];
197 
198  // Find which records are localized, which records are not localized and which are
199  // localized but miss default language record
200  $fieldNameWithDefaultLanguageUid = $GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField'];
201  foreach ($connectedUidsOfLocalizedOverlay as $localizedUid) {
202  $localizedRecord = $this->getRecordFromDatabase($childTableName, $localizedUid);
203  $uidOfDefaultLanguageRecord = $localizedRecord[$fieldNameWithDefaultLanguageUid];
204  if (in_array($uidOfDefaultLanguageRecord, $connectedUidsOfDefaultLanguageRecord)) {
205  // This localized child has a default language record. Remove this record from list of default language records
206  $connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
207  }
208  // Compile localized record
209  $compiledChild = $this->compileChild($result, $fieldName, $localizedUid);
210  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
211  }
212  if ($showPossible) {
213  foreach ($connectedUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
214  // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
215  // exist in default language, but are not localized yet. Compile and mark those
216  $compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid);
217  $compiledChild['isInlineDefaultLanguageRecordInLocalizedParentContext'] = true;
218  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
219  }
220  }
221  }
222  }
223 
224  return $result;
225  }
226 
236  protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName)
237  {
238  if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
239  return $result;
240  }
241 
242  $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
243  $foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
244  $selectorOrUniquePossibleRecords = [];
245 
246  if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
247  // Compile child table data for this field only
248  $selectDataInput = [
249  'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
250  'command' => 'new',
251  // Since there is no existing record that may have a type, it does not make sense to
252  // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
253  'pageTsConfig' => $result['pageTsConfig'],
254  'userTsConfig' => $result['userTsConfig'],
255  'processedTca' => [
256  'ctrl' => [],
257  'columns' => [
258  $foreignFieldName => [
259  'config' => $selectorOrUniqueConfiguration['config'],
260  ],
261  ],
262  ],
263  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
264  ];
266  $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
267  $formDataGroup->setProviderList([ TcaSelectItems::class ]);
269  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
270  $compilerResult = $formDataCompiler->compile($selectDataInput);
271  $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
272  }
273 
274  $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
275 
276  return $result;
277  }
278 
287  protected function compileChild(array $result, $parentFieldName, $childUid)
288  {
289  $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
290  $childTableName = $parentConfig['foreign_table'];
291 
293  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
294  $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
295  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
296 
298  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
300  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
301  $formDataCompilerInput = [
302  'command' => 'edit',
303  'tableName' => $childTableName,
304  'vanillaUid' => (int)$childUid,
305  'isInlineChild' => true,
306  'inlineStructure' => $result['inlineStructure'],
307  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
308  'inlineFirstPid' => $result['inlineFirstPid'],
309  'inlineParentConfig' => $parentConfig,
310 
311  // values of the current parent element
312  // it is always a string either an id or new...
313  'inlineParentUid' => $result['databaseRow']['uid'],
314  'inlineParentTableName' => $result['tableName'],
315  'inlineParentFieldName' => $parentFieldName,
316 
317  // values of the top most parent element set on first level and not overridden on following levels
318  'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
319  'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
320  'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
321  ];
322 
323  // For foreign_selector with useCombination $mainChild is the mm record
324  // and $combinationChild is the child-child. For 1:n "normal" relations,
325  // $mainChild is just the normal child record and $combinationChild is empty.
326  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
327  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
328  try {
329  $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig);
330  } catch (DatabaseRecordException $e) {
331  // The child could not be compiled, probably it was deleted and a dangling mm record
332  // exists. This is a data inconsistency, we catch this exception and create a flash message
333  $message = vsprintf(
334  $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:formEngine.databaseRecordErrorInlineChildChild'),
335  [ $e->getTableName(), $e->getUid(), $childTableName, (int)$childUid ]
336  );
337  $flashMessage = GeneralUtility::makeInstance(FlashMessage::class,
338  $message,
339  '',
341  );
342  GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->enqueue($flashMessage);
343  }
344  }
345  return $mainChild;
346  }
347 
356  protected function compileChildChild(array $child, array $parentConfig)
357  {
358  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
359  $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
360  // child-child table name is set in child tca "the selector field" foreign_table
361  $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
363  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
365  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
366 
367  $formDataCompilerInput = [
368  'command' => 'edit',
369  'tableName' => $childChildTableName,
370  'vanillaUid' => (int)$childChildUid,
371  'isInlineChild' => true,
372  'isInlineChildExpanded' => $child['isInlineChildExpanded'],
373  // @todo: this is the wrong inline structure, isn't it? Shouldn't it contain the part from child child, too?
374  'inlineStructure' => $child['inlineStructure'],
375  'inlineFirstPid' => $child['inlineFirstPid'],
376  // values of the top most parent element set on first level and not overridden on following levels
377  'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
378  'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
379  'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
380  ];
381  $childChild = $formDataCompiler->compile($formDataCompilerInput);
382  return $childChild;
383  }
384 
392  protected function getWorkspacedUids(array $connectedUids, $childTableName)
393  {
394  $backendUser = $this->getBackendUser();
395  $newConnectedUids = [];
396  foreach ($connectedUids as $uid) {
397  // Fetch workspace version of a record (if any):
398  // @todo: Needs handling
399  if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($childTableName)) {
400  $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state');
401  if (!empty($workspaceVersion)) {
402  $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
403  if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
404  continue;
405  }
406  $uid = $workspaceVersion['uid'];
407  }
408  }
409  $newConnectedUids[] = $uid;
410  }
411  return $newConnectedUids;
412  }
413 
424  protected function resolveConnectedRecordUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue)
425  {
426  $directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue);
427  if (empty($parentConfig['MM'])) {
428  $parentUid = $this->getLiveDefaultId($parentTableName, $parentUid);
429  }
431  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
432  $relationHandler->registerNonTableValues = (bool)$parentConfig['allowedIdValues'];
433  $relationHandler->start($parentFieldValue, $parentConfig['foreign_table'], $parentConfig['MM'], $parentUid, $parentTableName, $parentConfig);
434  $foreignRecordUids = $relationHandler->getValueArray();
435  $resolvedForeignRecordUids = [];
436  foreach ($foreignRecordUids as $aForeignRecordUid) {
437  if ($parentConfig['MM'] || $parentConfig['foreign_field']) {
438  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
439  } else {
440  foreach ($directlyConnectedIds as $id) {
441  if ((int)$aForeignRecordUid === (int)$id) {
442  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
443  }
444  }
445  }
446  }
447  return $resolvedForeignRecordUids;
448  }
449 
459  protected function getLiveDefaultId($tableName, $uid)
460  {
461  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $uid);
462  if ($liveDefaultId === null) {
463  $liveDefaultId = $uid;
464  }
465  return $liveDefaultId;
466  }
467 
471  protected function getBackendUser()
472  {
473  return $GLOBALS['BE_USER'];
474  }
475 
479  protected function getLanguageService()
480  {
481  return $GLOBALS['LANG'];
482  }
483 }
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static getRecord($table, $uid, $fields= '*', $where= '', $useDeleteClause=true)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields= '*')
resolveRelatedRecords(array $result, $fieldName)
Definition: TcaInline.php:119
getWorkspacedUids(array $connectedUids, $childTableName)
Definition: TcaInline.php:392