TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
FormInlineAjaxController.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Backend\Controller;
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 
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
31 
36 {
44  public function createAction(ServerRequestInterface $request, ResponseInterface $response)
45  {
46  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
47 
48  $domObjectId = $ajaxArguments[0];
49  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
50  $childChildUid = null;
51  if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
52  $childChildUid = (int)$ajaxArguments[1];
53  }
54 
55  // Parse the DOM identifier, add the levels to the structure stack
57  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
58  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
59  $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
60  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
61 
62  // Parent, this table embeds the child table
63  $parent = $inlineStackProcessor->getStructureLevel(-1);
64  $parentFieldName = $parent['field'];
65 
66  if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
67  $command = 'edit';
68  $vanillaUid = (int)$parent['uid'];
69  $databaseRow = [
70  // TcaInlineExpandCollapseState needs the record uid
71  'uid' => (int)$parent['uid'],
72  ];
73  } else {
74  $command = 'new';
75  $databaseRow = [];
76  $vanillaUid = (int)$inlineFirstPid;
77  }
78  $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
79 
80  $formDataCompilerInputForParent = [
81  'vanillaUid' => $vanillaUid,
82  'command' => $command,
83  'tableName' => $parent['table'],
84  'databaseRow' => $databaseRow,
85  'inlineFirstPid' => $inlineFirstPid,
86  'columnsToProcess' => array_merge(
87  [$parentFieldName],
88  array_keys($databaseRow)
89  ),
90  // Do not resolve existing children, we don't need them now
91  'inlineResolveExistingChildren' => false,
92  ];
94  $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
96  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
97  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
98  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
99 
100  // Child, a record from this table should be rendered
101  $child = $inlineStackProcessor->getUnstableStructure();
102  if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
103  // If uid comes in, it is the id of the record neighbor record "create after"
104  $childVanillaUid = -1 * abs((int)$child['uid']);
105  } else {
106  // Else inline first Pid is the storage pid of new inline records
107  $childVanillaUid = (int)$inlineFirstPid;
108  }
109 
110  if ($parentConfig['type'] === 'flex') {
111  $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
112  }
113  $childTableName = $parentConfig['foreign_table'];
114 
116  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
118  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
119  $formDataCompilerInput = [
120  'command' => 'new',
121  'tableName' => $childTableName,
122  'vanillaUid' => $childVanillaUid,
123  'isInlineChild' => true,
124  'inlineStructure' => $inlineStackProcessor->getStructure(),
125  'inlineFirstPid' => $inlineFirstPid,
126  'inlineParentUid' => $parent['uid'],
127  'inlineParentTableName' => $parent['table'],
128  'inlineParentFieldName' => $parent['field'],
129  'inlineParentConfig' => $parentConfig,
130  // Fallback to $parentData is probably not needed here.
131  'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
132  'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
133  'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
134  ];
135  if ($childChildUid) {
136  $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
137  }
138  $childData = $formDataCompiler->compile($formDataCompilerInput);
139 
140  // Set language of new child record to the language of the parent record:
141  // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
142  // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
143  // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
154  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
155  // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
156  // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
157  // existing record of the child-child table specified by the given uid. If there is no such id, user
158  // clicked on "created new" and a new child-child should be created, too.
159  if ($childChildUid) {
160  // Fetch existing child child
161  $childData['databaseRow'][$parentConfig['foreign_selector']] = [
162  $childChildUid,
163  ];
164  $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
165  } else {
167  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
169  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
170  $formDataCompilerInput = [
171  'command' => 'new',
172  'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
173  'vanillaUid' => (int)$inlineFirstPid,
174  'isInlineChild' => true,
175  'isInlineAjaxOpeningContext' => true,
176  'inlineStructure' => $inlineStackProcessor->getStructure(),
177  'inlineFirstPid' => (int)$inlineFirstPid,
178  ];
179  $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
180  }
181  }
182 
183  $childData['inlineParentUid'] = (int)$parent['uid'];
184  $childData['renderType'] = 'inlineRecordContainer';
185  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
186  $childResult = $nodeFactory->create($childData)->render();
187 
188  $jsonArray = [
189  'data' => '',
190  'stylesheetFiles' => [],
191  'scriptCall' => [],
192  ];
193 
194  // The HTML-object-id's prefix of the dynamically created record
195  $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
196  $objectPrefix = $objectName . '-' . $child['table'];
197  $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
198  $expandSingle = $parentConfig['appearance']['expandSingle'];
199  if (!$child['uid']) {
200  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
201  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
202  } else {
203  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
204  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
205  }
206  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
207  if ($parentConfig['appearance']['useSortable']) {
208  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
209  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
210  }
211  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
212  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
213  }
214  // Fade out and fade in the new record in the browser view to catch the user's eye
215  $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
216 
217  $response->getBody()->write(json_encode($jsonArray));
218 
219  return $response;
220  }
221 
229  public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
230  {
231  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
232 
233  $domObjectId = $ajaxArguments[0];
234  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
235 
236  // Parse the DOM identifier, add the levels to the structure stack
238  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
239  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
240  $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
241 
242  // Parent, this table embeds the child table
243  $parent = $inlineStackProcessor->getStructureLevel(-1);
244  $parentFieldName = $parent['field'];
245 
246  $databaseRow = [
247  // TcaInlineExpandCollapseState needs this
248  'uid' => (int)$parent['uid'],
249  ];
250 
251  $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
252 
253  $formDataCompilerInputForParent = [
254  'vanillaUid' => (int)$parent['uid'],
255  'command' => 'edit',
256  'tableName' => $parent['table'],
257  'databaseRow' => $databaseRow,
258  'inlineFirstPid' => $inlineFirstPid,
259  'columnsToProcess' => array_merge(
260  [$parentFieldName],
261  array_keys($databaseRow)
262  ),
263  // @todo: still needed?
264  'inlineStructure' => $inlineStackProcessor->getStructure(),
265  // Do not resolve existing children, we don't need them now
266  'inlineResolveExistingChildren' => false,
267  ];
269  $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
271  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
272  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
273  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
274 
275  if ($parentConfig['type'] === 'flex') {
276  $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
277  $parentData['processedTca']['columns'][$parentFieldName]['config'] = $parentConfig;
278  }
279 
280  // Set flag in config so that only the fields are rendered
281  // @todo: Solve differently / rename / whatever
282  $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
283 
284  // Child, a record from this table should be rendered
285  $child = $inlineStackProcessor->getUnstableStructure();
286 
287  $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
288 
289  $childData['inlineParentUid'] = (int)$parent['uid'];
290  $childData['renderType'] = 'inlineRecordContainer';
291  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
292  $childResult = $nodeFactory->create($childData)->render();
293 
294  $jsonArray = [
295  'data' => '',
296  'stylesheetFiles' => [],
297  'scriptCall' => [],
298  ];
299 
300  // The HTML-object-id's prefix of the dynamically created record
301  $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
302  $objectId = $objectPrefix . '-' . (int)$child['uid'];
303  $expandSingle = $parentConfig['appearance']['expandSingle'];
304  $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
305  if ($parentConfig['foreign_unique']) {
306  $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
307  }
308  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
309  if ($parentConfig['appearance']['useSortable']) {
310  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
311  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
312  }
313  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
314  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
315  }
316 
317  $response->getBody()->write(json_encode($jsonArray));
318 
319  return $response;
320  }
321 
330  public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
331  {
332  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
333  $domObjectId = $ajaxArguments[0];
334  $type = $ajaxArguments[1];
335 
337  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
338  // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
339  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
340  $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
341  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
342 
343  $jsonArray = false;
344  if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
345  // Parent, this table embeds the child table
346  $parent = $inlineStackProcessor->getStructureLevel(-1);
347  $parentFieldName = $parent['field'];
348 
349  // Child, a record from this table should be rendered
350  $child = $inlineStackProcessor->getUnstableStructure();
351 
352  $formDataCompilerInputForParent = [
353  'vanillaUid' => (int)$parent['uid'],
354  'command' => 'edit',
355  'tableName' => $parent['table'],
356  'databaseRow' => [
357  // TcaInlineExpandCollapseState needs this
358  'uid' => (int)$parent['uid'],
359  ],
360  'inlineFirstPid' => $inlineFirstPid,
361  'columnsToProcess' => [
362  $parentFieldName
363  ],
364  // @todo: still needed? NO!
365  'inlineStructure' => $inlineStackProcessor->getStructure(),
366  // Do not compile existing children, we don't need them now
367  'inlineCompileExistingChildren' => false,
368  ];
369  // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
371  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
373  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
374  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
375  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
376  $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
377  $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
378  $oldItemList = $parentData['databaseRow'][$parentFieldName];
379 
380  // DataHandler cannot handle arrays as field value
381  if (is_array($parentLanguage)) {
382  $parentLanguage = implode(',', $parentLanguage);
383  }
384 
385  $cmd = [];
386  // Localize a single child element from default language of the parent element
388  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
389  'field' => $parent['field'],
390  'language' => $parentLanguage,
391  'ids' => [$type],
392  ];
393  // Either localize or synchronize all child elements from default language of the parent element
394  } else {
395  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
396  'field' => $parent['field'],
397  'language' => $parentLanguage,
398  'action' => $type,
399  ];
400  }
401 
403  $tce = GeneralUtility::makeInstance(DataHandler::class);
404  $tce->start([], $cmd);
405  $tce->process_cmdmap();
406 
407  $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
408 
409  $jsonArray = [
410  'data' => '',
411  'stylesheetFiles' => [],
412  'scriptCall' => [],
413  ];
414  $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
415  $nameObjectForeignTable = $nameObject . '-' . $child['table'];
416 
417  $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
418  $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
419 
420  // Set the items that should be removed in the forms view:
421  $removedItems = array_diff($oldItems, $newItems);
422  foreach ($removedItems as $childUid) {
423  $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
424  }
425 
426  $localizedItems = array_diff($newItems, $oldItems);
427  foreach ($localizedItems as $childUid) {
428  $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
429 
430  $childData['inlineParentUid'] = (int)$parent['uid'];
431  $childData['renderType'] = 'inlineRecordContainer';
432  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
433  $childResult = $nodeFactory->create($childData)->render();
434 
435  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
436 
437  // Get the name of the field used as foreign selector (if any):
438  $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
439  $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
440  if (is_array($selectedValue)) {
441  $selectedValue = $selectedValue[0];
442  }
443  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
444  // Remove possible virtual records in the form which showed that a child records could be localized:
445  $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
446  if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
447  $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
448  if (is_array($transOrigPointerField)) {
449  $transOrigPointerField = $transOrigPointerField[0];
450  }
451  $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
452  }
453  }
454  // Tell JS to add new HTML of one or multiple (localize all) records to DOM
455  if (!empty($jsonArray['data'])) {
456  array_push(
457  $jsonArray['scriptCall'],
458  'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
459  . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
460  . ', json.data);'
461  );
462  }
463  }
464 
465  $response->getBody()->write(json_encode($jsonArray));
466 
467  return $response;
468  }
469 
477  public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
478  {
479  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
480  $domObjectId = $ajaxArguments[0];
481 
483  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
484  // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
485  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
486  $expand = $ajaxArguments[1];
487  $collapse = $ajaxArguments[2];
488 
489  $backendUser = $this->getBackendUserAuthentication();
490  // The current table - for this table we should add/import records
491  $currentTable = $inlineStackProcessor->getUnstableStructure();
492  $currentTable = $currentTable['table'];
493  // The top parent table - this table embeds the current table
494  $top = $inlineStackProcessor->getStructureLevel(0);
495  $topTable = $top['table'];
496  $topUid = $top['uid'];
497  $inlineView = $this->getInlineExpandCollapseStateArray();
498  // Only do some action if the top record and the current record were saved before
500  $expandUids = GeneralUtility::trimExplode(',', $expand);
501  $collapseUids = GeneralUtility::trimExplode(',', $collapse);
502  // Set records to be expanded
503  foreach ($expandUids as $uid) {
504  $inlineView[$topTable][$topUid][$currentTable][] = $uid;
505  }
506  // Set records to be collapsed
507  foreach ($collapseUids as $uid) {
508  $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
509  }
510  // Save states back to database
511  if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
512  $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
513  $backendUser->uc['inlineView'] = serialize($inlineView);
514  $backendUser->writeUC();
515  }
516  }
517 
518  $response->getBody()->write(json_encode([]));
519  return $response;
520  }
521 
534  protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
535  {
536  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
537 
539  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
540  $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
541  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
542 
543  // @todo: do not use stack processor here ...
544  $child = $inlineStackProcessor->getUnstableStructure();
545  $childTableName = $child['table'];
546 
548  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
550  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
551  $formDataCompilerInput = [
552  'command' => 'edit',
553  'tableName' => $childTableName,
554  'vanillaUid' => (int)$childUid,
555  'isInlineChild' => true,
556  'inlineStructure' => $inlineStructure,
557  'inlineFirstPid' => $parentData['inlineFirstPid'],
558  'inlineParentConfig' => $parentConfig,
559  'isInlineAjaxOpeningContext' => true,
560 
561  // values of the current parent element
562  // it is always a string either an id or new...
563  'inlineParentUid' => $parentData['databaseRow']['uid'],
564  'inlineParentTableName' => $parentData['tableName'],
565  'inlineParentFieldName' => $parentFieldName,
566 
567  // values of the top most parent element set on first level and not overridden on following levels
568  'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
569  'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
570  'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
571  ];
572  // For foreign_selector with useCombination $mainChild is the mm record
573  // and $combinationChild is the child-child. For "normal" relations, $mainChild
574  // is just the normal child record and $combinationChild is empty.
575  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
576  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
577  // This kicks in if opening an existing mainChild that has a child-child set
578  $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
579  }
580  return $mainChild;
581  }
582 
592  protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
593  {
594  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
595  $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
596  // child-child table name is set in child tca "the selector field" foreign_table
597  $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
599  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
601  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
602  $formDataCompilerInput = [
603  'command' => 'edit',
604  'tableName' => $childChildTableName,
605  'vanillaUid' => (int)$childChildUid,
606  'isInlineChild' => true,
607  'isInlineAjaxOpeningContext' => true,
608  // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
609  'inlineStructure' => $inlineStructure,
610  'inlineFirstPid' => $child['inlineFirstPid'],
611  // values of the top most parent element set on first level and not overridden on following levels
612  'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
613  'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
614  'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
615  ];
616  return $formDataCompiler->compile($formDataCompilerInput);
617  }
618 
627  protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
628  {
629  $jsonResult['data'] .= $childResult['html'];
630  $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
631  if (!empty($childResult['inlineData'])) {
632  $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
633  }
634  if (!empty($childResult['additionalJavaScriptSubmit'])) {
635  $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
636  $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
637  $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
638  }
639  foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
640  $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
641  }
642  $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
643  if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
644  $labels = [];
645  foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
647  $labels,
648  $this->addInlineLanguageLabelFile($additionalInlineLanguageLabelFile)
649  );
650  }
651  $javaScriptCode = [];
652  $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
653  $javaScriptCode[] = ' TYPO3.lang = {}';
654  $javaScriptCode[] = '}';
655  $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
656  $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
657  $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
658  $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
659  $javaScriptCode[] = ' }';
660  $javaScriptCode[] = '}';
661 
662  $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
663  }
664  if (!empty($childResult['requireJsModules'])) {
665  foreach ($childResult['requireJsModules'] as $module) {
666  $moduleName = null;
667  $callback = null;
668  if (is_string($module)) {
669  // if $module is a string, no callback
670  $moduleName = $module;
671  $callback = null;
672  } elseif (is_array($module)) {
673  // if $module is an array, callback is possible
674  foreach ($module as $key => $value) {
675  $moduleName = $key;
676  $callback = $value;
677  break;
678  }
679  }
680  if ($moduleName !== null) {
681  $inlineCodeKey = $moduleName;
682  $javaScriptCode = 'require(["' . $moduleName . '"]';
683  if ($callback !== null) {
684  $inlineCodeKey .= sha1($callback);
685  $javaScriptCode .= ', ' . $callback;
686  }
687  $javaScriptCode .= ');';
688  $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
689  }
690  }
691  }
692  return $jsonResult;
693  }
694 
700  protected function addInlineLanguageLabelFile($file)
701  {
703  $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
704  $language = $GLOBALS['LANG']->lang;
705  $localizationArray = $languageFactory->getParsedData(
706  $file,
707  $language,
708  'utf-8',
709  1
710  );
711  if (is_array($localizationArray) && !empty($localizationArray)) {
712  if (!empty($localizationArray[$language])) {
713  $xlfLabelArray = $localizationArray['default'];
714  ArrayUtility::mergeRecursiveWithOverrule($xlfLabelArray, $localizationArray[$language], true, false);
715  } else {
716  $xlfLabelArray = $localizationArray['default'];
717  }
718  } else {
719  $xlfLabelArray = [];
720  }
721  $labelArray = [];
722  foreach ($xlfLabelArray as $key => $value) {
723  if (isset($value[0]['target'])) {
724  $labelArray[$key] = $value[0]['target'];
725  } else {
726  $labelArray[$key] = '';
727  }
728  }
729  return $labelArray;
730  }
731 
740  protected function getInlineRelatedRecordsUidArray($itemList)
741  {
742  $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
743  // Perform modification of the selected items array:
744  foreach ($itemArray as &$value) {
745  $parts = explode('|', $value, 2);
746  $value = $parts[0];
747  }
748  unset($value);
749  return $itemArray;
750  }
751 
760  protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
761  {
762  if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
763  $allowedFileExtensions = GeneralUtility::trimExplode(
764  ',',
765  $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
766  true
767  );
768  if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
769  return false;
770  }
771  }
772  return true;
773  }
774 
782  protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
783  {
784  $inlineView = $this->getInlineExpandCollapseStateArray();
785  $result = [];
787  if (!empty($inlineView[$table][$uid])) {
788  $result = $inlineView[$table][$uid];
789  }
790  }
791  return $result;
792  }
793 
800  {
801  $backendUser = $this->getBackendUserAuthentication();
802  if (!$this->backendUserHasUcInlineView($backendUser)) {
803  return [];
804  }
805 
806  $inlineView = unserialize($backendUser->uc['inlineView']);
807  if (!is_array($inlineView)) {
808  $inlineView = [];
809  }
810 
811  return $inlineView;
812  }
813 
821  protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
822  {
823  return !empty($backendUser->uc['inlineView']);
824  }
825 
834  protected function removeFromArray($needle, $haystack, $strict = false)
835  {
836  $pos = array_search($needle, $haystack, $strict);
837  if ($pos !== false) {
838  unset($haystack[$pos]);
839  }
840  return $haystack;
841  }
842 
849  protected function getErrorMessageForAJAX($message)
850  {
851  return [
852  'data' => $message,
853  'scriptCall' => [
854  'alert("' . $message . '");'
855  ],
856  ];
857  }
858 
865  protected function getInlineFirstPidFromDomObjectId($domObjectId)
866  {
867  // Substitute FlexForm addition and make parsing a bit easier
868  $domObjectId = str_replace('---', ':', $domObjectId);
869  // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
870  $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
871  if (preg_match($pattern, $domObjectId, $match)) {
872  return $match[1];
873  }
874  return null;
875  }
876 
880  protected function getBackendUserAuthentication()
881  {
882  return $GLOBALS['BE_USER'];
883  }
884 
895  protected function getParentConfigFromFlexForm(array $parentConfig, string $domObjectId) : array
896  {
897  list($flexFormPath, $foreignTableName) = $this->splitDomObjectId($domObjectId);
898 
899  $childConfig = $parentConfig['ds']['sheets'];
900  $flexFormPath = explode(':', $flexFormPath);
901  foreach ($flexFormPath as $flexFormNode) {
902  // We are dealing with configuration information from a flexform,
903  // not value storage, identifiers that reference language or
904  // value nodes must be skipped.
905  if (!isset($childConfig[$flexFormNode]) && preg_match('/^[lv][[:alpha:]]+$/', $flexFormNode)) {
906  continue;
907  }
908  $childConfig = $childConfig[$flexFormNode];
909 
910  // Skip to the field configuration of a sheet
911  if (isset($childConfig['ROOT']) && $childConfig['ROOT']['type'] == 'array') {
912  $childConfig = $childConfig['ROOT']['el'];
913  }
914  }
915 
916  if (!isset($childConfig['config'])
917  || !is_array($childConfig['config'])
918  || $childConfig['config']['type'] !== 'inline'
919  || $childConfig['config']['foreign_table'] !== $foreignTableName
920  ) {
921  throw new \UnexpectedValueException(
922  'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
923  1446996319
924  );
925  }
926  return $childConfig['config'];
927  }
928 
939  protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
940  {
941  if (!isset($ajaxArguments['context'])) {
942  return $databaseRow;
943  }
944 
945  $context = json_decode($ajaxArguments['context'], true);
946  if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
947  return $databaseRow;
948  }
949 
950  if (isset($context['config']['flexDataStructurePointers'])
951  && is_array($context['config']['flexDataStructurePointers'])
952  ) {
953  $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);
954  }
955 
956  return $databaseRow;
957  }
958 
966  protected function splitDomObjectId(string $domObjectId) : array
967  {
968 
969  // Substitute FlexForm addition and make parsing a bit easier
970  $domObjectId = str_replace('---', ':', $domObjectId);
971  $pattern = '/:data:(?<flexformPath>.*?)-(?<tableName>[^-]+)(?:-(?:NEW)?\w+)?$/';
972 
973  /* EXPLANATION for the regex:
974  * according https://regex101.com/
975  *
976  * :data: matches the characters :data: literally (case sensitive)
977  * (?<flexformPath>.*?) Named capturing group flexformPath
978  * .*? matches any character (except newline)
979  * Quantifier: *? Between zero and unlimited times, as few times as possible, expanding as needed [lazy]
980  * - matches the character - literally
981  * (?<tableName>[^-]+) Named capturing group tableName
982  * [^-]+ match a single character not present in the list below
983  * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
984  * - the literal character -
985  * (?:-(?:NEW)?\w+)? Non-capturing group
986  * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
987  * - matches the character - literally
988  * (?:NEW)? Non-capturing group
989  * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
990  * NEW matches the characters NEW literally (case sensitive)
991  * \w+ match any word character [a-zA-Z0-9_]
992  * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
993  * $ assert position at end of a line
994  */
995 
996  if (preg_match($pattern, $domObjectId, $match)) {
997  return [$match['flexformPath'], $match['tableName']];
998  }
999 
1000  return [];
1001  }
1002 }
backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
getParentConfigFromFlexForm(array $parentConfig, string $domObjectId)
mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
static hmac($input, $additionalSecret= '')