[BUGFIX] IRRE: Check if TCA['ctrl']['languageField'] is set
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / FormInlineAjaxController.php
1 <?php
2 namespace TYPO3\CMS\Backend\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Form\FormDataCompiler;
20 use TYPO3\CMS\Backend\Form\FormDataGroup\InlineParentRecord;
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\Backend\Utility\BackendUtility;
25 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
26 use TYPO3\CMS\Core\DataHandling\DataHandler;
27 use TYPO3\CMS\Core\Localization\LocalizationFactory;
28 use TYPO3\CMS\Core\Utility\ArrayUtility;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Core\Utility\MathUtility;
31
32 /**
33 * Handle FormEngine inline ajax calls
34 */
35 class FormInlineAjaxController
36 {
37 /**
38 * Create a new inline child via AJAX.
39 *
40 * @param ServerRequestInterface $request
41 * @param ResponseInterface $response
42 * @return ResponseInterface
43 */
44 public function createAction(ServerRequestInterface $request, ResponseInterface $response)
45 {
46 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
47
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($ajaxArguments['context']);
60
61 // Parent, this table embeds the child table
62 $parent = $inlineStackProcessor->getStructureLevel(-1);
63 $parentFieldName = $parent['field'];
64
65 if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
66 $command = 'edit';
67 $vanillaUid = (int)$parent['uid'];
68 $databaseRow = [
69 // TcaInlineExpandCollapseState needs the record uid
70 'uid' => (int)$parent['uid'],
71 ];
72 } else {
73 $command = 'new';
74 $databaseRow = [];
75 $vanillaUid = (int)$inlineFirstPid;
76 }
77 $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
78
79 $formDataCompilerInputForParent = [
80 'vanillaUid' => $vanillaUid,
81 'command' => $command,
82 'tableName' => $parent['table'],
83 'databaseRow' => $databaseRow,
84 'inlineFirstPid' => $inlineFirstPid,
85 'columnsToProcess' => array_merge(
86 [$parentFieldName],
87 array_keys($databaseRow)
88 ),
89 // Do not resolve existing children, we don't need them now
90 'inlineResolveExistingChildren' => false,
91 ];
92 /** @var TcaDatabaseRecord $formDataGroup */
93 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
94 /** @var FormDataCompiler $formDataCompiler */
95 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
96 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
97 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
98
99 // Child, a record from this table should be rendered
100 $child = $inlineStackProcessor->getUnstableStructure();
101 if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
102 // If uid comes in, it is the id of the record neighbor record "create after"
103 $childVanillaUid = -1 * abs((int)$child['uid']);
104 } else {
105 // Else inline first Pid is the storage pid of new inline records
106 $childVanillaUid = (int)$inlineFirstPid;
107 }
108
109 if ($parentConfig['type'] === 'flex') {
110 $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
111 }
112 $childTableName = $parentConfig['foreign_table'];
113
114 /** @var TcaDatabaseRecord $formDataGroup */
115 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
116 /** @var FormDataCompiler $formDataCompiler */
117 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
118 $formDataCompilerInput = [
119 'command' => 'new',
120 'tableName' => $childTableName,
121 'vanillaUid' => $childVanillaUid,
122 'isInlineChild' => true,
123 'inlineStructure' => $inlineStackProcessor->getStructure(),
124 'inlineFirstPid' => $inlineFirstPid,
125 'inlineParentUid' => $parent['uid'],
126 'inlineParentTableName' => $parent['table'],
127 'inlineParentFieldName' => $parent['field'],
128 'inlineParentConfig' => $parentConfig,
129 ];
130 if ($childChildUid) {
131 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
132 }
133 $childData = $formDataCompiler->compile($formDataCompilerInput);
134
135 // Set language of new child record to the language of the parent record
136 if ($parent['localizationMode'] !== 'keep' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
137 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
138 $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
139 if (!empty($parentLanguageField) && !empty($childLanguageField)) {
140 $parentRecord = BackendUtility::getRecord($parent['table'], $parent['uid']);
141 $childData['databaseRow'][$childLanguageField][0] = $parentRecord[$parentLanguageField];
142 }
143 }
144
145 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
146 // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
147 // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
148 // existing record of the child-child table specified by the given uid. If there is no such id, user
149 // clicked on "created new" and a new child-child should be created, too.
150 if ($childChildUid) {
151 // Fetch existing child child
152 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
153 $childChildUid,
154 ];
155 $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
156 } else {
157 /** @var TcaDatabaseRecord $formDataGroup */
158 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
159 /** @var FormDataCompiler $formDataCompiler */
160 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
161 $formDataCompilerInput = [
162 'command' => 'new',
163 'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
164 'vanillaUid' => (int)$inlineFirstPid,
165 'isInlineChild' => true,
166 'isInlineAjaxOpeningContext' => true,
167 'inlineStructure' => $inlineStackProcessor->getStructure(),
168 'inlineFirstPid' => (int)$inlineFirstPid,
169 ];
170 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
171 }
172 }
173
174 $childData['inlineParentUid'] = (int)$parent['uid'];
175 $childData['renderType'] = 'inlineRecordContainer';
176 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
177 $childResult = $nodeFactory->create($childData)->render();
178
179 $jsonArray = [
180 'data' => '',
181 'stylesheetFiles' => [],
182 'scriptCall' => [],
183 ];
184
185 // The HTML-object-id's prefix of the dynamically created record
186 $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
187 $objectPrefix = $objectName . '-' . $child['table'];
188 $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
189 $expandSingle = $parentConfig['appearance']['expandSingle'];
190 if (!$child['uid']) {
191 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
192 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
193 } else {
194 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
195 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
196 }
197 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
198 if ($parentConfig['appearance']['useSortable']) {
199 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
200 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
201 }
202 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
203 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
204 }
205 // Fade out and fade in the new record in the browser view to catch the user's eye
206 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
207
208 $response->getBody()->write(json_encode($jsonArray));
209
210 return $response;
211 }
212
213 /**
214 * Show the details of a child record.
215 *
216 * @param ServerRequestInterface $request
217 * @param ResponseInterface $response
218 * @return ResponseInterface
219 */
220 public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
221 {
222 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
223
224 $domObjectId = $ajaxArguments[0];
225 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
226
227 // Parse the DOM identifier, add the levels to the structure stack
228 /** @var InlineStackProcessor $inlineStackProcessor */
229 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
230 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
231 $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
232
233 // Parent, this table embeds the child table
234 $parent = $inlineStackProcessor->getStructureLevel(-1);
235 $parentFieldName = $parent['field'];
236
237 $databaseRow = [
238 // TcaInlineExpandCollapseState needs this
239 'uid' => (int)$parent['uid'],
240 ];
241
242 $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
243
244 $formDataCompilerInputForParent = [
245 'vanillaUid' => (int)$parent['uid'],
246 'command' => 'edit',
247 'tableName' => $parent['table'],
248 'databaseRow' => $databaseRow,
249 'inlineFirstPid' => $inlineFirstPid,
250 'columnsToProcess' => array_merge(
251 [$parentFieldName],
252 array_keys($databaseRow)
253 ),
254 // @todo: still needed?
255 'inlineStructure' => $inlineStackProcessor->getStructure(),
256 // Do not resolve existing children, we don't need them now
257 'inlineResolveExistingChildren' => false,
258 ];
259 /** @var TcaDatabaseRecord $formDataGroup */
260 $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
261 /** @var FormDataCompiler $formDataCompiler */
262 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
263 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
264 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
265
266 if ($parentConfig['type'] === 'flex') {
267 $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
268 $parentData['processedTca']['columns'][$parentFieldName]['config'] = $parentConfig;
269 }
270
271 // Set flag in config so that only the fields are rendered
272 // @todo: Solve differently / rename / whatever
273 $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
274
275 // Child, a record from this table should be rendered
276 $child = $inlineStackProcessor->getUnstableStructure();
277
278 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
279
280 $childData['inlineParentUid'] = (int)$parent['uid'];
281 $childData['renderType'] = 'inlineRecordContainer';
282 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
283 $childResult = $nodeFactory->create($childData)->render();
284
285 $jsonArray = [
286 'data' => '',
287 'stylesheetFiles' => [],
288 'scriptCall' => [],
289 ];
290
291 // The HTML-object-id's prefix of the dynamically created record
292 $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
293 $objectId = $objectPrefix . '-' . (int)$child['uid'];
294 $expandSingle = $parentConfig['appearance']['expandSingle'];
295 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
296 if ($parentConfig['foreign_unique']) {
297 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
298 }
299 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
300 if ($parentConfig['appearance']['useSortable']) {
301 $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
302 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
303 }
304 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
305 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
306 }
307
308 $response->getBody()->write(json_encode($jsonArray));
309
310 return $response;
311 }
312
313 /**
314 * Adds localizations or synchronizes the locations of all child records.
315 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
316 *
317 * @param ServerRequestInterface $request the incoming request
318 * @param ResponseInterface $response the empty response
319 * @return ResponseInterface the filled response
320 */
321 public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
322 {
323 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
324 $domObjectId = $ajaxArguments[0];
325 $type = $ajaxArguments[1];
326
327 /** @var InlineStackProcessor $inlineStackProcessor */
328 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
329 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
330 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
331 $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
332 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
333
334 $jsonArray = false;
335 if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
336 // Parent, this table embeds the child table
337 $parent = $inlineStackProcessor->getStructureLevel(-1);
338 $parentFieldName = $parent['field'];
339
340 // Child, a record from this table should be rendered
341 $child = $inlineStackProcessor->getUnstableStructure();
342
343 $formDataCompilerInputForParent = [
344 'vanillaUid' => (int)$parent['uid'],
345 'command' => 'edit',
346 'tableName' => $parent['table'],
347 'inlineFirstPid' => $inlineFirstPid,
348 'columnsToProcess' => [
349 $parentFieldName
350 ],
351 // @todo: still needed? NO!
352 'inlineStructure' => $inlineStackProcessor->getStructure(),
353 // Do not compile existing children, we don't need them now
354 'inlineCompileExistingChildren' => false,
355 ];
356 // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
357 /** @var TcaDatabaseRecord $formDataGroup */
358 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
359 /** @var FormDataCompiler $formDataCompiler */
360 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
361 $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
362 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
363 $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
364 $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
365 $oldItemList = $parentData['databaseRow'][$parentFieldName];
366
367 // DataHandler cannot handle arrays as field value
368 if (is_array($parentLanguage)) {
369 $parentLanguage = implode(',', $parentLanguage);
370 }
371
372 $cmd = [];
373 // Localize a single child element from default language of the parent element
374 if (MathUtility::canBeInterpretedAsInteger($type)) {
375 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
376 'field' => $parent['field'],
377 'language' => $parentLanguage,
378 'ids' => [$type],
379 ];
380 // Either localize or synchronize all child elements from default language of the parent element
381 } else {
382 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
383 'field' => $parent['field'],
384 'language' => $parentLanguage,
385 'action' => $type,
386 ];
387 }
388
389 /** @var $tce DataHandler */
390 $tce = GeneralUtility::makeInstance(DataHandler::class);
391 $tce->stripslashes_values = false;
392 $tce->start([], $cmd);
393 $tce->process_cmdmap();
394
395 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
396
397 $jsonArray = [
398 'data' => '',
399 'stylesheetFiles' => [],
400 'scriptCall' => [],
401 ];
402 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
403 $nameObjectForeignTable = $nameObject . '-' . $child['table'];
404
405 $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
406 $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
407
408 // Set the items that should be removed in the forms view:
409 $removedItems = array_diff($oldItems, $newItems);
410 foreach ($removedItems as $childUid) {
411 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
412 }
413
414 $localizedItems = array_diff($newItems, $oldItems);
415 foreach ($localizedItems as $childUid) {
416 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
417
418 $childData['inlineParentUid'] = (int)$parent['uid'];
419 $childData['renderType'] = 'inlineRecordContainer';
420 $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
421 $childResult = $nodeFactory->create($childData)->render();
422
423 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
424
425 // Get the name of the field used as foreign selector (if any):
426 $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
427 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
428 if (is_array($selectedValue)) {
429 $selectedValue = $selectedValue[0];
430 }
431 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
432 // Remove possible virtual records in the form which showed that a child records could be localized:
433 $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
434 if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
435 $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
436 if (is_array($transOrigPointerField)) {
437 $transOrigPointerField = $transOrigPointerField[0];
438 }
439 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
440 }
441 }
442 // Tell JS to add new HTML of one or multiple (localize all) records to DOM
443 if (!empty($jsonArray['data'])) {
444 array_push(
445 $jsonArray['scriptCall'],
446 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
447 . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
448 . ', json.data);'
449 );
450 }
451 }
452
453 $response->getBody()->write(json_encode($jsonArray));
454
455 return $response;
456 }
457
458 /**
459 * Adds localizations or synchronizes the locations of all child records.
460 *
461 * @param ServerRequestInterface $request the incoming request
462 * @param ResponseInterface $response the empty response
463 * @return ResponseInterface the filled response
464 */
465 public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
466 {
467 $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
468 $domObjectId = $ajaxArguments[0];
469
470 /** @var InlineStackProcessor $inlineStackProcessor */
471 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
472 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
473 $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
474 $expand = $ajaxArguments[1];
475 $collapse = $ajaxArguments[2];
476
477 $backendUser = $this->getBackendUserAuthentication();
478 // The current table - for this table we should add/import records
479 $currentTable = $inlineStackProcessor->getUnstableStructure();
480 $currentTable = $currentTable['table'];
481 // The top parent table - this table embeds the current table
482 $top = $inlineStackProcessor->getStructureLevel(0);
483 $topTable = $top['table'];
484 $topUid = $top['uid'];
485 $inlineView = $this->getInlineExpandCollapseStateArray();
486 // Only do some action if the top record and the current record were saved before
487 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
488 $expandUids = GeneralUtility::trimExplode(',', $expand);
489 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
490 // Set records to be expanded
491 foreach ($expandUids as $uid) {
492 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
493 }
494 // Set records to be collapsed
495 foreach ($collapseUids as $uid) {
496 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
497 }
498 // Save states back to database
499 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
500 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
501 $backendUser->uc['inlineView'] = serialize($inlineView);
502 $backendUser->writeUC();
503 }
504 }
505
506 $response->getBody()->write(json_encode([]));
507 return $response;
508 }
509
510 /**
511 * Compile a full child record
512 *
513 * @param array $parentData Result array of parent
514 * @param string $parentFieldName Name of parent field
515 * @param int $childUid Uid of child to compile
516 * @param array $inlineStructure Current inline structure
517 * @return array Full result array
518 *
519 * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
520 * @todo: to also encapsulate the more complex scenarios with combination child and friends.
521 */
522 protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
523 {
524 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
525
526 /** @var InlineStackProcessor $inlineStackProcessor */
527 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
528 $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
529 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
530
531 // @todo: do not use stack processor here ...
532 $child = $inlineStackProcessor->getUnstableStructure();
533 $childTableName = $child['table'];
534
535 /** @var TcaDatabaseRecord $formDataGroup */
536 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
537 /** @var FormDataCompiler $formDataCompiler */
538 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
539 $formDataCompilerInput = [
540 'command' => 'edit',
541 'tableName' => $childTableName,
542 'vanillaUid' => (int)$childUid,
543 'isInlineChild' => true,
544 'inlineStructure' => $inlineStructure,
545 'inlineFirstPid' => $parentData['inlineFirstPid'],
546 'inlineParentConfig' => $parentConfig,
547 'isInlineAjaxOpeningContext' => true,
548
549 // values of the current parent element
550 // it is always a string either an id or new...
551 'inlineParentUid' => $parentData['databaseRow']['uid'],
552 'inlineParentTableName' => $parentData['tableName'],
553 'inlineParentFieldName' => $parentFieldName,
554
555 // values of the top most parent element set on first level and not overridden on following levels
556 'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
557 'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
558 'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
559 ];
560 // For foreign_selector with useCombination $mainChild is the mm record
561 // and $combinationChild is the child-child. For "normal" relations, $mainChild
562 // is just the normal child record and $combinationChild is empty.
563 $mainChild = $formDataCompiler->compile($formDataCompilerInput);
564 if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
565 // This kicks in if opening an existing mainChild that has a child-child set
566 $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
567 }
568 return $mainChild;
569 }
570
571 /**
572 * With useCombination set, not only content of the intermediate table, but also
573 * the connected child should be rendered in one go. Prepare this here.
574 *
575 * @param array $child Full data array of "mm" record
576 * @param array $parentConfig TCA configuration of "parent"
577 * @param array $inlineStructure Current inline structure
578 * @return array Full data array of child
579 */
580 protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
581 {
582 // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
583 $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
584 // child-child table name is set in child tca "the selector field" foreign_table
585 $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
586 /** @var TcaDatabaseRecord $formDataGroup */
587 $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
588 /** @var FormDataCompiler $formDataCompiler */
589 $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
590 $formDataCompilerInput = [
591 'command' => 'edit',
592 'tableName' => $childChildTableName,
593 'vanillaUid' => (int)$childChildUid,
594 'isInlineChild' => true,
595 'isInlineAjaxOpeningContext' => true,
596 // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
597 'inlineStructure' => $inlineStructure,
598 'inlineFirstPid' => $child['inlineFirstPid'],
599 // values of the top most parent element set on first level and not overridden on following levels
600 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
601 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
602 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
603 ];
604 return $formDataCompiler->compile($formDataCompilerInput);
605 }
606
607 /**
608 * Merge stuff from child array into json array.
609 * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
610 *
611 * @param array $jsonResult Given json result
612 * @param array $childResult Given child result
613 * @return array Merged json array
614 */
615 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
616 {
617 $jsonResult['data'] .= $childResult['html'];
618 $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
619 if (!empty($childResult['inlineData'])) {
620 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
621 }
622 if (!empty($childResult['additionalJavaScriptSubmit'])) {
623 $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
624 $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
625 $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
626 }
627 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
628 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
629 }
630 $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
631 if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
632 $labels = [];
633 foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
634 ArrayUtility::mergeRecursiveWithOverrule(
635 $labels,
636 $this->addInlineLanguageLabelFile($additionalInlineLanguageLabelFile)
637 );
638 }
639 $javaScriptCode = [];
640 $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
641 $javaScriptCode[] = ' TYPO3.lang = {}';
642 $javaScriptCode[] = '}';
643 $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
644 $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
645 $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
646 $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
647 $javaScriptCode[] = ' }';
648 $javaScriptCode[] = '}';
649
650 $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
651 }
652 if (!empty($childResult['requireJsModules'])) {
653 foreach ($childResult['requireJsModules'] as $module) {
654 $moduleName = null;
655 $callback = null;
656 if (is_string($module)) {
657 // if $module is a string, no callback
658 $moduleName = $module;
659 $callback = null;
660 } elseif (is_array($module)) {
661 // if $module is an array, callback is possible
662 foreach ($module as $key => $value) {
663 $moduleName = $key;
664 $callback = $value;
665 break;
666 }
667 }
668 if ($moduleName !== null) {
669 $inlineCodeKey = $moduleName;
670 $javaScriptCode = 'require(["' . $moduleName . '"]';
671 if ($callback !== null) {
672 $inlineCodeKey .= sha1($callback);
673 $javaScriptCode .= ', ' . $callback;
674 }
675 $javaScriptCode .= ');';
676 $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
677 }
678 }
679 }
680 return $jsonResult;
681 }
682
683 /**
684 * @param string $file
685 *
686 * @return array
687 */
688 protected function addInlineLanguageLabelFile($file)
689 {
690 /** @var $languageFactory LocalizationFactory */
691 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
692 $language = $GLOBALS['LANG']->lang;
693 $localizationArray = $languageFactory->getParsedData(
694 $file,
695 $language,
696 'utf-8',
697 1
698 );
699 if (is_array($localizationArray) && !empty($localizationArray)) {
700 if (!empty($localizationArray[$language])) {
701 $xlfLabelArray = $localizationArray['default'];
702 ArrayUtility::mergeRecursiveWithOverrule($xlfLabelArray, $localizationArray[$language], true, false);
703 } else {
704 $xlfLabelArray = $localizationArray['default'];
705 }
706 } else {
707 $xlfLabelArray = [];
708 }
709 $labelArray = [];
710 foreach ($xlfLabelArray as $key => $value) {
711 if (isset($value[0]['target'])) {
712 $labelArray[$key] = $value[0]['target'];
713 } else {
714 $labelArray[$key] = '';
715 }
716 }
717 return $labelArray;
718 }
719
720 /**
721 * Gets an array with the uids of related records out of a list of items.
722 * This list could contain more information than required. This methods just
723 * extracts the uids.
724 *
725 * @param string $itemList The list of related child records
726 * @return array An array with uids
727 */
728 protected function getInlineRelatedRecordsUidArray($itemList)
729 {
730 $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
731 // Perform modification of the selected items array:
732 foreach ($itemArray as &$value) {
733 $parts = explode('|', $value, 2);
734 $value = $parts[0];
735 }
736 unset($value);
737 return $itemArray;
738 }
739
740 /**
741 * Checks if a record selector may select a certain file type
742 *
743 * @param array $selectorConfiguration
744 * @param array $fileRecord
745 * @return bool
746 * @todo: check this ...
747 */
748 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
749 {
750 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
751 $allowedFileExtensions = GeneralUtility::trimExplode(
752 ',',
753 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
754 true
755 );
756 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
757 return false;
758 }
759 }
760 return true;
761 }
762
763 /**
764 * Return expand / collapse state array for a given table / uid combination
765 *
766 * @param string $table Handled table
767 * @param int $uid Handled uid
768 * @return array
769 */
770 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
771 {
772 $inlineView = $this->getInlineExpandCollapseStateArray();
773 $result = [];
774 if (MathUtility::canBeInterpretedAsInteger($uid)) {
775 if (!empty($inlineView[$table][$uid])) {
776 $result = $inlineView[$table][$uid];
777 }
778 }
779 return $result;
780 }
781
782 /**
783 * Get expand / collapse state of inline items
784 *
785 * @return array
786 */
787 protected function getInlineExpandCollapseStateArray()
788 {
789 $backendUser = $this->getBackendUserAuthentication();
790 if (!$this->backendUserHasUcInlineView($backendUser)) {
791 return [];
792 }
793
794 $inlineView = unserialize($backendUser->uc['inlineView']);
795 if (!is_array($inlineView)) {
796 $inlineView = [];
797 }
798
799 return $inlineView;
800 }
801
802 /**
803 * Method to check whether the backend user has the property inline view for the current IRRE item.
804 * In existing or old IRRE items the attribute may not exist, then the unserialize will fail.
805 *
806 * @param BackendUserAuthentication $backendUser
807 * @return bool
808 */
809 protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
810 {
811 return !empty($backendUser->uc['inlineView']);
812 }
813
814 /**
815 * Remove an element from an array.
816 *
817 * @param mixed $needle The element to be removed.
818 * @param array $haystack The array the element should be removed from.
819 * @param mixed $strict Search elements strictly.
820 * @return array The array $haystack without the $needle
821 */
822 protected function removeFromArray($needle, $haystack, $strict = null)
823 {
824 $pos = array_search($needle, $haystack, $strict);
825 if ($pos !== false) {
826 unset($haystack[$pos]);
827 }
828 return $haystack;
829 }
830
831 /**
832 * Generates an error message that transferred as JSON for AJAX calls
833 *
834 * @param string $message The error message to be shown
835 * @return array The error message in a JSON array
836 */
837 protected function getErrorMessageForAJAX($message)
838 {
839 return [
840 'data' => $message,
841 'scriptCall' => [
842 'alert("' . $message . '");'
843 ],
844 ];
845 }
846
847 /**
848 * Get inlineFirstPid from a given objectId string
849 *
850 * @param string $domObjectId The id attribute of an element
851 * @return int|NULL Pid or null
852 */
853 protected function getInlineFirstPidFromDomObjectId($domObjectId)
854 {
855 // Substitute FlexForm addition and make parsing a bit easier
856 $domObjectId = str_replace('---', ':', $domObjectId);
857 // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
858 $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
859 if (preg_match($pattern, $domObjectId, $match)) {
860 return $match[1];
861 }
862 return null;
863 }
864
865 /**
866 * @return BackendUserAuthentication
867 */
868 protected function getBackendUserAuthentication()
869 {
870 return $GLOBALS['BE_USER'];
871 }
872
873 /**
874 * Extract the inline child table configuration from the flexform data structure
875 * using the the domObjectId to traverse the XML structure.
876 *
877 * domObjectId parsing has been copied from InlineStackProcessor::initializeByDomObjectId
878 *
879 * @param array $parentConfig
880 * @param string $domObjectId
881 * @return array
882 */
883 protected function getParentConfigFromFlexForm(array $parentConfig, $domObjectId)
884 {
885 list($flexFormPath, $foreignTableName) = $this->splitDomObjectId($domObjectId);
886
887 $childConfig = $parentConfig['ds']['sheets'];
888 $flexFormPath = explode(':', $flexFormPath);
889 foreach ($flexFormPath as $flexFormNode) {
890 // We are dealing with configuration information from a flexform,
891 // not value storage, identifiers that reference language or
892 // value nodes must be skipped.
893 if (!isset($childConfig[$flexFormNode]) && preg_match('/^[lv][[:alpha:]]+$/', $flexFormNode)) {
894 continue;
895 }
896 $childConfig = $childConfig[$flexFormNode];
897
898 // Skip to the field configuration of a sheet
899 if (isset($childConfig['ROOT']) && $childConfig['ROOT']['type'] == 'array') {
900 $childConfig = $childConfig['ROOT']['el'];
901 }
902 }
903
904 if (!isset($childConfig['config'])
905 || !is_array($childConfig['config'])
906 || $childConfig['config']['type'] !== 'inline'
907 || $childConfig['config']['foreign_table'] !== $foreignTableName
908 ) {
909 throw new \UnexpectedValueException(
910 'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
911 1446996319
912 );
913 }
914 return $childConfig['config'];
915 }
916
917 /**
918 * Flexforms require additional database columns to be processed to determine the correct
919 * data structure to be used from a flexform. The required columns and their values are
920 * transmitted in the AJAX context of the request and need to be added to the fake database
921 * row for the inline parent.
922 *
923 * @param array $ajaxArguments The AJAX request arguments
924 * @param array $databaseRow The fake database row
925 * @return array The database row with the flexform data structure pointer columns added
926 */
927 protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
928 {
929 if (!isset($ajaxArguments['context'])) {
930 return $databaseRow;
931 }
932
933 $context = json_decode($ajaxArguments['context'], true);
934 if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
935 return $databaseRow;
936 }
937
938 if (isset($context['config']['flexDataStructurePointers'])
939 && is_array($context['config']['flexDataStructurePointers'])
940 ) {
941 $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);
942 }
943
944 return $databaseRow;
945 }
946
947 /**
948 * split the domObjectID and retrieve the needed parts
949 *
950 * @param string $domObjectId
951 *
952 * @return array
953 */
954 protected function splitDomObjectId($domObjectId)
955 {
956
957 // Substitute FlexForm addition and make parsing a bit easier
958 $domObjectId = str_replace('---', ':', $domObjectId);
959 $pattern = '/:data:(?<flexformPath>.*?)-(?<tableName>[^-]+)(?:-(?:NEW)?\w+)?$/';
960
961 /* EXPLANATION for the regex:
962 * according https://regex101.com/
963 *
964 * :data: matches the characters :data: literally (case sensitive)
965 * (?<flexformPath>.*?) Named capturing group flexformPath
966 * .*? matches any character (except newline)
967 * Quantifier: *? Between zero and unlimited times, as few times as possible, expanding as needed [lazy]
968 * - matches the character - literally
969 * (?<tableName>[^-]+) Named capturing group tableName
970 * [^-]+ match a single character not present in the list below
971 * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
972 * - the literal character -
973 * (?:-(?:NEW)?\w+)? Non-capturing group
974 * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
975 * - matches the character - literally
976 * (?:NEW)? Non-capturing group
977 * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
978 * NEW matches the characters NEW literally (case sensitive)
979 * \w+ match any word character [a-zA-Z0-9_]
980 * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
981 * $ assert position at end of a line
982 */
983
984 if (preg_match($pattern, $domObjectId, $match)) {
985 return [$match['flexformPath'], $match['tableName']];
986 }
987
988 return [];
989 }
990 }