2 namespace TYPO3\CMS\Backend\Controller
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
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
;
30 * Handle FormEngine inline ajax calls
32 class FormInlineAjaxController
35 * Create a new inline child via AJAX.
37 * @param ServerRequestInterface $request
38 * @param ResponseInterface $response
39 * @return ResponseInterface
41 public function createAction(ServerRequestInterface
$request, ResponseInterface
$response)
43 $ajaxArguments = isset($request->getParsedBody()['ajax']) ?
$request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
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];
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']);
58 // Parent, this table embeds the child table
59 $parent = $inlineStackProcessor->getStructureLevel(-1);
60 $parentFieldName = $parent['field'];
62 if (MathUtility
::canBeInterpretedAsInteger($parent['uid'])) {
64 $vanillaUid = (int)$parent['uid'];
66 // TcaInlineExpandCollapseState needs the record uid
67 'uid' => (int)$parent['uid'],
72 $vanillaUid = (int)$inlineFirstPid;
74 $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
76 $formDataCompilerInputForParent = [
77 'vanillaUid' => $vanillaUid,
78 'command' => $command,
79 'tableName' => $parent['table'],
80 'databaseRow' => $databaseRow,
81 'inlineFirstPid' => $inlineFirstPid,
82 'columnsToProcess' => array_merge(
84 array_keys($databaseRow)
86 // Do not resolve existing children, we don't need them now
87 'inlineResolveExistingChildren' => false,
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'];
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']);
102 // Else inline first Pid is the storage pid of new inline records
103 $childVanillaUid = (int)$inlineFirstPid;
106 if ($parentConfig['type'] === 'flex') {
107 $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
109 $childTableName = $parentConfig['foreign_table'];
111 /** @var TcaDatabaseRecord $formDataGroup */
112 $formDataGroup = GeneralUtility
::makeInstance(TcaDatabaseRecord
::class);
113 /** @var FormDataCompiler $formDataCompiler */
114 $formDataCompiler = GeneralUtility
::makeInstance(FormDataCompiler
::class, $formDataGroup);
115 $formDataCompilerInput = [
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,
127 if ($childChildUid) {
128 $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
130 $childData = $formDataCompiler->compile($formDataCompilerInput);
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.
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];
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']] = [
156 $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
158 /** @var TcaDatabaseRecord $formDataGroup */
159 $formDataGroup = GeneralUtility
::makeInstance(TcaDatabaseRecord
::class);
160 /** @var FormDataCompiler $formDataCompiler */
161 $formDataCompiler = GeneralUtility
::makeInstance(FormDataCompiler
::class, $formDataGroup);
162 $formDataCompilerInput = [
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,
171 $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
175 $childData['inlineParentUid'] = (int)$parent['uid'];
176 $childData['renderType'] = 'inlineRecordContainer';
177 $nodeFactory = GeneralUtility
::makeInstance(NodeFactory
::class);
178 $childResult = $nodeFactory->create($childData)->render();
182 'stylesheetFiles' => [],
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) . ');';
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) . ');';
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') . ');';
203 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
204 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility
::quoteJSvalue($objectId) . ',' . GeneralUtility
::quoteJSvalue($objectPrefix) . ',' . GeneralUtility
::quoteJSvalue($childData['databaseRow']['uid']) . ');';
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') . ');';
209 $response->getBody()->write(json_encode($jsonArray));
215 * Show the details of a child record.
217 * @param ServerRequestInterface $request
218 * @param ResponseInterface $response
219 * @return ResponseInterface
221 public function detailsAction(ServerRequestInterface
$request, ResponseInterface
$response)
223 $ajaxArguments = isset($request->getParsedBody()['ajax']) ?
$request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
225 $domObjectId = $ajaxArguments[0];
226 $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
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']);
234 // Parent, this table embeds the child table
235 $parent = $inlineStackProcessor->getStructureLevel(-1);
236 $parentFieldName = $parent['field'];
238 $formDataCompilerInputForParent = [
239 'vanillaUid' => (int)$parent['uid'],
241 'tableName' => $parent['table'],
243 // TcaInlineExpandCollapseState needs this
244 'uid' => (int)$parent['uid'],
246 'inlineFirstPid' => $inlineFirstPid,
247 'columnsToProcess' => [
250 // @todo: still needed?
251 'inlineStructure' => $inlineStackProcessor->getStructure(),
252 // Do not resolve existing children, we don't need them now
253 'inlineResolveExistingChildren' => false,
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'];
265 // Child, a record from this table should be rendered
266 $child = $inlineStackProcessor->getUnstableStructure();
268 $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
270 $childData['inlineParentUid'] = (int)$parent['uid'];
271 $childData['renderType'] = 'inlineRecordContainer';
272 $nodeFactory = GeneralUtility
::makeInstance(NodeFactory
::class);
273 $childResult = $nodeFactory->create($childData)->render();
277 'stylesheetFiles' => [],
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'] . '\');';
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') . ');';
294 if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
295 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility
::quoteJSvalue($objectId) . ',' . GeneralUtility
::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
298 $response->getBody()->write(json_encode($jsonArray));
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.
307 * @param ServerRequestInterface $request the incoming request
308 * @param ResponseInterface $response the empty response
309 * @return ResponseInterface the filled response
311 public function synchronizeLocalizeAction(ServerRequestInterface
$request, ResponseInterface
$response)
313 $ajaxArguments = isset($request->getParsedBody()['ajax']) ?
$request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
314 $domObjectId = $ajaxArguments[0];
315 $type = $ajaxArguments[1];
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);
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'];
330 // Child, a record from this table should be rendered
331 $child = $inlineStackProcessor->getUnstableStructure();
333 $formDataCompilerInputForParent = [
334 'vanillaUid' => (int)$parent['uid'],
336 'tableName' => $parent['table'],
338 // TcaInlineExpandCollapseState needs this
339 'uid' => (int)$parent['uid'],
341 'inlineFirstPid' => $inlineFirstPid,
342 'columnsToProcess' => [
345 // @todo: still needed? NO!
346 'inlineStructure' => $inlineStackProcessor->getStructure(),
347 // Do not compile existing children, we don't need them now
348 'inlineCompileExistingChildren' => false,
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];
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();
367 $newItemList = $tce->registerDBList
[$parent['table']][$parent['uid']][$parentFieldName];
371 'stylesheetFiles' => [],
374 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
375 $nameObjectForeignTable = $nameObject . '-' . $child['table'];
377 $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
378 $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
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});';
386 $localizedItems = array_diff($newItems, $oldItems);
387 foreach ($localizedItems as $childUid) {
388 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
390 $childData['inlineParentUid'] = (int)$parent['uid'];
391 $childData['renderType'] = 'inlineRecordContainer';
392 $nodeFactory = GeneralUtility
::makeInstance(NodeFactory
::class);
393 $childResult = $nodeFactory->create($childData)->render();
395 $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
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];
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];
411 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility
::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
414 // Tell JS to add new HTML of one or multiple (localize all) records to DOM
415 if (!empty($jsonArray['data'])) {
417 $jsonArray['scriptCall'],
418 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility
::quoteJSvalue($nameObject . '_records')
419 . ', ' . GeneralUtility
::quoteJSvalue($nameObjectForeignTable)
425 $response->getBody()->write(json_encode($jsonArray));
431 * Adds localizations or synchronizes the locations of all child records.
433 * @param ServerRequestInterface $request the incoming request
434 * @param ResponseInterface $response the empty response
435 * @return ResponseInterface the filled response
437 public function expandOrCollapseAction(ServerRequestInterface
$request, ResponseInterface
$response)
439 $ajaxArguments = isset($request->getParsedBody()['ajax']) ?
$request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
440 $domObjectId = $ajaxArguments[0];
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];
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;
466 // Set records to be collapsed
467 foreach ($collapseUids as $uid) {
468 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
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();
478 $response->getBody()->write(json_encode(array()));
483 * Compile a full child record
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
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.
494 protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
496 $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
498 /** @var InlineStackProcessor $inlineStackProcessor */
499 $inlineStackProcessor = GeneralUtility
::makeInstance(InlineStackProcessor
::class);
500 $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
501 $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
503 // @todo: do not use stack processor here ...
504 $child = $inlineStackProcessor->getUnstableStructure();
505 $childTableName = $child['table'];
507 /** @var TcaDatabaseRecord $formDataGroup */
508 $formDataGroup = GeneralUtility
::makeInstance(TcaDatabaseRecord
::class);
509 /** @var FormDataCompiler $formDataCompiler */
510 $formDataCompiler = GeneralUtility
::makeInstance(FormDataCompiler
::class, $formDataGroup);
511 $formDataCompilerInput = [
513 'tableName' => $childTableName,
514 'vanillaUid' => (int)$childUid,
515 'isInlineChild' => true,
516 'inlineStructure' => $inlineStructure,
517 'inlineFirstPid' => $parentData['inlineFirstPid'],
518 'inlineParentConfig' => $parentConfig,
519 'isInlineAjaxOpeningContext' => true,
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,
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'],
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);
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.
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
552 protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
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 = [
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'],
576 return $formDataCompiler->compile($formDataCompilerInput);
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.
583 * @param array $jsonResult Given json result
584 * @param array $childResult Given child result
585 * @return array Merged json array
587 protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
589 $jsonResult['data'] .= $childResult['html'];
590 $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
591 if (!empty($childResult['inlineData'])) {
592 $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
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) . '");';
599 foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
600 $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
602 $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
603 if (!empty($childResult['requireJsModules'])) {
604 foreach ($childResult['requireJsModules'] as $module) {
607 if (is_string($module)) {
608 // if $module is a string, no callback
609 $moduleName = $module;
611 } elseif (is_array($module)) {
612 // if $module is an array, callback is possible
613 foreach ($module as $key => $value) {
619 if ($moduleName !== null) {
620 $inlineCodeKey = $moduleName;
621 $javaScriptCode = 'require(["' . $moduleName . '"]';
622 if ($callback !== null) {
623 $inlineCodeKey .= sha1($callback);
624 $javaScriptCode .= ', ' . $callback;
626 $javaScriptCode .= ');';
627 $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF
. $javaScriptCode;
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
639 * @param string $itemList The list of related child records
640 * @return array An array with uids
642 protected function getInlineRelatedRecordsUidArray($itemList)
644 $itemArray = GeneralUtility
::trimExplode(',', $itemList, true);
645 // Perform modification of the selected items array:
646 foreach ($itemArray as &$value) {
647 $parts = explode('|', $value, 2);
655 * Checks if a record selector may select a certain file type
657 * @param array $selectorConfiguration
658 * @param array $fileRecord
660 * @todo: check this ...
662 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
664 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
665 $allowedFileExtensions = GeneralUtility
::trimExplode(
667 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
670 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
678 * Return expand / collapse state array for a given table / uid combination
680 * @param string $table Handled table
681 * @param int $uid Handled uid
684 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
686 $inlineView = $this->getInlineExpandCollapseStateArray();
688 if (MathUtility
::canBeInterpretedAsInteger($uid)) {
689 if (!empty($inlineView[$table][$uid])) {
690 $result = $inlineView[$table][$uid];
697 * Get expand / collapse state of inline items
701 protected function getInlineExpandCollapseStateArray()
703 $backendUser = $this->getBackendUserAuthentication();
704 $inlineView = unserialize($backendUser->uc
['inlineView']);
705 if (!is_array($inlineView)) {
706 $inlineView = array();
712 * Remove an element from an array.
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
719 protected function removeFromArray($needle, $haystack, $strict = null)
721 $pos = array_search($needle, $haystack, $strict);
722 if ($pos !== false) {
723 unset($haystack[$pos]);
729 * Generates an error message that transferred as JSON for AJAX calls
731 * @param string $message The error message to be shown
732 * @return array The error message in a JSON array
734 protected function getErrorMessageForAJAX($message)
739 'alert("' . $message . '");'
745 * Get inlineFirstPid from a given objectId string
747 * @param string $domObjectId The id attribute of an element
748 * @return int|NULL Pid or null
750 protected function getInlineFirstPidFromDomObjectId($domObjectId)
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)) {
763 * @return BackendUserAuthentication
765 protected function getBackendUserAuthentication()
767 return $GLOBALS['BE_USER'];
771 * Extract the inline child table configuration from the flexform data structure
772 * using the the domObjectId to traverse the XML structure.
774 * domObjectId parsing has been copied from InlineStackProcessor::initializeByDomObjectId
776 * @param array $parentConfig
777 * @param string $domObjectId
780 protected function getParentConfigFromFlexForm(array $parentConfig, $domObjectId)
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>.+)$/';
788 // Will be checked against the FlexForm configuration as an additional safeguard
789 $foreignTableName = '';
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);
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.',
804 $fieldParts = GeneralUtility
::trimExplode(':', $parts[0]);
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],
814 $flexFormPath = array_slice($fieldParts, 2);
815 $foreignTableName = $parts[1];
818 $childConfig = $parentConfig['ds']['sheets'];
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)) {
827 $childConfig = $childConfig[$flexFormNode];
829 // Skip to the field configuration of a sheet
830 if (isset($childConfig['ROOT']) && $childConfig['ROOT']['type'] == 'array') {
831 $childConfig = $childConfig['ROOT']['el'];
835 if (!isset($childConfig['config'])
836 ||
!is_array($childConfig['config'])
837 ||
$childConfig['config']['type'] !== 'inline'
838 ||
$childConfig['config']['foreign_table'] !== $foreignTableName
840 throw new \
UnexpectedValueException(
841 'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
845 return $childConfig['config'];
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.
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
858 protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
860 if (!isset($ajaxArguments['context'])) {
864 $context = json_decode($ajaxArguments['context'], true);
865 if (GeneralUtility
::hmac(serialize($context['config'])) !== $context['hmac']) {
869 if (isset($context['config']['flexDataStructurePointers'])
870 && is_array($context['config']['flexDataStructurePointers'])
872 $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);