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