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