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