fec04f11dfd7b6da29a89c21aff682342a731fe7
[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\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\Http\JsonResponse;
27 use TYPO3\CMS\Core\Messaging\AbstractMessage;
28 use TYPO3\CMS\Core\Messaging\FlashMessageService;
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 * @return ResponseInterface
43 */
44 public function createAction(ServerRequestInterface $request): ResponseInterface
45 {
46 $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
47 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
48
49 $domObjectId = $ajaxArguments[0];
50 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
51 $childChildUid = null;
52 if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
53 $childChildUid = (int)$ajaxArguments[1];
54 }
55
56 // Parse the DOM identifier, add the levels to the structure stack
57 /** @var InlineStackProcessor $inlineStackProcessor */
58 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
59 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
60 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
61 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
62
63 // Parent, this table embeds the child table
64 $parent = $inlineStackProcessor->getStructureLevel(-1);
65
66 // Child, a record from this table should be rendered
67 $child = $inlineStackProcessor->getUnstableStructure();
68 if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
69 // If uid comes in, it is the id of the record neighbor record "create after"
70 $childVanillaUid = -1 * abs((int)$child['uid']);
71 } else {
72 // Else inline first Pid is the storage pid of new inline records
73 $childVanillaUid = (int)$inlineFirstPid;
74 }
75
76 $childTableName = $parentConfig['foreign_table'];
77
78 /** @var TcaDatabaseRecord $formDataGroup */
79 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
80 /** @var FormDataCompiler $formDataCompiler */
81 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
82 $formDataCompilerInput = [
83 'command' => 'new',
84 'tableName' => $childTableName,
85 'vanillaUid' => $childVanillaUid,
86 'isInlineChild' => true,
87 'inlineStructure' => $inlineStackProcessor->getStructure(),
88 'inlineFirstPid' => $inlineFirstPid,
89 'inlineParentUid' => $parent['uid'],
90 'inlineParentTableName' => $parent['table'],
91 'inlineParentFieldName' => $parent['field'],
92 'inlineParentConfig' => $parentConfig,
93 'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
94 'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
95 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
96 ];
97 if ($childChildUid) {
98 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
99 }
100 $childData = $formDataCompiler->compile($formDataCompilerInput);
101
102 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
103 // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
104 // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
105 // existing record of the child-child table specified by the given uid. If there is no such id, user
106 // clicked on "created new" and a new child-child should be created, too.
107 if ($childChildUid) {
108 // Fetch existing child child
109 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
110 $childChildUid,
111 ];
112 $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
113 } else {
114 /** @var TcaDatabaseRecord $formDataGroup */
115 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
116 /** @var FormDataCompiler $formDataCompiler */
117 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
118 $formDataCompilerInput = [
119 'command' => 'new',
120 'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
121 'vanillaUid' => (int)$inlineFirstPid,
122 'isInlineChild' => true,
123 'isInlineAjaxOpeningContext' => true,
124 'inlineStructure' => $inlineStackProcessor->getStructure(),
125 'inlineFirstPid' => (int)$inlineFirstPid,
126 ];
127 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
128 }
129 }
130
131 $childData['inlineParentUid'] = (int)$parent['uid'];
132 $childData['renderType'] = 'inlineRecordContainer';
133 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
134 $childResult = $nodeFactory->create($childData)->render();
135
136 $jsonArray = [
137 'data' => '',
138 'stylesheetFiles' => [],
139 'scriptCall' => [],
140 ];
141
142 // The HTML-object-id's prefix of the dynamically created record
143 $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
144 $objectPrefix = $objectName . '-' . $child['table'];
145 $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
146 $expandSingle = $parentConfig['appearance']['expandSingle'];
147 if (!$child['uid']) {
148 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
149 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
150 } else {
151 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
152 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
153 }
154 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
155 if ($parentConfig['appearance']['useSortable']) {
156 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
157 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
158 }
159 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
160 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
161 }
162 // Fade out and fade in the new record in the browser view to catch the user's eye
163 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
164
165 return GeneralUtility::makeInstance(JsonResponse::class, $jsonArray);
166 }
167
168 /**
169 * Show the details of a child record.
170 *
171 * @param ServerRequestInterface $request
172 * @return ResponseInterface
173 */
174 public function detailsAction(ServerRequestInterface $request): ResponseInterface
175 {
176 $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
177
178 $domObjectId = $ajaxArguments[0];
179 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
180 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
181
182 // Parse the DOM identifier, add the levels to the structure stack
183 /** @var InlineStackProcessor $inlineStackProcessor */
184 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
185 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
186 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
187
188 // Parent, this table embeds the child table
189 $parent = $inlineStackProcessor->getStructureLevel(-1);
190 $parentFieldName = $parent['field'];
191
192 // Set flag in config so that only the fields are rendered
193 // @todo: Solve differently / rename / whatever
194 $parentConfig['renderFieldsOnly'] = true;
195
196 $parentData = [
197 'processedTca' => [
198 'columns' => [
199 $parentFieldName => [
200 'config' => $parentConfig,
201 ],
202 ],
203 ],
204 'tableName' => $parent['table'],
205 'inlineFirstPid' => $inlineFirstPid,
206 // Hand over given original return url to compile stack. Needed if inline children compile links to
207 // another view (eg. edit metadata in a nested inline situation like news with inline content element image),
208 // so the back link is still the link from the original request. See issue #82525. This is additionally
209 // given down in TcaInline data provider to compiled children data.
210 'returnUrl' => $parentConfig['originalReturnUrl'],
211 ];
212
213 // Child, a record from this table should be rendered
214 $child = $inlineStackProcessor->getUnstableStructure();
215
216 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
217
218 $childData['inlineParentUid'] = (int)$parent['uid'];
219 $childData['renderType'] = 'inlineRecordContainer';
220 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
221 $childResult = $nodeFactory->create($childData)->render();
222
223 $jsonArray = [
224 'data' => '',
225 'stylesheetFiles' => [],
226 'scriptCall' => [],
227 ];
228
229 // The HTML-object-id's prefix of the dynamically created record
230 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
231 $objectId = $objectPrefix . '-' . (int)$child['uid'];
232 $expandSingle = $parentConfig['appearance']['expandSingle'];
233 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
234 if ($parentConfig['foreign_unique']) {
235 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
236 }
237 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
238 if ($parentConfig['appearance']['useSortable']) {
239 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
240 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
241 }
242 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
243 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
244 }
245
246 return GeneralUtility::makeInstance(JsonResponse::class, $jsonArray);
247 }
248
249 /**
250 * Adds localizations or synchronizes the locations of all child records.
251 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
252 *
253 * @param ServerRequestInterface $request the incoming request
254 * @return ResponseInterface the filled response
255 */
256 public function synchronizeLocalizeAction(ServerRequestInterface $request): ResponseInterface
257 {
258 $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
259 $domObjectId = $ajaxArguments[0];
260 $type = $ajaxArguments[1];
261 $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
262
263 /** @var InlineStackProcessor $inlineStackProcessor */
264 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
265 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
266 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
267 $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
268 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
269
270 $jsonArray = [
271 'data' => '',
272 'stylesheetFiles' => [],
273 'scriptCall' => [],
274 ];
275 if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
276 // Parent, this table embeds the child table
277 $parent = $inlineStackProcessor->getStructureLevel(-1);
278 $parentFieldName = $parent['field'];
279
280 $processedTca = $GLOBALS['TCA'][$parent['table']];
281 $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
282
283 // Child, a record from this table should be rendered
284 $child = $inlineStackProcessor->getUnstableStructure();
285
286 $formDataCompilerInputForParent = [
287 'vanillaUid' => (int)$parent['uid'],
288 'command' => 'edit',
289 'tableName' => $parent['table'],
290 'processedTca' => $processedTca,
291 'inlineFirstPid' => $inlineFirstPid,
292 'columnsToProcess' => [
293 $parentFieldName
294 ],
295 // @todo: still needed? NO!
296 'inlineStructure' => $inlineStackProcessor->getStructure(),
297 // Do not compile existing children, we don't need them now
298 'inlineCompileExistingChildren' => false,
299 ];
300 // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
301 /** @var TcaDatabaseRecord $formDataGroup */
302 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
303 /** @var FormDataCompiler $formDataCompiler */
304 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
305 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
306 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
307 $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
308 $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
309 $oldItemList = $parentData['databaseRow'][$parentFieldName];
310
311 // DataHandler cannot handle arrays as field value
312 if (is_array($parentLanguage)) {
313 $parentLanguage = implode(',', $parentLanguage);
314 }
315
316 $cmd = [];
317 // Localize a single child element from default language of the parent element
318 if (MathUtility::canBeInterpretedAsInteger($type)) {
319 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
320 'field' => $parent['field'],
321 'language' => $parentLanguage,
322 'ids' => [$type],
323 ];
324 } else {
325 // Either localize or synchronize all child elements from default language of the parent element
326 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
327 'field' => $parent['field'],
328 'language' => $parentLanguage,
329 'action' => $type,
330 ];
331 }
332
333 /** @var $tce DataHandler */
334 $tce = GeneralUtility::makeInstance(DataHandler::class);
335 $tce->start([], $cmd);
336 $tce->process_cmdmap();
337
338 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
339
340 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
341 $nameObjectForeignTable = $nameObject . '-' . $child['table'];
342
343 $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
344 $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
345
346 // Render error messages from DataHandler
347 $tce->printLogErrorMessages();
348 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
349 $messages = $flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush();
350 if (!empty($messages)) {
351 foreach ($messages as $message) {
352 $jsonArray['messages'][] = [
353 'title' => $message->getTitle(),
354 'message' => $message->getMessage(),
355 'severity' => $message->getSeverity()
356 ];
357 if ($message->getSeverity() === AbstractMessage::ERROR) {
358 $jsonArray['hasErrors'] = true;
359 }
360 }
361 }
362
363 // Set the items that should be removed in the forms view:
364 $removedItems = array_diff($oldItems, $newItems);
365 foreach ($removedItems as $childUid) {
366 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
367 }
368
369 $localizedItems = array_diff($newItems, $oldItems);
370 foreach ($localizedItems as $childUid) {
371 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
372
373 $childData['inlineParentUid'] = (int)$parent['uid'];
374 $childData['renderType'] = 'inlineRecordContainer';
375 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
376 $childResult = $nodeFactory->create($childData)->render();
377
378 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
379
380 // Get the name of the field used as foreign selector (if any):
381 $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
382 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
383 if (is_array($selectedValue)) {
384 $selectedValue = $selectedValue[0];
385 }
386 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
387 // Remove possible virtual records in the form which showed that a child records could be localized:
388 $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
389 if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
390 $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
391 if (is_array($transOrigPointerField)) {
392 $transOrigPointerField = $transOrigPointerField[0];
393 }
394 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
395 }
396 }
397 // Tell JS to add new HTML of one or multiple (localize all) records to DOM
398 if (!empty($jsonArray['data'])) {
399 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
400 . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
401 . ', json.data);';
402 }
403 }
404 return GeneralUtility::makeInstance(JsonResponse::class)->setPayload($jsonArray);
405 }
406
407 /**
408 * Store status of inline children expand / collapse state in backend user uC.
409 *
410 * @param ServerRequestInterface $request the incoming request
411 * @return ResponseInterface the filled response
412 */
413 public function expandOrCollapseAction(ServerRequestInterface $request): ResponseInterface
414 {
415 $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
416 $domObjectId = $ajaxArguments[0];
417
418 /** @var InlineStackProcessor $inlineStackProcessor */
419 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
420 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
421 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
422 $expand = $ajaxArguments[1];
423 $collapse = $ajaxArguments[2];
424
425 $backendUser = $this->getBackendUserAuthentication();
426 // The current table - for this table we should add/import records
427 $currentTable = $inlineStackProcessor->getUnstableStructure();
428 $currentTable = $currentTable['table'];
429 // The top parent table - this table embeds the current table
430 $top = $inlineStackProcessor->getStructureLevel(0);
431 $topTable = $top['table'];
432 $topUid = $top['uid'];
433 $inlineView = $this->getInlineExpandCollapseStateArray();
434 // Only do some action if the top record and the current record were saved before
435 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
436 $expandUids = GeneralUtility::trimExplode(',', $expand);
437 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
438 // Set records to be expanded
439 foreach ($expandUids as $uid) {
440 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
441 }
442 // Set records to be collapsed
443 foreach ($collapseUids as $uid) {
444 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
445 }
446 // Save states back to database
447 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
448 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
449 $backendUser->uc['inlineView'] = serialize($inlineView);
450 $backendUser->writeUC();
451 }
452 }
453 return GeneralUtility::makeInstance(JsonResponse::class)->setPayload([]);
454 }
455
456 /**
457 * Compile a full child record
458 *
459 * @param array $parentData Result array of parent
460 * @param string $parentFieldName Name of parent field
461 * @param int $childUid Uid of child to compile
462 * @param array $inlineStructure Current inline structure
463 * @return array Full result array
464 *
465 * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
466 * @todo: to also encapsulate the more complex scenarios with combination child and friends.
467 */
468 protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
469 {
470 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
471
472 /** @var InlineStackProcessor $inlineStackProcessor */
473 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
474 $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
475 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
476
477 // @todo: do not use stack processor here ...
478 $child = $inlineStackProcessor->getUnstableStructure();
479 $childTableName = $child['table'];
480
481 /** @var TcaDatabaseRecord $formDataGroup */
482 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
483 /** @var FormDataCompiler $formDataCompiler */
484 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
485 $formDataCompilerInput = [
486 'command' => 'edit',
487 'tableName' => $childTableName,
488 'vanillaUid' => (int)$childUid,
489 'returnUrl' => $parentData['returnUrl'],
490 'isInlineChild' => true,
491 'inlineStructure' => $inlineStructure,
492 'inlineFirstPid' => $parentData['inlineFirstPid'],
493 'inlineParentConfig' => $parentConfig,
494 'isInlineAjaxOpeningContext' => true,
495
496 // values of the current parent element
497 // it is always a string either an id or new...
498 'inlineParentUid' => $parentData['databaseRow']['uid'],
499 'inlineParentTableName' => $parentData['tableName'],
500 'inlineParentFieldName' => $parentFieldName,
501
502 // values of the top most parent element set on first level and not overridden on following levels
503 'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
504 'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
505 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
506 ];
507 // For foreign_selector with useCombination $mainChild is the mm record
508 // and $combinationChild is the child-child. For "normal" relations, $mainChild
509 // is just the normal child record and $combinationChild is empty.
510 $mainChild = $formDataCompiler->compile($formDataCompilerInput);
511 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
512 // This kicks in if opening an existing mainChild that has a child-child set
513 $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
514 }
515 return $mainChild;
516 }
517
518 /**
519 * With useCombination set, not only content of the intermediate table, but also
520 * the connected child should be rendered in one go. Prepare this here.
521 *
522 * @param array $child Full data array of "mm" record
523 * @param array $parentConfig TCA configuration of "parent"
524 * @param array $inlineStructure Current inline structure
525 * @return array Full data array of child
526 */
527 protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
528 {
529 // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
530 $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
531 // child-child table name is set in child tca "the selector field" foreign_table
532 $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
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' => $childChildTableName,
540 'vanillaUid' => (int)$childChildUid,
541 'isInlineChild' => true,
542 'isInlineAjaxOpeningContext' => true,
543 // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
544 'inlineStructure' => $inlineStructure,
545 'inlineFirstPid' => $child['inlineFirstPid'],
546 // values of the top most parent element set on first level and not overridden on following levels
547 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
548 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
549 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
550 ];
551 return $formDataCompiler->compile($formDataCompilerInput);
552 }
553
554 /**
555 * Merge stuff from child array into json array.
556 * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
557 *
558 * @param array $jsonResult Given json result
559 * @param array $childResult Given child result
560 * @return array Merged json array
561 */
562 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
563 {
564 $jsonResult['data'] .= $childResult['html'];
565 $jsonResult['stylesheetFiles'] = [];
566 foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
567 $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
568 }
569 if (!empty($childResult['inlineData'])) {
570 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
571 }
572 if (!empty($childResult['additionalJavaScriptSubmit'])) {
573 $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
574 $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
575 $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
576 }
577 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
578 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
579 }
580 if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
581 $labels = [];
582 foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
583 ArrayUtility::mergeRecursiveWithOverrule(
584 $labels,
585 $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
586 );
587 }
588 $javaScriptCode = [];
589 $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
590 $javaScriptCode[] = ' TYPO3.lang = {}';
591 $javaScriptCode[] = '}';
592 $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
593 $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
594 $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
595 $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
596 $javaScriptCode[] = ' }';
597 $javaScriptCode[] = '}';
598
599 $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
600 }
601 $requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
602 $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
603
604 return $jsonResult;
605 }
606
607 /**
608 * Gets an array with the uids of related records out of a list of items.
609 * This list could contain more information than required. This methods just
610 * extracts the uids.
611 *
612 * @param string $itemList The list of related child records
613 * @return array An array with uids
614 */
615 protected function getInlineRelatedRecordsUidArray($itemList)
616 {
617 $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
618 // Perform modification of the selected items array:
619 foreach ($itemArray as &$value) {
620 $parts = explode('|', $value, 2);
621 $value = $parts[0];
622 }
623 unset($value);
624 return $itemArray;
625 }
626
627 /**
628 * Return expand / collapse state array for a given table / uid combination
629 *
630 * @param string $table Handled table
631 * @param int $uid Handled uid
632 * @return array
633 */
634 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
635 {
636 $inlineView = $this->getInlineExpandCollapseStateArray();
637 $result = [];
638 if (MathUtility::canBeInterpretedAsInteger($uid)) {
639 if (!empty($inlineView[$table][$uid])) {
640 $result = $inlineView[$table][$uid];
641 }
642 }
643 return $result;
644 }
645
646 /**
647 * Get expand / collapse state of inline items
648 *
649 * @return array
650 */
651 protected function getInlineExpandCollapseStateArray()
652 {
653 $backendUser = $this->getBackendUserAuthentication();
654 if (!$this->backendUserHasUcInlineView($backendUser)) {
655 return [];
656 }
657
658 $inlineView = unserialize($backendUser->uc['inlineView']);
659 if (!is_array($inlineView)) {
660 $inlineView = [];
661 }
662
663 return $inlineView;
664 }
665
666 /**
667 * Method to check whether the backend user has the property inline view for the current IRRE item.
668 * In existing or old IRRE items the attribute may not exist, then the unserialize will fail.
669 *
670 * @param BackendUserAuthentication $backendUser
671 * @return bool
672 */
673 protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
674 {
675 return !empty($backendUser->uc['inlineView']);
676 }
677
678 /**
679 * Remove an element from an array.
680 *
681 * @param mixed $needle The element to be removed.
682 * @param array $haystack The array the element should be removed from.
683 * @param bool $strict Search elements strictly.
684 * @return array The array $haystack without the $needle
685 */
686 protected function removeFromArray($needle, $haystack, $strict = false)
687 {
688 $pos = array_search($needle, $haystack, $strict);
689 if ($pos !== false) {
690 unset($haystack[$pos]);
691 }
692 return $haystack;
693 }
694
695 /**
696 * Generates an error message that transferred as JSON for AJAX calls
697 *
698 * @param string $message The error message to be shown
699 * @return array The error message in a JSON array
700 */
701 protected function getErrorMessageForAJAX($message)
702 {
703 return [
704 'data' => $message,
705 'scriptCall' => [
706 'alert("' . $message . '");'
707 ],
708 ];
709 }
710
711 /**
712 * Get inlineFirstPid from a given objectId string
713 *
714 * @param string $domObjectId The id attribute of an element
715 * @return int|null Pid or null
716 */
717 protected function getInlineFirstPidFromDomObjectId($domObjectId)
718 {
719 // Substitute FlexForm addition and make parsing a bit easier
720 $domObjectId = str_replace('---', ':', $domObjectId);
721 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
722 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
723 if (preg_match($pattern, $domObjectId, $match)) {
724 return $match[1];
725 }
726 return null;
727 }
728
729 /**
730 * Validates the config that is transferred over the wire to provide the
731 * correct TCA config for the parent table
732 *
733 * @param string $contextString
734 * @throws \RuntimeException
735 * @return array
736 */
737 protected function extractSignedParentConfigFromRequest(string $contextString): array
738 {
739 if ($contextString === '') {
740 throw new \RuntimeException('Empty context string given', 1489751361);
741 }
742 $context = json_decode($contextString, true);
743 if (empty($context['config'])) {
744 throw new \RuntimeException('Empty context config section given', 1489751362);
745 }
746 if (!hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) {
747 throw new \RuntimeException('Hash does not validate', 1489751363);
748 }
749 return $context['config'];
750 }
751
752 /**
753 * @return BackendUserAuthentication
754 */
755 protected function getBackendUserAuthentication()
756 {
757 return $GLOBALS['BE_USER'];
758 }
759 }