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