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