TYPO3 CMS  TYPO3_8-7
TcaInline.php
Go to the documentation of this file.
1 <?php
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 
33 
38 {
45  public function addData(array $result)
46  {
47  $result = $this->addInlineFirstPid($result);
48 
49  foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
50  if (!$this->isInlineField($fieldConfig)) {
51  continue;
52  }
53  $result['processedTca']['columns'][$fieldName]['children'] = [];
54  if (!$this->isUserAllowedToModify($fieldConfig)) {
55  continue;
56  }
57  if ($result['inlineResolveExistingChildren']) {
58  $result = $this->resolveRelatedRecords($result, $fieldName);
59  $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
60  }
61  }
62 
63  return $result;
64  }
65 
72  protected function isInlineField($fieldConfig)
73  {
74  return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline';
75  }
76 
83  protected function isUserAllowedToModify($fieldConfig)
84  {
85  return $this->getBackendUser()->check('tables_modify', $fieldConfig['config']['foreign_table']);
86  }
87 
96  protected function addInlineFirstPid(array $result)
97  {
98  if (is_null($result['inlineFirstPid'])) {
99  $table = $result['tableName'];
100  $row = $result['databaseRow'];
101  // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
102  if ($table === 'pages') {
103  $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
104  $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
105  } elseif ($row['pid'] < 0) {
106  $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
107  $pid = $prevRec['pid'];
108  } else {
109  $pid = $row['pid'];
110  }
111  $result['inlineFirstPid'] = (int)$pid;
112  }
113  return $result;
114  }
115 
124  protected function resolveRelatedRecords(array $result, $fieldName)
125  {
126  $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
127 
128  // localizationMode is either "none", "keep" or "select":
129  // * none: Handled parent row is not a localized record, or if it is a localized row, this is ignored.
130  // Default language records and overlays have distinct children that are not connected to each other.
131  // * keep: Handled parent row is a localized record, but child table is either not localizable, or
132  // "keep" is explicitly set. A localized parent and its default language row share the same
133  // children records. Editing a child from a localized record will change this record for the
134  // default language row, too.
135  // * select: Handled parent row is a localized record, child table is localizable. Children records are
136  // localized overlays of a default language record. Three scenarios can happen:
137  // ** Localized child overlay and its default language row exist - show localized overlay record
138  // ** Default child language row exists but child overlay doesn't - show a "synchronize this record" button
139  // ** Localized child overlay exists but default language row does not - this dangling child is a data inconsistency
140 
141  // Mode was prepared by TcaInlineConfiguration provider
142  // @deprecated: IRRE 'localizationMode' is deprecated and will be removed in TYPO3 CMS 9
143  $mode = $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'];
144  if ($mode === 'none') {
145  $connectedUids = [];
146  // A new record that has distinct children can not have children yet, fetch connected uids for existing only
147  if ($result['command'] === 'edit') {
148  $connectedUids = $this->resolveConnectedRecordUids(
149  $result['processedTca']['columns'][$fieldName]['config'],
150  $result['tableName'],
151  $result['databaseRow']['uid'],
152  $result['databaseRow'][$fieldName]
153  );
154  }
155  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
156  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
157  // @todo: If inlineCompileExistingChildren must be kept, it might be better to change the data
158  // @todo: format of databaseRow for this field and separate the child compilation to an own provider?
159  if ($result['inlineCompileExistingChildren']) {
160  foreach ($connectedUids as $childUid) {
161  try {
162  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
163  } catch (DatabaseRecordException $e) {
164  // The child could not be compiled, probably it was deleted and a dangling mm record exists
165  $this->getLogger()->warning(
166  $e->getMessage(),
167  [
168  'table' => $childTableName,
169  'uid' => $childUid,
170  'exception' => $e
171  ]
172  );
173  continue;
174  }
175  }
176  }
177  } elseif ($mode === 'keep') {
178  // Fetch connected uids of default language record
179  $connectedUids = $this->resolveConnectedRecordUids(
180  $result['processedTca']['columns'][$fieldName]['config'],
181  $result['tableName'],
182  $result['defaultLanguageRow']['uid'],
183  $result['defaultLanguageRow'][$fieldName]
184  );
185  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
186  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
187  if ($result['inlineCompileExistingChildren']) {
188  foreach ($connectedUids as $childUid) {
189  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
190  }
191  }
192  } else {
193  $connectedUidsOfLocalizedOverlay = [];
194  if ($result['command'] === 'edit') {
195  $connectedUidsOfLocalizedOverlay = $this->resolveConnectedRecordUids(
196  $result['processedTca']['columns'][$fieldName]['config'],
197  $result['tableName'],
198  $result['databaseRow']['uid'],
199  $result['databaseRow'][$fieldName]
200  );
201  }
202  $result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfLocalizedOverlay);
203  if ($result['inlineCompileExistingChildren']) {
204  $tableNameWithDefaultRecords = $result['tableName'];
205  if ($tableNameWithDefaultRecords === 'pages_language_overlay') {
206  $tableNameWithDefaultRecords = 'pages';
207  }
208  $connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
209  $result['processedTca']['columns'][$fieldName]['config'],
210  $tableNameWithDefaultRecords,
211  $result['defaultLanguageRow']['uid'],
212  $result['defaultLanguageRow'][$fieldName]
213  );
214 
215  $showPossible = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'];
216 
217  // Find which records are localized, which records are not localized and which are
218  // localized but miss default language record
219  $fieldNameWithDefaultLanguageUid = $GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField'];
220  foreach ($connectedUidsOfLocalizedOverlay as $localizedUid) {
221  try {
222  $localizedRecord = $this->getRecordFromDatabase($childTableName, $localizedUid);
223  } catch (DatabaseRecordException $e) {
224  // The child could not be compiled, probably it was deleted and a dangling mm record exists
225  $this->getLogger()->warning(
226  $e->getMessage(),
227  [
228  'table' => $childTableName,
229  'uid' => $localizedUid,
230  'exception' => $e
231  ]
232  );
233  continue;
234  }
235  $uidOfDefaultLanguageRecord = $localizedRecord[$fieldNameWithDefaultLanguageUid];
236  if (in_array($uidOfDefaultLanguageRecord, $connectedUidsOfDefaultLanguageRecord)) {
237  // This localized child has a default language record. Remove this record from list of default language records
238  $connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
239  }
240  // Compile localized record
241  try {
242  $compiledChild = $this->compileChild($result, $fieldName, $localizedUid);
243  } catch (DatabaseRecordException $e) {
244  // The child could not be compiled, probably it was deleted and a dangling mm record exists
245  $this->getLogger()->warning(
246  $e->getMessage(),
247  [
248  'table' => $childTableName,
249  'uid' => $localizedUid,
250  'exception' => $e
251  ]
252  );
253  continue;
254  }
255  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
256  }
257  if ($showPossible) {
258  foreach ($connectedUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
259  // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
260  // exist in default language, but are not localized yet. Compile and mark those
261  try {
262  $compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid);
263  } catch (DatabaseRecordException $e) {
264  // The child could not be compiled, probably it was deleted and a dangling mm record exists
265  $this->getLogger()->warning(
266  $e->getMessage(),
267  [
268  'table' => $childTableName,
269  'uid' => $defaultLanguageUid,
270  'exception' => $e
271  ]
272  );
273  continue;
274  }
275  $compiledChild['isInlineDefaultLanguageRecordInLocalizedParentContext'] = true;
276  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
277  }
278  }
279  }
280  }
281 
282  return $result;
283  }
284 
294  protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName)
295  {
296  if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
297  return $result;
298  }
299 
300  $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
301  $foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
302  $selectorOrUniquePossibleRecords = [];
303 
304  if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
305  // Compile child table data for this field only
306  $selectDataInput = [
307  'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
308  'command' => 'new',
309  // Since there is no existing record that may have a type, it does not make sense to
310  // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
311  'pageTsConfig' => $result['pageTsConfig'],
312  'userTsConfig' => $result['userTsConfig'],
313  'databaseRow' => $result['databaseRow'],
314  'processedTca' => [
315  'ctrl' => [],
316  'columns' => [
317  $foreignFieldName => [
318  'config' => $selectorOrUniqueConfiguration['config'],
319  ],
320  ],
321  ],
322  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
323  ];
325  $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
326  $formDataGroup->setProviderList([ TcaSelectItems::class ]);
328  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
329  $compilerResult = $formDataCompiler->compile($selectDataInput);
330  $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
331  }
332 
333  $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
334 
335  return $result;
336  }
337 
346  protected function compileChild(array $result, $parentFieldName, $childUid)
347  {
348  $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
349  $childTableName = $parentConfig['foreign_table'];
350 
352  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
353  $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
354  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
355 
357  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
359  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
360  $formDataCompilerInput = [
361  'command' => 'edit',
362  'tableName' => $childTableName,
363  'vanillaUid' => (int)$childUid,
364  // Give incoming returnUrl down to children so they generate a returnUrl back to
365  // the originally opening record, also see "originalReturnUrl" in inline container
366  // and FormInlineAjaxController
367  'returnUrl' => $result['returnUrl'],
368  'isInlineChild' => true,
369  'inlineStructure' => $result['inlineStructure'],
370  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
371  'inlineFirstPid' => $result['inlineFirstPid'],
372  'inlineParentConfig' => $parentConfig,
373 
374  // values of the current parent element
375  // it is always a string either an id or new...
376  'inlineParentUid' => $result['databaseRow']['uid'],
377  'inlineParentTableName' => $result['tableName'],
378  'inlineParentFieldName' => $parentFieldName,
379 
380  // values of the top most parent element set on first level and not overridden on following levels
381  'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
382  'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
383  'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
384  ];
385 
386  // For foreign_selector with useCombination $mainChild is the mm record
387  // and $combinationChild is the child-child. For 1:n "normal" relations,
388  // $mainChild is just the normal child record and $combinationChild is empty.
389  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
390  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
391  try {
392  $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig);
393  } catch (DatabaseRecordException $e) {
394  // The child could not be compiled, probably it was deleted and a dangling mm record
395  // exists. This is a data inconsistency, we catch this exception and create a flash message
396  $message = vsprintf(
397  $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:formEngine.databaseRecordErrorInlineChildChild'),
398  [ $e->getTableName(), $e->getUid(), $childTableName, (int)$childUid ]
399  );
400  $flashMessage = GeneralUtility::makeInstance(
401  FlashMessage::class,
402  $message,
403  '',
405  );
406  GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->enqueue($flashMessage);
407  }
408  }
409  return $mainChild;
410  }
411 
420  protected function compileChildChild(array $child, array $parentConfig)
421  {
422  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
423  $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
424  // child-child table name is set in child tca "the selector field" foreign_table
425  $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
427  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
429  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
430 
431  $formDataCompilerInput = [
432  'command' => 'edit',
433  'tableName' => $childChildTableName,
434  'vanillaUid' => (int)$childChildUid,
435  'isInlineChild' => true,
436  'isInlineChildExpanded' => $child['isInlineChildExpanded'],
437  // @todo: this is the wrong inline structure, isn't it? Shouldn't it contain the part from child child, too?
438  'inlineStructure' => $child['inlineStructure'],
439  'inlineFirstPid' => $child['inlineFirstPid'],
440  // values of the top most parent element set on first level and not overridden on following levels
441  'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
442  'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
443  'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
444  ];
445  $childChild = $formDataCompiler->compile($formDataCompilerInput);
446  return $childChild;
447  }
448 
456  protected function getWorkspacedUids(array $connectedUids, $childTableName)
457  {
458  $backendUser = $this->getBackendUser();
459  $newConnectedUids = [];
460  foreach ($connectedUids as $uid) {
461  // Fetch workspace version of a record (if any):
462  // @todo: Needs handling
463  if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($childTableName)) {
464  $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state');
465  if (!empty($workspaceVersion)) {
466  $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
467  if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
468  continue;
469  }
470  $uid = $workspaceVersion['uid'];
471  }
472  }
473  $newConnectedUids[] = $uid;
474  }
475  return $newConnectedUids;
476  }
477 
488  protected function resolveConnectedRecordUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue)
489  {
490  $directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue);
491  if (empty($parentConfig['MM'])) {
492  $parentUid = $this->getLiveDefaultId($parentTableName, $parentUid);
493  }
495  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
496  $relationHandler->registerNonTableValues = (bool)$parentConfig['allowedIdValues'];
497  $relationHandler->start($parentFieldValue, $parentConfig['foreign_table'], $parentConfig['MM'], $parentUid, $parentTableName, $parentConfig);
498  $foreignRecordUids = $relationHandler->getValueArray();
499  $resolvedForeignRecordUids = [];
500  foreach ($foreignRecordUids as $aForeignRecordUid) {
501  if ($parentConfig['MM'] || $parentConfig['foreign_field']) {
502  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
503  } else {
504  foreach ($directlyConnectedIds as $id) {
505  if ((int)$aForeignRecordUid === (int)$id) {
506  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
507  }
508  }
509  }
510  }
511  return $resolvedForeignRecordUids;
512  }
513 
523  protected function getLiveDefaultId($tableName, $uid)
524  {
525  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $uid);
526  if ($liveDefaultId === null) {
527  $liveDefaultId = $uid;
528  }
529  return $liveDefaultId;
530  }
531 
535  protected function getBackendUser()
536  {
537  return $GLOBALS['BE_USER'];
538  }
539 
543  protected function getLanguageService()
544  {
545  return $GLOBALS['LANG'];
546  }
547 
551  protected function getLogger()
552  {
553  return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
554  }
555 }
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
resolveRelatedRecords(array $result, $fieldName)
Definition: TcaInline.php:124
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
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']
getWorkspacedUids(array $connectedUids, $childTableName)
Definition: TcaInline.php:456