72204045b52042b6e7533a65b70c0e0fb7de1b5b
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / FormInlineAjaxController.php
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;
20 use TYPO3\CMS\Backend\Form\FormDataCompiler;
21 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
22 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
23 use TYPO3\CMS\Backend\Form\NodeFactory;
24 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
25 use TYPO3\CMS\Core\DataHandling\DataHandler;
26 use TYPO3\CMS\Core\Utility\ArrayUtility;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Core\Utility\MathUtility;
29
30 /**
31 * Handle FormEngine inline ajax calls
32 */
33 class FormInlineAjaxController extends AbstractFormEngineAjaxController
34 {
35 /**
36 * Create a new inline child via AJAX.
37 *
38 * @param ServerRequestInterface $request
39 * @param ResponseInterface $response
40 * @return ResponseInterface
41 */
42 public function createAction(ServerRequestInterface $request, ResponseInterface $response)
43 {
44 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
45 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
46
47 $domObjectId = $ajaxArguments[0];
48 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
49 $childChildUid = null;
50 if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
51 $childChildUid = (int)$ajaxArguments[1];
52 }
53
54 // Parse the DOM identifier, add the levels to the structure stack
55 /** @var InlineStackProcessor $inlineStackProcessor */
56 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
57 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
58 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
59 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
60
61 // Parent, this table embeds the child table
62 $parent = $inlineStackProcessor->getStructureLevel(-1);
63
64 // Child, a record from this table should be rendered
65 $child = $inlineStackProcessor->getUnstableStructure();
66 if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
67 // If uid comes in, it is the id of the record neighbor record "create after"
68 $childVanillaUid = -1 * abs((int)$child['uid']);
69 } else {
70 // Else inline first Pid is the storage pid of new inline records
71 $childVanillaUid = (int)$inlineFirstPid;
72 }
73
74 $childTableName = $parentConfig['foreign_table'];
75
76 /** @var TcaDatabaseRecord $formDataGroup */
77 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
78 /** @var FormDataCompiler $formDataCompiler */
79 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
80 $formDataCompilerInput = [
81 'command' => 'new',
82 'tableName' => $childTableName,
83 'vanillaUid' => $childVanillaUid,
84 'isInlineChild' => true,
85 'inlineStructure' => $inlineStackProcessor->getStructure(),
86 'inlineFirstPid' => $inlineFirstPid,
87 'inlineParentUid' => $parent['uid'],
88 'inlineParentTableName' => $parent['table'],
89 'inlineParentFieldName' => $parent['field'],
90 'inlineParentConfig' => $parentConfig,
91 'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
92 'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
93 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
94 ];
95 if ($childChildUid) {
96 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
97 }
98 $childData = $formDataCompiler->compile($formDataCompilerInput);
99
100 // Set language of new child record to the language of the parent record:
101 // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
102 // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
103 // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
104 // @deprecated: IRRE 'localizationMode' is deprecated and will be removed in TYPO3 CMS 9
105 /*
106 if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
107 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
108 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
109 $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
110 if ($parentRecord[$parentLanguageField] > 0) {
111 $record[$childLanguageField] = $parentRecord[$parentLanguageField];
112 }
113 }
114 */
115 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
116 // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
117 // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
118 // existing record of the child-child table specified by the given uid. If there is no such id, user
119 // clicked on "created new" and a new child-child should be created, too.
120 if ($childChildUid) {
121 // Fetch existing child child
122 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
123 $childChildUid,
124 ];
125 $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
126 } else {
127 /** @var TcaDatabaseRecord $formDataGroup */
128 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
129 /** @var FormDataCompiler $formDataCompiler */
130 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
131 $formDataCompilerInput = [
132 'command' => 'new',
133 'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
134 'vanillaUid' => (int)$inlineFirstPid,
135 'isInlineChild' => true,
136 'isInlineAjaxOpeningContext' => true,
137 'inlineStructure' => $inlineStackProcessor->getStructure(),
138 'inlineFirstPid' => (int)$inlineFirstPid,
139 ];
140 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
141 }
142 }
143
144 $childData['inlineParentUid'] = (int)$parent['uid'];
145 $childData['renderType'] = 'inlineRecordContainer';
146 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
147 $childResult = $nodeFactory->create($childData)->render();
148
149 $jsonArray = [
150 'data' => '',
151 'stylesheetFiles' => [],
152 'scriptCall' => [],
153 ];
154
155 // The HTML-object-id's prefix of the dynamically created record
156 $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
157 $objectPrefix = $objectName . '-' . $child['table'];
158 $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
159 $expandSingle = $parentConfig['appearance']['expandSingle'];
160 if (!$child['uid']) {
161 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
162 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
163 } else {
164 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
165 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
166 }
167 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
168 if ($parentConfig['appearance']['useSortable']) {
169 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
170 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
171 }
172 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
173 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
174 }
175 // Fade out and fade in the new record in the browser view to catch the user's eye
176 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
177
178 $response->getBody()->write(json_encode($jsonArray));
179
180 return $response;
181 }
182
183 /**
184 * Show the details of a child record.
185 *
186 * @param ServerRequestInterface $request
187 * @param ResponseInterface $response
188 * @return ResponseInterface
189 */
190 public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
191 {
192 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
193
194 $domObjectId = $ajaxArguments[0];
195 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
196 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
197
198 // Parse the DOM identifier, add the levels to the structure stack
199 /** @var InlineStackProcessor $inlineStackProcessor */
200 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
201 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
202 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
203
204 // Parent, this table embeds the child table
205 $parent = $inlineStackProcessor->getStructureLevel(-1);
206 $parentFieldName = $parent['field'];
207
208 // Set flag in config so that only the fields are rendered
209 // @todo: Solve differently / rename / whatever
210 $parentConfig['renderFieldsOnly'] = true;
211
212 $parentData = [
213 'processedTca' => [
214 'columns' => [
215 $parentFieldName => [
216 'config' => $parentConfig,
217 ],
218 ],
219 ],
220 'databaseRow' => [
221 'uid' => (int)$parent['uid'],
222 ],
223 'tableName' => $parent['table'],
224 'inlineFirstPid' => $inlineFirstPid,
225 ];
226
227 // Child, a record from this table should be rendered
228 $child = $inlineStackProcessor->getUnstableStructure();
229
230 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
231
232 $childData['inlineParentUid'] = (int)$parent['uid'];
233 $childData['renderType'] = 'inlineRecordContainer';
234 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
235 $childResult = $nodeFactory->create($childData)->render();
236
237 $jsonArray = [
238 'data' => '',
239 'stylesheetFiles' => [],
240 'scriptCall' => [],
241 ];
242
243 // The HTML-object-id's prefix of the dynamically created record
244 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
245 $objectId = $objectPrefix . '-' . (int)$child['uid'];
246 $expandSingle = $parentConfig['appearance']['expandSingle'];
247 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
248 if ($parentConfig['foreign_unique']) {
249 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
250 }
251 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
252 if ($parentConfig['appearance']['useSortable']) {
253 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
254 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
255 }
256 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
257 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
258 }
259
260 $response->getBody()->write(json_encode($jsonArray));
261
262 return $response;
263 }
264
265 /**
266 * Adds localizations or synchronizes the locations of all child records.
267 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
268 *
269 * @param ServerRequestInterface $request the incoming request
270 * @param ResponseInterface $response the empty response
271 * @return ResponseInterface the filled response
272 */
273 public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
274 {
275 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
276 $domObjectId = $ajaxArguments[0];
277 $type = $ajaxArguments[1];
278 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
279
280 /** @var InlineStackProcessor $inlineStackProcessor */
281 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
282 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
283 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
284 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
285 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
286
287 $jsonArray = false;
288 if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
289 // Parent, this table embeds the child table
290 $parent = $inlineStackProcessor->getStructureLevel(-1);
291 $parentFieldName = $parent['field'];
292
293 $processedTca = $GLOBALS['TCA'][$parent['table']];
294 $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
295
296 // Child, a record from this table should be rendered
297 $child = $inlineStackProcessor->getUnstableStructure();
298
299 $formDataCompilerInputForParent = [
300 'vanillaUid' => (int)$parent['uid'],
301 'command' => 'edit',
302 'tableName' => $parent['table'],
303 'databaseRow' => [
304 // TcaInlineExpandCollapseState needs this
305 'uid' => (int)$parent['uid'],
306 ],
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
318 /** @var TcaDatabaseRecord $formDataGroup */
319 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
320 /** @var FormDataCompiler $formDataCompiler */
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
335 if (MathUtility::canBeInterpretedAsInteger($type)) {
336 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
337 'field' => $parent['field'],
338 'language' => $parentLanguage,
339 'ids' => [$type],
340 ];
341 // Either localize or synchronize all child elements from default language of the parent element
342 } else {
343 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
344 'field' => $parent['field'],
345 'language' => $parentLanguage,
346 'action' => $type,
347 ];
348 }
349
350 /** @var $tce DataHandler */
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 // Set the items that should be removed in the forms view:
369 $removedItems = array_diff($oldItems, $newItems);
370 foreach ($removedItems as $childUid) {
371 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
372 }
373
374 $localizedItems = array_diff($newItems, $oldItems);
375 foreach ($localizedItems as $childUid) {
376 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
377
378 $childData['inlineParentUid'] = (int)$parent['uid'];
379 $childData['renderType'] = 'inlineRecordContainer';
380 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
381 $childResult = $nodeFactory->create($childData)->render();
382
383 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
384
385 // Get the name of the field used as foreign selector (if any):
386 $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
387 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
388 if (is_array($selectedValue)) {
389 $selectedValue = $selectedValue[0];
390 }
391 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
392 // Remove possible virtual records in the form which showed that a child records could be localized:
393 $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
394 if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
395 $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
396 if (is_array($transOrigPointerField)) {
397 $transOrigPointerField = $transOrigPointerField[0];
398 }
399 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
400 }
401 }
402 // Tell JS to add new HTML of one or multiple (localize all) records to DOM
403 if (!empty($jsonArray['data'])) {
404 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
405 . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
406 . ', json.data);';
407 }
408 }
409
410 $response->getBody()->write(json_encode($jsonArray));
411
412 return $response;
413 }
414
415 /**
416 * Store status of inline children expand / collapse state in backend user uC.
417 *
418 * @param ServerRequestInterface $request the incoming request
419 * @param ResponseInterface $response the empty response
420 * @return ResponseInterface the filled response
421 */
422 public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
423 {
424 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
425 $domObjectId = $ajaxArguments[0];
426
427 /** @var InlineStackProcessor $inlineStackProcessor */
428 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
429 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
430 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
431 $expand = $ajaxArguments[1];
432 $collapse = $ajaxArguments[2];
433
434 $backendUser = $this->getBackendUserAuthentication();
435 // The current table - for this table we should add/import records
436 $currentTable = $inlineStackProcessor->getUnstableStructure();
437 $currentTable = $currentTable['table'];
438 // The top parent table - this table embeds the current table
439 $top = $inlineStackProcessor->getStructureLevel(0);
440 $topTable = $top['table'];
441 $topUid = $top['uid'];
442 $inlineView = $this->getInlineExpandCollapseStateArray();
443 // Only do some action if the top record and the current record were saved before
444 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
445 $expandUids = GeneralUtility::trimExplode(',', $expand);
446 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
447 // Set records to be expanded
448 foreach ($expandUids as $uid) {
449 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
450 }
451 // Set records to be collapsed
452 foreach ($collapseUids as $uid) {
453 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
454 }
455 // Save states back to database
456 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
457 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
458 $backendUser->uc['inlineView'] = serialize($inlineView);
459 $backendUser->writeUC();
460 }
461 }
462
463 $response->getBody()->write(json_encode([]));
464 return $response;
465 }
466
467 /**
468 * Compile a full child record
469 *
470 * @param array $parentData Result array of parent
471 * @param string $parentFieldName Name of parent field
472 * @param int $childUid Uid of child to compile
473 * @param array $inlineStructure Current inline structure
474 * @return array Full result array
475 *
476 * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
477 * @todo: to also encapsulate the more complex scenarios with combination child and friends.
478 */
479 protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
480 {
481 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
482
483 /** @var InlineStackProcessor $inlineStackProcessor */
484 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
485 $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
486 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
487
488 // @todo: do not use stack processor here ...
489 $child = $inlineStackProcessor->getUnstableStructure();
490 $childTableName = $child['table'];
491
492 /** @var TcaDatabaseRecord $formDataGroup */
493 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
494 /** @var FormDataCompiler $formDataCompiler */
495 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
496 $formDataCompilerInput = [
497 'command' => 'edit',
498 'tableName' => $childTableName,
499 'vanillaUid' => (int)$childUid,
500 'isInlineChild' => true,
501 'inlineStructure' => $inlineStructure,
502 'inlineFirstPid' => $parentData['inlineFirstPid'],
503 'inlineParentConfig' => $parentConfig,
504 'isInlineAjaxOpeningContext' => true,
505
506 // values of the current parent element
507 // it is always a string either an id or new...
508 'inlineParentUid' => $parentData['databaseRow']['uid'],
509 'inlineParentTableName' => $parentData['tableName'],
510 'inlineParentFieldName' => $parentFieldName,
511
512 // values of the top most parent element set on first level and not overridden on following levels
513 'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
514 'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
515 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
516 ];
517 // For foreign_selector with useCombination $mainChild is the mm record
518 // and $combinationChild is the child-child. For "normal" relations, $mainChild
519 // is just the normal child record and $combinationChild is empty.
520 $mainChild = $formDataCompiler->compile($formDataCompilerInput);
521 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
522 // This kicks in if opening an existing mainChild that has a child-child set
523 $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
524 }
525 return $mainChild;
526 }
527
528 /**
529 * With useCombination set, not only content of the intermediate table, but also
530 * the connected child should be rendered in one go. Prepare this here.
531 *
532 * @param array $child Full data array of "mm" record
533 * @param array $parentConfig TCA configuration of "parent"
534 * @param array $inlineStructure Current inline structure
535 * @return array Full data array of child
536 */
537 protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
538 {
539 // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
540 $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
541 // child-child table name is set in child tca "the selector field" foreign_table
542 $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
543 /** @var TcaDatabaseRecord $formDataGroup */
544 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
545 /** @var FormDataCompiler $formDataCompiler */
546 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
547 $formDataCompilerInput = [
548 'command' => 'edit',
549 'tableName' => $childChildTableName,
550 'vanillaUid' => (int)$childChildUid,
551 'isInlineChild' => true,
552 'isInlineAjaxOpeningContext' => true,
553 // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
554 'inlineStructure' => $inlineStructure,
555 'inlineFirstPid' => $child['inlineFirstPid'],
556 // values of the top most parent element set on first level and not overridden on following levels
557 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
558 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
559 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
560 ];
561 return $formDataCompiler->compile($formDataCompilerInput);
562 }
563
564 /**
565 * Merge stuff from child array into json array.
566 * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
567 *
568 * @param array $jsonResult Given json result
569 * @param array $childResult Given child result
570 * @return array Merged json array
571 */
572 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
573 {
574 $jsonResult['data'] .= $childResult['html'];
575 $jsonResult['stylesheetFiles'] = [];
576 foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
577 $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
578 }
579 if (!empty($childResult['inlineData'])) {
580 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
581 }
582 if (!empty($childResult['additionalJavaScriptSubmit'])) {
583 $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
584 $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
585 $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
586 }
587 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
588 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
589 }
590 if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
591 $labels = [];
592 foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
593 ArrayUtility::mergeRecursiveWithOverrule(
594 $labels,
595 $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
596 );
597 }
598 $javaScriptCode = [];
599 $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
600 $javaScriptCode[] = ' TYPO3.lang = {}';
601 $javaScriptCode[] = '}';
602 $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
603 $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
604 $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
605 $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
606 $javaScriptCode[] = ' }';
607 $javaScriptCode[] = '}';
608
609 $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
610 }
611 $requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
612 $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
613
614 return $jsonResult;
615 }
616
617 /**
618 * Gets an array with the uids of related records out of a list of items.
619 * This list could contain more information than required. This methods just
620 * extracts the uids.
621 *
622 * @param string $itemList The list of related child records
623 * @return array An array with uids
624 */
625 protected function getInlineRelatedRecordsUidArray($itemList)
626 {
627 $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
628 // Perform modification of the selected items array:
629 foreach ($itemArray as &$value) {
630 $parts = explode('|', $value, 2);
631 $value = $parts[0];
632 }
633 unset($value);
634 return $itemArray;
635 }
636
637 /**
638 * Return expand / collapse state array for a given table / uid combination
639 *
640 * @param string $table Handled table
641 * @param int $uid Handled uid
642 * @return array
643 */
644 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
645 {
646 $inlineView = $this->getInlineExpandCollapseStateArray();
647 $result = [];
648 if (MathUtility::canBeInterpretedAsInteger($uid)) {
649 if (!empty($inlineView[$table][$uid])) {
650 $result = $inlineView[$table][$uid];
651 }
652 }
653 return $result;
654 }
655
656 /**
657 * Get expand / collapse state of inline items
658 *
659 * @return array
660 */
661 protected function getInlineExpandCollapseStateArray()
662 {
663 $backendUser = $this->getBackendUserAuthentication();
664 if (!$this->backendUserHasUcInlineView($backendUser)) {
665 return [];
666 }
667
668 $inlineView = unserialize($backendUser->uc['inlineView']);
669 if (!is_array($inlineView)) {
670 $inlineView = [];
671 }
672
673 return $inlineView;
674 }
675
676 /**
677 * Method to check whether the backend user has the property inline view for the current IRRE item.
678 * In existing or old IRRE items the attribute may not exist, then the unserialize will fail.
679 *
680 * @param BackendUserAuthentication $backendUser
681 * @return bool
682 */
683 protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
684 {
685 return !empty($backendUser->uc['inlineView']);
686 }
687
688 /**
689 * Remove an element from an array.
690 *
691 * @param mixed $needle The element to be removed.
692 * @param array $haystack The array the element should be removed from.
693 * @param bool $strict Search elements strictly.
694 * @return array The array $haystack without the $needle
695 */
696 protected function removeFromArray($needle, $haystack, $strict = false)
697 {
698 $pos = array_search($needle, $haystack, $strict);
699 if ($pos !== false) {
700 unset($haystack[$pos]);
701 }
702 return $haystack;
703 }
704
705 /**
706 * Generates an error message that transferred as JSON for AJAX calls
707 *
708 * @param string $message The error message to be shown
709 * @return array The error message in a JSON array
710 */
711 protected function getErrorMessageForAJAX($message)
712 {
713 return [
714 'data' => $message,
715 'scriptCall' => [
716 'alert("' . $message . '");'
717 ],
718 ];
719 }
720
721 /**
722 * Get inlineFirstPid from a given objectId string
723 *
724 * @param string $domObjectId The id attribute of an element
725 * @return int|NULL Pid or null
726 */
727 protected function getInlineFirstPidFromDomObjectId($domObjectId)
728 {
729 // Substitute FlexForm addition and make parsing a bit easier
730 $domObjectId = str_replace('---', ':', $domObjectId);
731 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
732 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
733 if (preg_match($pattern, $domObjectId, $match)) {
734 return $match[1];
735 }
736 return null;
737 }
738
739 /**
740 * Validates the config that is transferred over the wire to provide the
741 * correct TCA config for the parent table
742 *
743 * @param string $contextString
744 * @throws \RuntimeException
745 * @return array
746 */
747 protected function extractSignedParentConfigFromRequest(string $contextString): array
748 {
749 if ($contextString === '') {
750 throw new \RuntimeException('Empty context string given', 1489751361);
751 }
752 $context = json_decode($contextString, true);
753 if (empty($context['config'])) {
754 throw new \RuntimeException('Empty context config section given', 1489751362);
755 }
756 if (!\hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) {
757 throw new \RuntimeException('Hash does not validate', 1489751363);
758 }
759 return $context['config'];
760 }
761
762 /**
763 * @return BackendUserAuthentication
764 */
765 protected function getBackendUserAuthentication()
766 {
767 return $GLOBALS['BE_USER'];
768 }
769 }