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