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