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