TYPO3 CMS  TYPO3_8-7
FormInlineAjaxController.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
31 
36 {
44  public function createAction(ServerRequestInterface $request, ResponseInterface $response)
45  {
46  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
47  $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
48 
49  $domObjectId = $ajaxArguments[0];
50  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
51  $childChildUid = null;
52  if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
53  $childChildUid = (int)$ajaxArguments[1];
54  }
55 
56  // Parse the DOM identifier, add the levels to the structure stack
58  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
59  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
60  $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
61  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
62 
63  // Parent, this table embeds the child table
64  $parent = $inlineStackProcessor->getStructureLevel(-1);
65 
66  // Child, a record from this table should be rendered
67  $child = $inlineStackProcessor->getUnstableStructure();
68  if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
69  // If uid comes in, it is the id of the record neighbor record "create after"
70  $childVanillaUid = -1 * abs((int)$child['uid']);
71  } else {
72  // Else inline first Pid is the storage pid of new inline records
73  $childVanillaUid = (int)$inlineFirstPid;
74  }
75 
76  $childTableName = $parentConfig['foreign_table'];
77 
79  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
81  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
82  $formDataCompilerInput = [
83  'command' => 'new',
84  'tableName' => $childTableName,
85  'vanillaUid' => $childVanillaUid,
86  'isInlineChild' => true,
87  'inlineStructure' => $inlineStackProcessor->getStructure(),
88  'inlineFirstPid' => $inlineFirstPid,
89  'inlineParentUid' => $parent['uid'],
90  'inlineParentTableName' => $parent['table'],
91  'inlineParentFieldName' => $parent['field'],
92  'inlineParentConfig' => $parentConfig,
93  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
94  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
95  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
96  ];
97  if ($childChildUid) {
98  $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
99  }
100  $childData = $formDataCompiler->compile($formDataCompilerInput);
101 
102  // Set language of new child record to the language of the parent record:
103  // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
104  // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
105  // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
106  // @deprecated: IRRE 'localizationMode' is deprecated and will be removed in TYPO3 CMS 9
107  /*
108  if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
109  $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
110  $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
111  $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
112  if ($parentRecord[$parentLanguageField] > 0) {
113  $record[$childLanguageField] = $parentRecord[$parentLanguageField];
114  }
115  }
116  */
117  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
118  // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
119  // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
120  // existing record of the child-child table specified by the given uid. If there is no such id, user
121  // clicked on "created new" and a new child-child should be created, too.
122  if ($childChildUid) {
123  // Fetch existing child child
124  $childData['databaseRow'][$parentConfig['foreign_selector']] = [
125  $childChildUid,
126  ];
127  $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
128  } else {
130  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
132  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
133  $formDataCompilerInput = [
134  'command' => 'new',
135  'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
136  'vanillaUid' => (int)$inlineFirstPid,
137  'isInlineChild' => true,
138  'isInlineAjaxOpeningContext' => true,
139  'inlineStructure' => $inlineStackProcessor->getStructure(),
140  'inlineFirstPid' => (int)$inlineFirstPid,
141  ];
142  $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
143  }
144  }
145 
146  $childData['inlineParentUid'] = (int)$parent['uid'];
147  $childData['renderType'] = 'inlineRecordContainer';
148  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
149  $childResult = $nodeFactory->create($childData)->render();
150 
151  $jsonArray = [
152  'data' => '',
153  'stylesheetFiles' => [],
154  'scriptCall' => [],
155  ];
156 
157  // The HTML-object-id's prefix of the dynamically created record
158  $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
159  $objectPrefix = $objectName . '-' . $child['table'];
160  $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
161  $expandSingle = $parentConfig['appearance']['expandSingle'];
162  if (!$child['uid']) {
163  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
164  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
165  } else {
166  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
167  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
168  }
169  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
170  if ($parentConfig['appearance']['useSortable']) {
171  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
172  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
173  }
174  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
175  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
176  }
177  // Fade out and fade in the new record in the browser view to catch the user's eye
178  $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
179 
180  $response->getBody()->write(json_encode($jsonArray));
181 
182  return $response;
183  }
184 
192  public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
193  {
194  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
195 
196  $domObjectId = $ajaxArguments[0];
197  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
198  $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
199 
200  // Parse the DOM identifier, add the levels to the structure stack
202  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
203  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
204  $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
205 
206  // Parent, this table embeds the child table
207  $parent = $inlineStackProcessor->getStructureLevel(-1);
208  $parentFieldName = $parent['field'];
209 
210  // Set flag in config so that only the fields are rendered
211  // @todo: Solve differently / rename / whatever
212  $parentConfig['renderFieldsOnly'] = true;
213 
214  $parentData = [
215  'processedTca' => [
216  'columns' => [
217  $parentFieldName => [
218  'config' => $parentConfig,
219  ],
220  ],
221  ],
222  'tableName' => $parent['table'],
223  'inlineFirstPid' => $inlineFirstPid,
224  // Hand over given original return url to compile stack. Needed if inline children compile links to
225  // another view (eg. edit metadata in a nested inline situation like news with inline content element image),
226  // so the back link is still the link from the original request. See issue #82525. This is additionally
227  // given down in TcaInline data provider to compiled children data.
228  'returnUrl' => $parentConfig['originalReturnUrl'],
229  ];
230 
231  // Child, a record from this table should be rendered
232  $child = $inlineStackProcessor->getUnstableStructure();
233 
234  $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
235 
236  $childData['inlineParentUid'] = (int)$parent['uid'];
237  $childData['renderType'] = 'inlineRecordContainer';
238  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
239  $childResult = $nodeFactory->create($childData)->render();
240 
241  $jsonArray = [
242  'data' => '',
243  'stylesheetFiles' => [],
244  'scriptCall' => [],
245  ];
246 
247  // The HTML-object-id's prefix of the dynamically created record
248  $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
249  $objectId = $objectPrefix . '-' . (int)$child['uid'];
250  $expandSingle = $parentConfig['appearance']['expandSingle'];
251  $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
252  if ($parentConfig['foreign_unique']) {
253  $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
254  }
255  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
256  if ($parentConfig['appearance']['useSortable']) {
257  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
258  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
259  }
260  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
261  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
262  }
263 
264  $response->getBody()->write(json_encode($jsonArray));
265 
266  return $response;
267  }
268 
277  public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
278  {
279  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
280  $domObjectId = $ajaxArguments[0];
281  $type = $ajaxArguments[1];
282  $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
283 
285  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
286  // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
287  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
288  $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
289  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
290 
291  $jsonArray = false;
292  if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
293  // Parent, this table embeds the child table
294  $parent = $inlineStackProcessor->getStructureLevel(-1);
295  $parentFieldName = $parent['field'];
296 
297  $processedTca = $GLOBALS['TCA'][$parent['table']];
298  $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
299 
300  // Child, a record from this table should be rendered
301  $child = $inlineStackProcessor->getUnstableStructure();
302 
303  $formDataCompilerInputForParent = [
304  'vanillaUid' => (int)$parent['uid'],
305  'command' => 'edit',
306  'tableName' => $parent['table'],
307  'processedTca' => $processedTca,
308  'inlineFirstPid' => $inlineFirstPid,
309  'columnsToProcess' => [
310  $parentFieldName
311  ],
312  // @todo: still needed? NO!
313  'inlineStructure' => $inlineStackProcessor->getStructure(),
314  // Do not compile existing children, we don't need them now
315  'inlineCompileExistingChildren' => false,
316  ];
317  // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
319  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
321  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
322  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
323  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
324  $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
325  $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
326  $oldItemList = $parentData['databaseRow'][$parentFieldName];
327 
328  // DataHandler cannot handle arrays as field value
329  if (is_array($parentLanguage)) {
330  $parentLanguage = implode(',', $parentLanguage);
331  }
332 
333  $cmd = [];
334  // Localize a single child element from default language of the parent element
336  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
337  'field' => $parent['field'],
338  'language' => $parentLanguage,
339  'ids' => [$type],
340  ];
341  } else {
342  // Either localize or synchronize all child elements from default language of the parent element
343  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
344  'field' => $parent['field'],
345  'language' => $parentLanguage,
346  'action' => $type,
347  ];
348  }
349 
351  $tce = GeneralUtility::makeInstance(DataHandler::class);
352  $tce->start([], $cmd);
353  $tce->process_cmdmap();
354 
355  $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
356 
357  $jsonArray = [
358  'data' => '',
359  'stylesheetFiles' => [],
360  'scriptCall' => [],
361  ];
362  $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
363  $nameObjectForeignTable = $nameObject . '-' . $child['table'];
364 
365  $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
366  $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
367 
368  // Render error messages from DataHandler
369  $tce->printLogErrorMessages();
370  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
371  $messages = $flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush();
372  if (!empty($messages)) {
373  foreach ($messages as $message) {
374  $jsonArray['messages'][] = [
375  'title' => $message->getTitle(),
376  'message' => $message->getMessage(),
377  'severity' => $message->getSeverity()
378  ];
379  if ($message->getSeverity() === AbstractMessage::ERROR) {
380  $jsonArray['hasErrors'] = true;
381  }
382  }
383  }
384 
385  // Set the items that should be removed in the forms view:
386  $removedItems = array_diff($oldItems, $newItems);
387  foreach ($removedItems as $childUid) {
388  $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
389  }
390 
391  $localizedItems = array_diff($newItems, $oldItems);
392  foreach ($localizedItems as $childUid) {
393  $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
394 
395  $childData['inlineParentUid'] = (int)$parent['uid'];
396  $childData['renderType'] = 'inlineRecordContainer';
397  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
398  $childResult = $nodeFactory->create($childData)->render();
399 
400  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
401 
402  // Get the name of the field used as foreign selector (if any):
403  $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
404  $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
405  if (is_array($selectedValue)) {
406  $selectedValue = $selectedValue[0];
407  }
408  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
409  // Remove possible virtual records in the form which showed that a child records could be localized:
410  $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
411  if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
412  $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
413  if (is_array($transOrigPointerField)) {
414  $transOrigPointerField = $transOrigPointerField[0];
415  }
416  $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
417  }
418  }
419  // Tell JS to add new HTML of one or multiple (localize all) records to DOM
420  if (!empty($jsonArray['data'])) {
421  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
422  . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
423  . ', json.data);';
424  }
425  }
426 
427  $response->getBody()->write(json_encode($jsonArray));
428 
429  return $response;
430  }
431 
439  public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
440  {
441  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
442  $domObjectId = $ajaxArguments[0];
443 
445  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
446  // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
447  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
448  $expand = $ajaxArguments[1];
449  $collapse = $ajaxArguments[2];
450 
451  $backendUser = $this->getBackendUserAuthentication();
452  // The current table - for this table we should add/import records
453  $currentTable = $inlineStackProcessor->getUnstableStructure();
454  $currentTable = $currentTable['table'];
455  // The top parent table - this table embeds the current table
456  $top = $inlineStackProcessor->getStructureLevel(0);
457  $topTable = $top['table'];
458  $topUid = $top['uid'];
459  $inlineView = $this->getInlineExpandCollapseStateArray();
460  // Only do some action if the top record and the current record were saved before
462  $expandUids = GeneralUtility::trimExplode(',', $expand);
463  $collapseUids = GeneralUtility::trimExplode(',', $collapse);
464  // Set records to be expanded
465  foreach ($expandUids as $uid) {
466  $inlineView[$topTable][$topUid][$currentTable][] = $uid;
467  }
468  // Set records to be collapsed
469  foreach ($collapseUids as $uid) {
470  $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
471  }
472  // Save states back to database
473  if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
474  $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
475  $backendUser->uc['inlineView'] = serialize($inlineView);
476  $backendUser->writeUC();
477  }
478  }
479 
480  $response->getBody()->write(json_encode([]));
481  return $response;
482  }
483 
496  protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
497  {
498  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
499 
501  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
502  $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
503  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
504 
505  // @todo: do not use stack processor here ...
506  $child = $inlineStackProcessor->getUnstableStructure();
507  $childTableName = $child['table'];
508 
510  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
512  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
513  $formDataCompilerInput = [
514  'command' => 'edit',
515  'tableName' => $childTableName,
516  'vanillaUid' => (int)$childUid,
517  'returnUrl' => $parentData['returnUrl'],
518  'isInlineChild' => true,
519  'inlineStructure' => $inlineStructure,
520  'inlineFirstPid' => $parentData['inlineFirstPid'],
521  'inlineParentConfig' => $parentConfig,
522  'isInlineAjaxOpeningContext' => true,
523 
524  // values of the current parent element
525  // it is always a string either an id or new...
526  'inlineParentUid' => $parentData['databaseRow']['uid'],
527  'inlineParentTableName' => $parentData['tableName'],
528  'inlineParentFieldName' => $parentFieldName,
529 
530  // values of the top most parent element set on first level and not overridden on following levels
531  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
532  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
533  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
534  ];
535  // For foreign_selector with useCombination $mainChild is the mm record
536  // and $combinationChild is the child-child. For "normal" relations, $mainChild
537  // is just the normal child record and $combinationChild is empty.
538  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
539  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
540  // This kicks in if opening an existing mainChild that has a child-child set
541  $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
542  }
543  return $mainChild;
544  }
545 
555  protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
556  {
557  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
558  $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
559  // child-child table name is set in child tca "the selector field" foreign_table
560  $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
562  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
564  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
565  $formDataCompilerInput = [
566  'command' => 'edit',
567  'tableName' => $childChildTableName,
568  'vanillaUid' => (int)$childChildUid,
569  'isInlineChild' => true,
570  'isInlineAjaxOpeningContext' => true,
571  // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
572  'inlineStructure' => $inlineStructure,
573  'inlineFirstPid' => $child['inlineFirstPid'],
574  // values of the top most parent element set on first level and not overridden on following levels
575  'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
576  'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
577  'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
578  ];
579  return $formDataCompiler->compile($formDataCompilerInput);
580  }
581 
590  protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
591  {
592  $jsonResult['data'] .= $childResult['html'];
593  $jsonResult['stylesheetFiles'] = [];
594  foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
595  $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
596  }
597  if (!empty($childResult['inlineData'])) {
598  $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
599  }
600  if (!empty($childResult['additionalJavaScriptSubmit'])) {
601  $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
602  $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
603  $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
604  }
605  foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
606  $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
607  }
608  if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
609  $labels = [];
610  foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
612  $labels,
613  $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
614  );
615  }
616  $javaScriptCode = [];
617  $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
618  $javaScriptCode[] = ' TYPO3.lang = {}';
619  $javaScriptCode[] = '}';
620  $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
621  $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
622  $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
623  $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
624  $javaScriptCode[] = ' }';
625  $javaScriptCode[] = '}';
626 
627  $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
628  }
629  $requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
630  $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
631 
632  return $jsonResult;
633  }
634 
643  protected function getInlineRelatedRecordsUidArray($itemList)
644  {
645  $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
646  // Perform modification of the selected items array:
647  foreach ($itemArray as &$value) {
648  $parts = explode('|', $value, 2);
649  $value = $parts[0];
650  }
651  unset($value);
652  return $itemArray;
653  }
654 
662  protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
663  {
664  $inlineView = $this->getInlineExpandCollapseStateArray();
665  $result = [];
667  if (!empty($inlineView[$table][$uid])) {
668  $result = $inlineView[$table][$uid];
669  }
670  }
671  return $result;
672  }
673 
680  {
681  $backendUser = $this->getBackendUserAuthentication();
682  if (!$this->backendUserHasUcInlineView($backendUser)) {
683  return [];
684  }
685 
686  $inlineView = unserialize($backendUser->uc['inlineView']);
687  if (!is_array($inlineView)) {
688  $inlineView = [];
689  }
690 
691  return $inlineView;
692  }
693 
701  protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
702  {
703  return !empty($backendUser->uc['inlineView']);
704  }
705 
714  protected function removeFromArray($needle, $haystack, $strict = false)
715  {
716  $pos = array_search($needle, $haystack, $strict);
717  if ($pos !== false) {
718  unset($haystack[$pos]);
719  }
720  return $haystack;
721  }
722 
729  protected function getErrorMessageForAJAX($message)
730  {
731  return [
732  'data' => $message,
733  'scriptCall' => [
734  'alert("' . $message . '");'
735  ],
736  ];
737  }
738 
745  protected function getInlineFirstPidFromDomObjectId($domObjectId)
746  {
747  // Substitute FlexForm addition and make parsing a bit easier
748  $domObjectId = str_replace('---', ':', $domObjectId);
749  // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
750  $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
751  if (preg_match($pattern, $domObjectId, $match)) {
752  return $match[1];
753  }
754  return null;
755  }
756 
765  protected function extractSignedParentConfigFromRequest(string $contextString): array
766  {
767  if ($contextString === '') {
768  throw new \RuntimeException('Empty context string given', 1489751361);
769  }
770  $context = json_decode($contextString, true);
771  if (empty($context['config'])) {
772  throw new \RuntimeException('Empty context config section given', 1489751362);
773  }
774  if (!hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) {
775  throw new \RuntimeException('Hash does not validate', 1489751363);
776  }
777  return $context['config'];
778  }
779 
783  protected function getBackendUserAuthentication()
784  {
785  return $GLOBALS['BE_USER'];
786  }
787 }
static hmac($input, $additionalSecret='')
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)