[BUGFIX] FormEngine inline foreign_selector and foreign_unique
[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\Backend\Form\Utility\FormEngineUtility;
25 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26 use TYPO3\CMS\Core\DataHandling\DataHandler;
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
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
46 $domObjectId = $ajaxArguments[0];
47 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
48 $childChildUid = null;
49 if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
50 $childChildUid = (int)$ajaxArguments[1];
51 }
52
53 // Parse the DOM identifier, add the levels to the structure stack
54 /** @var InlineStackProcessor $inlineStackProcessor */
55 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
56 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
57 $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
58
59 // Parent, this table embeds the child table
60 $parent = $inlineStackProcessor->getStructureLevel(-1);
61 $parentFieldName = $parent['field'];
62
63 if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
64 $command = 'edit';
65 $vanillaUid = (int)$parent['uid'];
66 $databaseRow = [
67 // TcaInlineExpandCollapseState needs the record uid
68 'uid' => (int)$parent['uid'],
69 ];
70 } else {
71 $command = 'new';
72 $databaseRow = [];
73 $vanillaUid = (int)$inlineFirstPid;
74 }
75 $formDataCompilerInputForParent = [
76 'vanillaUid' => $vanillaUid,
77 'command' => $command,
78 'tableName' => $parent['table'],
79 'databaseRow' => $databaseRow,
80 'inlineFirstPid' => $inlineFirstPid,
81 'columnsToProcess' => [
82 $parentFieldName
83 ],
84 // @todo: still needed?
85 'inlineStructure' => $inlineStackProcessor->getStructure(),
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 $childTableName = $parentConfig['foreign_table'];
107 /** @var TcaDatabaseRecord $formDataGroup */
108 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
109 /** @var FormDataCompiler $formDataCompiler */
110 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
111 $formDataCompilerInput = [
112 'command' => 'new',
113 'tableName' => $childTableName,
114 'vanillaUid' => $childVanillaUid,
115 'inlineFirstPid' => $inlineFirstPid,
116 'inlineParentConfig' => $parentConfig,
117 ];
118 if ($childChildUid) {
119 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
120 }
121 $childData = $formDataCompiler->compile($formDataCompilerInput);
122
123 // Set language of new child record to the language of the parent record:
124 // @todo: To my understanding, the below case can't happen: With localizationMode select, lang overlays
125 // @todo: of children are only created with the "synchronize" button that will trigger a different ajax action.
126 // @todo: The edge case of new page overlay together with localized media field, this code won't kick in either.
127 /**
128 if ($parent['localizationMode'] === 'select' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
129 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
130 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
131 $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
132 if ($parentRecord[$parentLanguageField] > 0) {
133 $record[$childLanguageField] = $parentRecord[$parentLanguageField];
134 }
135 }
136 */
137
138 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
139 // We have a foreign_selector. So, we just created a new record on an intermediate table in $mainChild.
140 // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
141 // existing record of the child-child table specified by the given uid. If there is no such id, user
142 // clicked on "created new" and a new child-child should be created, too.
143 if ($childChildUid) {
144 // Fetch existing child child
145 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
146 $childChildUid,
147 ];
148 $childData['combinationChild'] = $this->compileCombinationChild($childData, $parentConfig);
149 } else {
150 /** @var TcaDatabaseRecord $formDataGroup */
151 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
152 /** @var FormDataCompiler $formDataCompiler */
153 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
154 $formDataCompilerInput = [
155 'command' => 'new',
156 'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
157 'vanillaUid' => (int)$inlineFirstPid,
158 'inlineFirstPid' => (int)$inlineFirstPid,
159 ];
160 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
161 }
162 }
163
164 $childData['inlineParentUid'] = (int)$parent['uid'];
165 // @todo: needed?
166 $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
167 // @todo: needed?
168 $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
169 $childData['renderType'] = 'inlineRecordContainer';
170 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
171 $childResult = $nodeFactory->create($childData)->render();
172
173 $jsonArray = [
174 'data' => '',
175 'stylesheetFiles' => [],
176 'scriptCall' => [],
177 ];
178
179 // The HTML-object-id's prefix of the dynamically created record
180 $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
181 $objectPrefix = $objectName . '-' . $child['table'];
182 $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
183 $expandSingle = $parentConfig['appearance']['expandSingle'];
184 if (!$child['uid']) {
185 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
186 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
187 } else {
188 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
189 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
190 }
191 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
192 if ($parentConfig['appearance']['useSortable']) {
193 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
194 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
195 }
196 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
197 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
198 }
199 // Fade out and fade in the new record in the browser view to catch the user's eye
200 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
201
202 $response->getBody()->write(json_encode($jsonArray));
203
204 return $response;
205 }
206
207 /**
208 * Show the details of a child record.
209 *
210 * @param ServerRequestInterface $request
211 * @param ResponseInterface $response
212 * @return ResponseInterface
213 */
214 public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
215 {
216 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
217
218 $domObjectId = $ajaxArguments[0];
219 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
220
221 // Parse the DOM identifier, add the levels to the structure stack
222 /** @var InlineStackProcessor $inlineStackProcessor */
223 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
224 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
225 $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
226
227 // Parent, this table embeds the child table
228 $parent = $inlineStackProcessor->getStructureLevel(-1);
229 $parentFieldName = $parent['field'];
230
231 $formDataCompilerInputForParent = [
232 'vanillaUid' => (int)$parent['uid'],
233 'command' => 'edit',
234 'tableName' => $parent['table'],
235 'databaseRow' => [
236 // TcaInlineExpandCollapseState needs this
237 'uid' => (int)$parent['uid'],
238 ],
239 'inlineFirstPid' => $inlineFirstPid,
240 'columnsToProcess' => [
241 $parentFieldName
242 ],
243 // @todo: still needed?
244 'inlineStructure' => $inlineStackProcessor->getStructure(),
245 // Do not resolve existing children, we don't need them now
246 'inlineResolveExistingChildren' => false,
247 ];
248 /** @var TcaDatabaseRecord $formDataGroup */
249 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
250 /** @var FormDataCompiler $formDataCompiler */
251 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
252 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
253 // Set flag in config so that only the fields are rendered
254 // @todo: Solve differently / rename / whatever
255 $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
256 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
257
258 // Child, a record from this table should be rendered
259 $child = $inlineStackProcessor->getUnstableStructure();
260
261 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid']);
262
263 $childData['inlineParentUid'] = (int)$parent['uid'];
264 // @todo: needed?
265 $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
266 // @todo: needed?
267 $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
268 $childData['renderType'] = 'inlineRecordContainer';
269 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
270 $childResult = $nodeFactory->create($childData)->render();
271
272 $jsonArray = [
273 'data' => '',
274 'stylesheetFiles' => [],
275 'scriptCall' => [],
276 ];
277
278 // The HTML-object-id's prefix of the dynamically created record
279 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
280 $objectId = $objectPrefix . '-' . (int)$child['uid'];
281 $expandSingle = $parentConfig['appearance']['expandSingle'];
282 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
283 if ($parentConfig['foreign_unique']) {
284 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
285 }
286 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
287 if ($parentConfig['appearance']['useSortable']) {
288 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
289 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
290 }
291 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
292 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
293 }
294
295 $response->getBody()->write(json_encode($jsonArray));
296
297 return $response;
298 }
299
300 /**
301 * Adds localizations or synchronizes the locations of all child records.
302 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
303 *
304 * @param ServerRequestInterface $request the incoming request
305 * @param ResponseInterface $response the empty response
306 * @return ResponseInterface the filled response
307 */
308 public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
309 {
310 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
311 $domObjectId = $ajaxArguments[0];
312 $type = $ajaxArguments[1];
313
314 /** @var InlineStackProcessor $inlineStackProcessor */
315 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
316 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
317 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
318 $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
319 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
320
321 $jsonArray = false;
322 if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
323 // Parent, this table embeds the child table
324 $parent = $inlineStackProcessor->getStructureLevel(-1);
325 $parentFieldName = $parent['field'];
326
327 // Child, a record from this table should be rendered
328 $child = $inlineStackProcessor->getUnstableStructure();
329
330 $formDataCompilerInputForParent = [
331 'vanillaUid' => (int)$parent['uid'],
332 'command' => 'edit',
333 'tableName' => $parent['table'],
334 'databaseRow' => [
335 // TcaInlineExpandCollapseState needs this
336 'uid' => (int)$parent['uid'],
337 ],
338 'inlineFirstPid' => $inlineFirstPid,
339 'columnsToProcess' => [
340 $parentFieldName
341 ],
342 // @todo: still needed?
343 'inlineStructure' => $inlineStackProcessor->getStructure(),
344 // Do not compile existing children, we don't need them now
345 'inlineCompileExistingChildren' => false,
346 ];
347 /** @var TcaDatabaseRecord $formDataGroup */
348 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
349 /** @var FormDataCompiler $formDataCompiler */
350 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
351 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
352 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
353 $oldItemList = $parentData['databaseRow'][$parentFieldName];
354
355 $cmd = array();
356 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = $parent['field'] . ',' . $type;
357 /** @var $tce DataHandler */
358 $tce = GeneralUtility::makeInstance(DataHandler::class);
359 $tce->stripslashes_values = false;
360 $tce->start(array(), $cmd);
361 $tce->process_cmdmap();
362
363 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
364
365 $jsonArray = array(
366 'data' => '',
367 'stylesheetFiles' => [],
368 'scriptCall' => [],
369 );
370 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
371 $nameObjectForeignTable = $nameObject . '-' . $child['table'];
372
373 $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
374 $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
375
376 // Set the items that should be removed in the forms view:
377 $removedItems = array_diff($oldItems, $newItems);
378 foreach ($removedItems as $childUid) {
379 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
380 }
381
382 $localizedItems = array_diff($newItems, $oldItems);
383 foreach ($localizedItems as $childUid) {
384 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid);
385
386 $childData['inlineParentUid'] = (int)$parent['uid'];
387 // @todo: needed?
388 $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
389 // @todo: needed?
390 $childData['inlineExpandCollapseStateArray'] = $parentData['inlineExpandCollapseStateArray'];
391 $childData['renderType'] = 'inlineRecordContainer';
392 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
393 $childResult = $nodeFactory->create($childData)->render();
394
395 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
396
397 // Get the name of the field used as foreign selector (if any):
398 $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
399 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
400 if (is_array($selectedValue)) {
401 $selectedValue = $selectedValue[0];
402 }
403 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
404 // Remove possible virtual records in the form which showed that a child records could be localized:
405 $transOrigPointerFieldName = $GLOBALS['TCA'][$childData['table']]['ctrl']['transOrigPointerField'];
406 if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
407 $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
408 if (is_array($transOrigPointerField)) {
409 $transOrigPointerField = $transOrigPointerField[0];
410 }
411 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
412 }
413 if (!empty($childResult['html'])) {
414 array_unshift($jsonArray['scriptCall'], 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records') . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', json.data);');
415 }
416 }
417 }
418
419 $response->getBody()->write(json_encode($jsonArray));
420
421 return $response;
422 }
423
424 /**
425 * Adds localizations or synchronizes the locations of all child records.
426 *
427 * @param ServerRequestInterface $request the incoming request
428 * @param ResponseInterface $response the empty response
429 * @return ResponseInterface the filled response
430 */
431 public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
432 {
433 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
434 $domObjectId = $ajaxArguments[0];
435
436 /** @var InlineStackProcessor $inlineStackProcessor */
437 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
438 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
439 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
440 $expand = $ajaxArguments[1];
441 $collapse = $ajaxArguments[2];
442
443 $backendUser = $this->getBackendUserAuthentication();
444 // The current table - for this table we should add/import records
445 $currentTable = $inlineStackProcessor->getUnstableStructure();
446 $currentTable = $currentTable['table'];
447 // The top parent table - this table embeds the current table
448 $top = $inlineStackProcessor->getStructureLevel(0);
449 $topTable = $top['table'];
450 $topUid = $top['uid'];
451 $inlineView = $this->getInlineExpandCollapseStateArray();
452 // Only do some action if the top record and the current record were saved before
453 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
454 $expandUids = GeneralUtility::trimExplode(',', $expand);
455 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
456 // Set records to be expanded
457 foreach ($expandUids as $uid) {
458 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
459 }
460 // Set records to be collapsed
461 foreach ($collapseUids as $uid) {
462 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
463 }
464 // Save states back to database
465 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
466 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
467 $backendUser->uc['inlineView'] = serialize($inlineView);
468 $backendUser->writeUC();
469 }
470 }
471
472 $response->getBody()->write(json_encode(array()));
473 return $response;
474 }
475
476 /**
477 * Compile a full child record
478 *
479 * @param array $parentData Result array of parent
480 * @param string $parentFieldName Name of parent field
481 * @param int $childUid Uid of child to compile
482 * @return array Full result array
483 *
484 * @todo: This clones methods compileChild and compileCombinationChild from TcaInline Provider.
485 * @todo: Find something around that, eg. some option to force TcaInline provider to calculate a
486 * @todo: specific forced-open element only :)
487 */
488 protected function compileChild(array $parentData, $parentFieldName, $childUid)
489 {
490 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
491 $childTableName = $parentConfig['foreign_table'];
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 'inlineFirstPid' => $parentData['inlineFirstPid'],
501 'inlineParentConfig' => $parentConfig,
502 ];
503 // For foreign_selector with useCombination $mainChild is the mm record
504 // and $combinationChild is the child-child. For "normal" relations, $mainChild
505 // is just the normal child record and $combinationChild is empty.
506 $mainChild = $formDataCompiler->compile($formDataCompilerInput);
507 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
508 $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig);
509 }
510 return $mainChild;
511 }
512
513 /**
514 * With useCombination set, not only content of the intermediate table, but also
515 * the connected child should be rendered in one go. Prepare this here.
516 *
517 * @param array $intermediate Full data array of "mm" record
518 * @param array $parentConfig TCA configuration of "parent"
519 * @return array Full data array of child
520 */
521 protected function compileCombinationChild(array $intermediate, array $parentConfig)
522 {
523 // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
524 $intermediateUid = $intermediate['databaseRow'][$parentConfig['foreign_selector']][0];
525 $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid);
526 return $combinationChild;
527 }
528
529 /**
530 * Merge stuff from child array into json array.
531 * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
532 *
533 * @param array $jsonResult Given json result
534 * @param array $childResult Given child result
535 * @return array Merged json array
536 */
537 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
538 {
539 $jsonResult['data'] = $childResult['html'];
540 $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
541 if (!empty($childResult['inlineData'])) {
542 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
543 }
544 if (!empty($childResult['additionalJavaScriptSubmit'])) {
545 $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
546 $additionalJavaScriptSubmit = str_replace(array(CR, LF), '', $additionalJavaScriptSubmit);
547 $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
548 }
549 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
550 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
551 }
552 $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
553 if (!empty($childResult['requireJsModules'])) {
554 foreach ($childResult['requireJsModules'] as $module) {
555 $moduleName = null;
556 $callback = null;
557 if (is_string($module)) {
558 // if $module is a string, no callback
559 $moduleName = $module;
560 $callback = null;
561 } elseif (is_array($module)) {
562 // if $module is an array, callback is possible
563 foreach ($module as $key => $value) {
564 $moduleName = $key;
565 $callback = $value;
566 break;
567 }
568 }
569 if ($moduleName !== null) {
570 $inlineCodeKey = $moduleName;
571 $javaScriptCode = 'require(["' . $moduleName . '"]';
572 if ($callback !== null) {
573 $inlineCodeKey .= sha1($callback);
574 $javaScriptCode .= ', ' . $callback;
575 }
576 $javaScriptCode .= ');';
577 $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
578 }
579 }
580 }
581 return $jsonResult;
582 }
583
584 /**
585 * Gets an array with the uids of related records out of a list of items.
586 * This list could contain more information than required. This methods just
587 * extracts the uids.
588 *
589 * @param string $itemList The list of related child records
590 * @return array An array with uids
591 */
592 protected function getInlineRelatedRecordsUidArray($itemList)
593 {
594 $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
595 // Perform modification of the selected items array:
596 foreach ($itemArray as &$value) {
597 $parts = explode('|', $value, 2);
598 $value = $parts[0];
599 }
600 unset($value);
601 return $itemArray;
602 }
603
604 /**
605 * Checks if a record selector may select a certain file type
606 *
607 * @param array $selectorConfiguration
608 * @param array $fileRecord
609 * @return bool
610 * @todo: check this ...
611 */
612 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
613 {
614 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
615 $allowedFileExtensions = GeneralUtility::trimExplode(
616 ',',
617 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
618 true
619 );
620 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
621 return false;
622 }
623 }
624 return true;
625 }
626
627 /**
628 * Return expand / collapse state array for a given table / uid combination
629 *
630 * @param string $table Handled table
631 * @param int $uid Handled uid
632 * @return array
633 */
634 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
635 {
636 $inlineView = $this->getInlineExpandCollapseStateArray();
637 $result = array();
638 if (MathUtility::canBeInterpretedAsInteger($uid)) {
639 if (!empty($inlineView[$table][$uid])) {
640 $result = $inlineView[$table][$uid];
641 }
642 }
643 return $result;
644 }
645
646 /**
647 * Get expand / collapse state of inline items
648 *
649 * @return array
650 */
651 protected function getInlineExpandCollapseStateArray()
652 {
653 $backendUser = $this->getBackendUserAuthentication();
654 $inlineView = unserialize($backendUser->uc['inlineView']);
655 if (!is_array($inlineView)) {
656 $inlineView = array();
657 }
658 return $inlineView;
659 }
660
661 /**
662 * Remove an element from an array.
663 *
664 * @param mixed $needle The element to be removed.
665 * @param array $haystack The array the element should be removed from.
666 * @param mixed $strict Search elements strictly.
667 * @return array The array $haystack without the $needle
668 */
669 protected function removeFromArray($needle, $haystack, $strict = null)
670 {
671 $pos = array_search($needle, $haystack, $strict);
672 if ($pos !== false) {
673 unset($haystack[$pos]);
674 }
675 return $haystack;
676 }
677
678 /**
679 * Generates an error message that transferred as JSON for AJAX calls
680 *
681 * @param string $message The error message to be shown
682 * @return array The error message in a JSON array
683 */
684 protected function getErrorMessageForAJAX($message)
685 {
686 return [
687 'data' => $message,
688 'scriptCall' => [
689 'alert("' . $message . '");'
690 ],
691 ];
692 }
693
694 /**
695 * Get inlineFirstPid from a given objectId string
696 *
697 * @param string $domObjectId The id attribute of an element
698 * @return int|NULL Pid or null
699 */
700 protected function getInlineFirstPidFromDomObjectId($domObjectId)
701 {
702 // Substitute FlexForm addition and make parsing a bit easier
703 $domObjectId = str_replace('---', ':', $domObjectId);
704 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
705 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
706 if (preg_match($pattern, $domObjectId, $match)) {
707 return $match[1];
708 }
709 return null;
710 }
711
712 /**
713 * @return BackendUserAuthentication
714 */
715 protected function getBackendUserAuthentication()
716 {
717 return $GLOBALS['BE_USER'];
718 }
719 }