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