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