[BUGFIX] Respect IRRE parent config in Ajax calls
[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\InlineParentRecord;
22 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
23 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
24 use TYPO3\CMS\Backend\Form\NodeFactory;
25 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
27 use TYPO3\CMS\Core\DataHandling\DataHandler;
28 use TYPO3\CMS\Core\Localization\LocalizationFactory;
29 use TYPO3\CMS\Core\Utility\ArrayUtility;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32
33 /**
34 * Handle FormEngine inline ajax calls
35 */
36 class FormInlineAjaxController extends AbstractFormEngineAjaxController
37 {
38 /**
39 * Create a new inline child via AJAX.
40 *
41 * @param ServerRequestInterface $request
42 * @param ResponseInterface $response
43 * @return ResponseInterface
44 */
45 public function createAction(ServerRequestInterface $request, ResponseInterface $response)
46 {
47 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
48 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
49
50 $domObjectId = $ajaxArguments[0];
51 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
52 $childChildUid = null;
53 if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
54 $childChildUid = (int)$ajaxArguments[1];
55 }
56
57 // Parse the DOM identifier, add the levels to the structure stack
58 /** @var InlineStackProcessor $inlineStackProcessor */
59 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
60 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
61 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
62 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
63
64 // Parent, this table embeds the child table
65 $parent = $inlineStackProcessor->getStructureLevel(-1);
66 $parentFieldName = $parent['field'];
67
68 if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
69 $command = 'edit';
70 $vanillaUid = (int)$parent['uid'];
71 $databaseRow = [
72 // TcaInlineExpandCollapseState needs the record uid
73 'uid' => (int)$parent['uid'],
74 ];
75 } else {
76 $command = 'new';
77 $databaseRow = [];
78 $vanillaUid = (int)$inlineFirstPid;
79 }
80
81 $processedTca = $GLOBALS['TCA'][$parent['table']];
82 $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
83 if (!empty($parentConfig['dataStructureIdentifier'])) {
84 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
85 $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']);
86 }
87
88 $formDataCompilerInputForParent = [
89 'vanillaUid' => $vanillaUid,
90 'command' => $command,
91 'tableName' => $parent['table'],
92 'databaseRow' => $databaseRow,
93 'processedTca' => $processedTca,
94 'inlineFirstPid' => $inlineFirstPid,
95 'columnsToProcess' => [
96 $parentFieldName,
97 ],
98 // Do not resolve existing children, we don't need them now
99 'inlineResolveExistingChildren' => false,
100 ];
101 /** @var TcaDatabaseRecord $formDataGroup */
102 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
103 /** @var FormDataCompiler $formDataCompiler */
104 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
105 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
106 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
107
108 // Child, a record from this table should be rendered
109 $child = $inlineStackProcessor->getUnstableStructure();
110 if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
111 // If uid comes in, it is the id of the record neighbor record "create after"
112 $childVanillaUid = -1 * abs((int)$child['uid']);
113 } else {
114 // Else inline first Pid is the storage pid of new inline records
115 $childVanillaUid = (int)$inlineFirstPid;
116 }
117
118 if ($parentConfig['type'] === 'flex') {
119 $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
120 }
121 $childTableName = $parentConfig['foreign_table'];
122
123 /** @var TcaDatabaseRecord $formDataGroup */
124 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
125 /** @var FormDataCompiler $formDataCompiler */
126 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
127 $formDataCompilerInput = [
128 'command' => 'new',
129 'tableName' => $childTableName,
130 'vanillaUid' => $childVanillaUid,
131 'isInlineChild' => true,
132 'inlineStructure' => $inlineStackProcessor->getStructure(),
133 'inlineFirstPid' => $inlineFirstPid,
134 'inlineParentUid' => $parent['uid'],
135 'inlineParentTableName' => $parent['table'],
136 'inlineParentFieldName' => $parent['field'],
137 'inlineParentConfig' => $parentConfig,
138 // Fallback to $parentData is probably not needed here.
139 'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
140 'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
141 'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
142 ];
143 if ($childChildUid) {
144 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
145 }
146 $childData = $formDataCompiler->compile($formDataCompilerInput);
147
148 // Set language of new child record to the language of the parent record:
149 // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
150 // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
151 // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
152 /**
153 if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
154 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
155 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
156 $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
157 if ($parentRecord[$parentLanguageField] > 0) {
158 $record[$childLanguageField] = $parentRecord[$parentLanguageField];
159 }
160 }
161 */
162 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
163 // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
164 // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
165 // existing record of the child-child table specified by the given uid. If there is no such id, user
166 // clicked on "created new" and a new child-child should be created, too.
167 if ($childChildUid) {
168 // Fetch existing child child
169 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
170 $childChildUid,
171 ];
172 $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
173 } else {
174 /** @var TcaDatabaseRecord $formDataGroup */
175 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
176 /** @var FormDataCompiler $formDataCompiler */
177 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
178 $formDataCompilerInput = [
179 'command' => 'new',
180 'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
181 'vanillaUid' => (int)$inlineFirstPid,
182 'isInlineChild' => true,
183 'isInlineAjaxOpeningContext' => true,
184 'inlineStructure' => $inlineStackProcessor->getStructure(),
185 'inlineFirstPid' => (int)$inlineFirstPid,
186 ];
187 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
188 }
189 }
190
191 $childData['inlineParentUid'] = (int)$parent['uid'];
192 $childData['renderType'] = 'inlineRecordContainer';
193 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
194 $childResult = $nodeFactory->create($childData)->render();
195
196 $jsonArray = [
197 'data' => '',
198 'stylesheetFiles' => [],
199 'scriptCall' => [],
200 ];
201
202 // The HTML-object-id's prefix of the dynamically created record
203 $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
204 $objectPrefix = $objectName . '-' . $child['table'];
205 $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
206 $expandSingle = $parentConfig['appearance']['expandSingle'];
207 if (!$child['uid']) {
208 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
209 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
210 } else {
211 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
212 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
213 }
214 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
215 if ($parentConfig['appearance']['useSortable']) {
216 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
217 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
218 }
219 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
220 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
221 }
222 // Fade out and fade in the new record in the browser view to catch the user's eye
223 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
224
225 $response->getBody()->write(json_encode($jsonArray));
226
227 return $response;
228 }
229
230 /**
231 * Show the details of a child record.
232 *
233 * @param ServerRequestInterface $request
234 * @param ResponseInterface $response
235 * @return ResponseInterface
236 */
237 public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
238 {
239 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
240
241 $domObjectId = $ajaxArguments[0];
242 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
243 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
244
245 // Parse the DOM identifier, add the levels to the structure stack
246 /** @var InlineStackProcessor $inlineStackProcessor */
247 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
248 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
249 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
250
251 // Parent, this table embeds the child table
252 $parent = $inlineStackProcessor->getStructureLevel(-1);
253 $parentFieldName = $parent['field'];
254
255 $databaseRow = [
256 // TcaInlineExpandCollapseState needs this
257 'uid' => (int)$parent['uid'],
258 ];
259
260 $processedTca = $GLOBALS['TCA'][$parent['table']];
261 $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
262 if (!empty($parentConfig['dataStructureIdentifier'])) {
263 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
264 $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']);
265 }
266
267 $formDataCompilerInputForParent = [
268 'vanillaUid' => (int)$parent['uid'],
269 'command' => 'edit',
270 'tableName' => $parent['table'],
271 'databaseRow' => $databaseRow,
272 'processedTca' => $processedTca,
273 'inlineFirstPid' => $inlineFirstPid,
274 'columnsToProcess' => [
275 $parentFieldName
276 ],
277 // @todo: still needed?
278 'inlineStructure' => $inlineStackProcessor->getStructure(),
279 // Do not resolve existing children, we don't need them now
280 'inlineResolveExistingChildren' => false,
281 ];
282 /** @var TcaDatabaseRecord $formDataGroup */
283 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
284 /** @var FormDataCompiler $formDataCompiler */
285 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
286 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
287 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
288
289 if ($parentConfig['type'] === 'flex') {
290 $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
291 $parentData['processedTca']['columns'][$parentFieldName]['config'] = $parentConfig;
292 }
293
294 // Set flag in config so that only the fields are rendered
295 // @todo: Solve differently / rename / whatever
296 $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
297
298 // Child, a record from this table should be rendered
299 $child = $inlineStackProcessor->getUnstableStructure();
300
301 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
302
303 $childData['inlineParentUid'] = (int)$parent['uid'];
304 $childData['renderType'] = 'inlineRecordContainer';
305 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
306 $childResult = $nodeFactory->create($childData)->render();
307
308 $jsonArray = [
309 'data' => '',
310 'stylesheetFiles' => [],
311 'scriptCall' => [],
312 ];
313
314 // The HTML-object-id's prefix of the dynamically created record
315 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
316 $objectId = $objectPrefix . '-' . (int)$child['uid'];
317 $expandSingle = $parentConfig['appearance']['expandSingle'];
318 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
319 if ($parentConfig['foreign_unique']) {
320 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
321 }
322 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
323 if ($parentConfig['appearance']['useSortable']) {
324 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
325 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
326 }
327 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
328 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
329 }
330
331 $response->getBody()->write(json_encode($jsonArray));
332
333 return $response;
334 }
335
336 /**
337 * Adds localizations or synchronizes the locations of all child records.
338 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
339 *
340 * @param ServerRequestInterface $request the incoming request
341 * @param ResponseInterface $response the empty response
342 * @return ResponseInterface the filled response
343 */
344 public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
345 {
346 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
347 $domObjectId = $ajaxArguments[0];
348 $type = $ajaxArguments[1];
349 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
350
351 /** @var InlineStackProcessor $inlineStackProcessor */
352 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
353 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
354 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
355 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
356 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
357
358 $jsonArray = false;
359 if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
360 // Parent, this table embeds the child table
361 $parent = $inlineStackProcessor->getStructureLevel(-1);
362 $parentFieldName = $parent['field'];
363
364 $processedTca = $GLOBALS['TCA'][$parent['table']];
365 $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
366
367 // Child, a record from this table should be rendered
368 $child = $inlineStackProcessor->getUnstableStructure();
369
370 $formDataCompilerInputForParent = [
371 'vanillaUid' => (int)$parent['uid'],
372 'command' => 'edit',
373 'tableName' => $parent['table'],
374 'databaseRow' => [
375 // TcaInlineExpandCollapseState needs this
376 'uid' => (int)$parent['uid'],
377 ],
378 'processedTca' => $processedTca,
379 'inlineFirstPid' => $inlineFirstPid,
380 'columnsToProcess' => [
381 $parentFieldName
382 ],
383 // @todo: still needed? NO!
384 'inlineStructure' => $inlineStackProcessor->getStructure(),
385 // Do not compile existing children, we don't need them now
386 'inlineCompileExistingChildren' => false,
387 ];
388 // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
389 /** @var TcaDatabaseRecord $formDataGroup */
390 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
391 /** @var FormDataCompiler $formDataCompiler */
392 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
393 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
394 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
395 $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
396 $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
397 $oldItemList = $parentData['databaseRow'][$parentFieldName];
398
399 // DataHandler cannot handle arrays as field value
400 if (is_array($parentLanguage)) {
401 $parentLanguage = implode(',', $parentLanguage);
402 }
403
404 $cmd = [];
405 // Localize a single child element from default language of the parent element
406 if (MathUtility::canBeInterpretedAsInteger($type)) {
407 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
408 'field' => $parent['field'],
409 'language' => $parentLanguage,
410 'ids' => [$type],
411 ];
412 // Either localize or synchronize all child elements from default language of the parent element
413 } else {
414 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
415 'field' => $parent['field'],
416 'language' => $parentLanguage,
417 'action' => $type,
418 ];
419 }
420
421 /** @var $tce DataHandler */
422 $tce = GeneralUtility::makeInstance(DataHandler::class);
423 $tce->start([], $cmd);
424 $tce->process_cmdmap();
425
426 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
427
428 $jsonArray = [
429 'data' => '',
430 'stylesheetFiles' => [],
431 'scriptCall' => [],
432 ];
433 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
434 $nameObjectForeignTable = $nameObject . '-' . $child['table'];
435
436 $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
437 $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
438
439 // Set the items that should be removed in the forms view:
440 $removedItems = array_diff($oldItems, $newItems);
441 foreach ($removedItems as $childUid) {
442 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
443 }
444
445 $localizedItems = array_diff($newItems, $oldItems);
446 foreach ($localizedItems as $childUid) {
447 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
448
449 $childData['inlineParentUid'] = (int)$parent['uid'];
450 $childData['renderType'] = 'inlineRecordContainer';
451 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
452 $childResult = $nodeFactory->create($childData)->render();
453
454 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
455
456 // Get the name of the field used as foreign selector (if any):
457 $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
458 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
459 if (is_array($selectedValue)) {
460 $selectedValue = $selectedValue[0];
461 }
462 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
463 // Remove possible virtual records in the form which showed that a child records could be localized:
464 $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
465 if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
466 $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
467 if (is_array($transOrigPointerField)) {
468 $transOrigPointerField = $transOrigPointerField[0];
469 }
470 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
471 }
472 }
473 // Tell JS to add new HTML of one or multiple (localize all) records to DOM
474 if (!empty($jsonArray['data'])) {
475 array_push(
476 $jsonArray['scriptCall'],
477 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
478 . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
479 . ', json.data);'
480 );
481 }
482 }
483
484 $response->getBody()->write(json_encode($jsonArray));
485
486 return $response;
487 }
488
489 /**
490 * Adds localizations or synchronizes the locations of all child records.
491 *
492 * @param ServerRequestInterface $request the incoming request
493 * @param ResponseInterface $response the empty response
494 * @return ResponseInterface the filled response
495 */
496 public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
497 {
498 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
499 $domObjectId = $ajaxArguments[0];
500
501 /** @var InlineStackProcessor $inlineStackProcessor */
502 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
503 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
504 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
505 $expand = $ajaxArguments[1];
506 $collapse = $ajaxArguments[2];
507
508 $backendUser = $this->getBackendUserAuthentication();
509 // The current table - for this table we should add/import records
510 $currentTable = $inlineStackProcessor->getUnstableStructure();
511 $currentTable = $currentTable['table'];
512 // The top parent table - this table embeds the current table
513 $top = $inlineStackProcessor->getStructureLevel(0);
514 $topTable = $top['table'];
515 $topUid = $top['uid'];
516 $inlineView = $this->getInlineExpandCollapseStateArray();
517 // Only do some action if the top record and the current record were saved before
518 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
519 $expandUids = GeneralUtility::trimExplode(',', $expand);
520 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
521 // Set records to be expanded
522 foreach ($expandUids as $uid) {
523 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
524 }
525 // Set records to be collapsed
526 foreach ($collapseUids as $uid) {
527 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
528 }
529 // Save states back to database
530 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
531 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
532 $backendUser->uc['inlineView'] = serialize($inlineView);
533 $backendUser->writeUC();
534 }
535 }
536
537 $response->getBody()->write(json_encode([]));
538 return $response;
539 }
540
541 /**
542 * Compile a full child record
543 *
544 * @param array $parentData Result array of parent
545 * @param string $parentFieldName Name of parent field
546 * @param int $childUid Uid of child to compile
547 * @param array $inlineStructure Current inline structure
548 * @return array Full result array
549 *
550 * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
551 * @todo: to also encapsulate the more complex scenarios with combination child and friends.
552 */
553 protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
554 {
555 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
556
557 /** @var InlineStackProcessor $inlineStackProcessor */
558 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
559 $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
560 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
561
562 // @todo: do not use stack processor here ...
563 $child = $inlineStackProcessor->getUnstableStructure();
564 $childTableName = $child['table'];
565
566 /** @var TcaDatabaseRecord $formDataGroup */
567 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
568 /** @var FormDataCompiler $formDataCompiler */
569 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
570 $formDataCompilerInput = [
571 'command' => 'edit',
572 'tableName' => $childTableName,
573 'vanillaUid' => (int)$childUid,
574 'isInlineChild' => true,
575 'inlineStructure' => $inlineStructure,
576 'inlineFirstPid' => $parentData['inlineFirstPid'],
577 'inlineParentConfig' => $parentConfig,
578 'isInlineAjaxOpeningContext' => true,
579
580 // values of the current parent element
581 // it is always a string either an id or new...
582 'inlineParentUid' => $parentData['databaseRow']['uid'],
583 'inlineParentTableName' => $parentData['tableName'],
584 'inlineParentFieldName' => $parentFieldName,
585
586 // values of the top most parent element set on first level and not overridden on following levels
587 'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
588 'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
589 'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
590 ];
591 // For foreign_selector with useCombination $mainChild is the mm record
592 // and $combinationChild is the child-child. For "normal" relations, $mainChild
593 // is just the normal child record and $combinationChild is empty.
594 $mainChild = $formDataCompiler->compile($formDataCompilerInput);
595 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
596 // This kicks in if opening an existing mainChild that has a child-child set
597 $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
598 }
599 return $mainChild;
600 }
601
602 /**
603 * With useCombination set, not only content of the intermediate table, but also
604 * the connected child should be rendered in one go. Prepare this here.
605 *
606 * @param array $child Full data array of "mm" record
607 * @param array $parentConfig TCA configuration of "parent"
608 * @param array $inlineStructure Current inline structure
609 * @return array Full data array of child
610 */
611 protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
612 {
613 // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
614 $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
615 // child-child table name is set in child tca "the selector field" foreign_table
616 $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
617 /** @var TcaDatabaseRecord $formDataGroup */
618 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
619 /** @var FormDataCompiler $formDataCompiler */
620 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
621 $formDataCompilerInput = [
622 'command' => 'edit',
623 'tableName' => $childChildTableName,
624 'vanillaUid' => (int)$childChildUid,
625 'isInlineChild' => true,
626 'isInlineAjaxOpeningContext' => true,
627 // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
628 'inlineStructure' => $inlineStructure,
629 'inlineFirstPid' => $child['inlineFirstPid'],
630 // values of the top most parent element set on first level and not overridden on following levels
631 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
632 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
633 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
634 ];
635 return $formDataCompiler->compile($formDataCompilerInput);
636 }
637
638 /**
639 * Merge stuff from child array into json array.
640 * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
641 *
642 * @param array $jsonResult Given json result
643 * @param array $childResult Given child result
644 * @return array Merged json array
645 */
646 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
647 {
648 $jsonResult['data'] .= $childResult['html'];
649 $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
650 if (!empty($childResult['inlineData'])) {
651 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
652 }
653 if (!empty($childResult['additionalJavaScriptSubmit'])) {
654 $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
655 $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
656 $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
657 }
658 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
659 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
660 }
661 if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
662 $labels = [];
663 foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
664 ArrayUtility::mergeRecursiveWithOverrule(
665 $labels,
666 $this->addInlineLanguageLabelFile($additionalInlineLanguageLabelFile)
667 );
668 }
669 $javaScriptCode = [];
670 $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
671 $javaScriptCode[] = ' TYPO3.lang = {}';
672 $javaScriptCode[] = '}';
673 $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
674 $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
675 $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
676 $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
677 $javaScriptCode[] = ' }';
678 $javaScriptCode[] = '}';
679
680 $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
681 }
682 $requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
683 $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
684
685 return $jsonResult;
686 }
687
688 /**
689 * @param string $file
690 *
691 * @return array
692 */
693 protected function addInlineLanguageLabelFile($file)
694 {
695 /** @var $languageFactory LocalizationFactory */
696 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
697 $language = $GLOBALS['LANG']->lang;
698 $localizationArray = $languageFactory->getParsedData(
699 $file,
700 $language,
701 'utf-8',
702 1
703 );
704 if (is_array($localizationArray) && !empty($localizationArray)) {
705 if (!empty($localizationArray[$language])) {
706 $xlfLabelArray = $localizationArray['default'];
707 ArrayUtility::mergeRecursiveWithOverrule($xlfLabelArray, $localizationArray[$language], true, false);
708 } else {
709 $xlfLabelArray = $localizationArray['default'];
710 }
711 } else {
712 $xlfLabelArray = [];
713 }
714 $labelArray = [];
715 foreach ($xlfLabelArray as $key => $value) {
716 if (isset($value[0]['target'])) {
717 $labelArray[$key] = $value[0]['target'];
718 } else {
719 $labelArray[$key] = '';
720 }
721 }
722 return $labelArray;
723 }
724
725 /**
726 * Gets an array with the uids of related records out of a list of items.
727 * This list could contain more information than required. This methods just
728 * extracts the uids.
729 *
730 * @param string $itemList The list of related child records
731 * @return array An array with uids
732 */
733 protected function getInlineRelatedRecordsUidArray($itemList)
734 {
735 $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
736 // Perform modification of the selected items array:
737 foreach ($itemArray as &$value) {
738 $parts = explode('|', $value, 2);
739 $value = $parts[0];
740 }
741 unset($value);
742 return $itemArray;
743 }
744
745 /**
746 * Checks if a record selector may select a certain file type
747 *
748 * @param array $selectorConfiguration
749 * @param array $fileRecord
750 * @return bool
751 * @todo: check this ...
752 */
753 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
754 {
755 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
756 $allowedFileExtensions = GeneralUtility::trimExplode(
757 ',',
758 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
759 true
760 );
761 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
762 return false;
763 }
764 }
765 return true;
766 }
767
768 /**
769 * Return expand / collapse state array for a given table / uid combination
770 *
771 * @param string $table Handled table
772 * @param int $uid Handled uid
773 * @return array
774 */
775 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
776 {
777 $inlineView = $this->getInlineExpandCollapseStateArray();
778 $result = [];
779 if (MathUtility::canBeInterpretedAsInteger($uid)) {
780 if (!empty($inlineView[$table][$uid])) {
781 $result = $inlineView[$table][$uid];
782 }
783 }
784 return $result;
785 }
786
787 /**
788 * Get expand / collapse state of inline items
789 *
790 * @return array
791 */
792 protected function getInlineExpandCollapseStateArray()
793 {
794 $backendUser = $this->getBackendUserAuthentication();
795 if (!$this->backendUserHasUcInlineView($backendUser)) {
796 return [];
797 }
798
799 $inlineView = unserialize($backendUser->uc['inlineView']);
800 if (!is_array($inlineView)) {
801 $inlineView = [];
802 }
803
804 return $inlineView;
805 }
806
807 /**
808 * Method to check whether the backend user has the property inline view for the current IRRE item.
809 * In existing or old IRRE items the attribute may not exist, then the unserialize will fail.
810 *
811 * @param BackendUserAuthentication $backendUser
812 * @return bool
813 */
814 protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
815 {
816 return !empty($backendUser->uc['inlineView']);
817 }
818
819 /**
820 * Remove an element from an array.
821 *
822 * @param mixed $needle The element to be removed.
823 * @param array $haystack The array the element should be removed from.
824 * @param bool $strict Search elements strictly.
825 * @return array The array $haystack without the $needle
826 */
827 protected function removeFromArray($needle, $haystack, $strict = false)
828 {
829 $pos = array_search($needle, $haystack, $strict);
830 if ($pos !== false) {
831 unset($haystack[$pos]);
832 }
833 return $haystack;
834 }
835
836 /**
837 * Generates an error message that transferred as JSON for AJAX calls
838 *
839 * @param string $message The error message to be shown
840 * @return array The error message in a JSON array
841 */
842 protected function getErrorMessageForAJAX($message)
843 {
844 return [
845 'data' => $message,
846 'scriptCall' => [
847 'alert("' . $message . '");'
848 ],
849 ];
850 }
851
852 /**
853 * Get inlineFirstPid from a given objectId string
854 *
855 * @param string $domObjectId The id attribute of an element
856 * @return int|NULL Pid or null
857 */
858 protected function getInlineFirstPidFromDomObjectId($domObjectId)
859 {
860 // Substitute FlexForm addition and make parsing a bit easier
861 $domObjectId = str_replace('---', ':', $domObjectId);
862 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
863 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
864 if (preg_match($pattern, $domObjectId, $match)) {
865 return $match[1];
866 }
867 return null;
868 }
869
870 /**
871 * @return BackendUserAuthentication
872 */
873 protected function getBackendUserAuthentication()
874 {
875 return $GLOBALS['BE_USER'];
876 }
877
878 /**
879 * Extract the inline child table configuration from the flexform data structure
880 * using the the domObjectId to traverse the XML structure.
881 *
882 * domObjectId parsing has been copied from InlineStackProcessor::initializeByDomObjectId
883 *
884 * @param array $parentConfig
885 * @param string $domObjectId
886 * @return array
887 */
888 protected function getParentConfigFromFlexForm(array $parentConfig, string $domObjectId) : array
889 {
890 list($flexFormPath, $foreignTableName) = $this->splitDomObjectId($domObjectId);
891
892 $childConfig = $parentConfig['ds']['sheets'];
893 $flexFormPath = explode(':', $flexFormPath);
894 foreach ($flexFormPath as $flexFormNode) {
895 // We are dealing with configuration information from a flexform,
896 // not value storage, identifiers that reference language or
897 // value nodes must be skipped.
898 if (!isset($childConfig[$flexFormNode]) && preg_match('/^[lv][[:alpha:]]+$/', $flexFormNode)) {
899 continue;
900 }
901 $childConfig = $childConfig[$flexFormNode];
902
903 // Skip to the field configuration of a sheet
904 if (isset($childConfig['ROOT']) && $childConfig['ROOT']['type'] === 'array') {
905 $childConfig = $childConfig['ROOT']['el'];
906 }
907 }
908
909 if (!isset($childConfig['config'])
910 || !is_array($childConfig['config'])
911 || $childConfig['config']['type'] !== 'inline'
912 || $childConfig['config']['foreign_table'] !== $foreignTableName
913 ) {
914 throw new \UnexpectedValueException(
915 'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
916 1446996319
917 );
918 }
919 return $childConfig['config'];
920 }
921
922 /**
923 * Validates the config that is transferred over the wire to provide the
924 * correct TCA config for the parent table
925 *
926 * @param string $contextString
927 * @return array
928 * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead?
929 */
930 protected function extractSignedParentConfigFromRequest(string $contextString): array
931 {
932 if ($contextString === '') {
933 return [];
934 }
935 $context = json_decode($contextString, true);
936 if (empty($context['config'])) {
937 return [];
938 }
939 if (!\hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) {
940 return [];
941 }
942 return $context['config'];
943 }
944
945 /**
946 * split the domObjectID and retrieve the needed parts
947 *
948 * @param string $domObjectId
949 *
950 * @return array
951 */
952 protected function splitDomObjectId(string $domObjectId) : array
953 {
954
955 // Substitute FlexForm addition and make parsing a bit easier
956 $domObjectId = str_replace('---', ':', $domObjectId);
957 $pattern = '/:data:(?<flexformPath>.*?)-(?<tableName>[^-]+)(?:-(?:NEW)?\w+)?$/';
958
959 /* EXPLANATION for the regex:
960 * according https://regex101.com/
961 *
962 * :data: matches the characters :data: literally (case sensitive)
963 * (?<flexformPath>.*?) Named capturing group flexformPath
964 * .*? matches any character (except newline)
965 * Quantifier: *? Between zero and unlimited times, as few times as possible, expanding as needed [lazy]
966 * - matches the character - literally
967 * (?<tableName>[^-]+) Named capturing group tableName
968 * [^-]+ match a single character not present in the list below
969 * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
970 * - the literal character -
971 * (?:-(?:NEW)?\w+)? Non-capturing group
972 * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
973 * - matches the character - literally
974 * (?:NEW)? Non-capturing group
975 * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
976 * NEW matches the characters NEW literally (case sensitive)
977 * \w+ match any word character [a-zA-Z0-9_]
978 * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
979 * $ assert position at end of a line
980 */
981
982 if (preg_match($pattern, $domObjectId, $match)) {
983 return [$match['flexformPath'], $match['tableName']];
984 }
985
986 return [];
987 }
988 }