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