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