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