TYPO3 CMS  TYPO3_7-6
FormInlineAjaxController.php
Go to the documentation of this file.
1 <?php
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 
31 
36 {
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
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  ];
93  $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
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 
115  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
117  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
118  $formDataCompilerInput = [
119  'command' => 'new',
120  'tableName' => $childTableName,
121  'vanillaUid' => $childVanillaUid,
122  'isInlineChild' => true,
123  'inlineStructure' => $inlineStackProcessor->getStructure(),
124  'inlineFirstPid' => $inlineFirstPid,
125  'inlineParentUid' => $parent['uid'],
126  'inlineParentTableName' => $parent['table'],
127  'inlineParentFieldName' => $parent['field'],
128  'inlineParentConfig' => $parentConfig,
129  ];
130  if ($childChildUid) {
131  $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
132  }
133  $childData = $formDataCompiler->compile($formDataCompilerInput);
134 
135  // Set language of new child record to the language of the parent record
136  if ($parent['localizationMode'] !== 'keep' && MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
137  $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
138  $childLanguageField = $GLOBALS['TCA'][$child['table']]['ctrl']['languageField'];
139  if (!empty($parentLanguageField) && !empty($childLanguageField)) {
140  $parentRecord = BackendUtility::getRecord($parent['table'], $parent['uid']);
141  $childData['databaseRow'][$childLanguageField][0] = $parentRecord[$parentLanguageField];
142  }
143  }
144 
145  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
146  // We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
147  // Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
148  // existing record of the child-child table specified by the given uid. If there is no such id, user
149  // clicked on "created new" and a new child-child should be created, too.
150  if ($childChildUid) {
151  // Fetch existing child child
152  $childData['databaseRow'][$parentConfig['foreign_selector']] = [
153  $childChildUid,
154  ];
155  $childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
156  } else {
158  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
160  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
161  $formDataCompilerInput = [
162  'command' => 'new',
163  'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
164  'vanillaUid' => (int)$inlineFirstPid,
165  'isInlineChild' => true,
166  'isInlineAjaxOpeningContext' => true,
167  'inlineStructure' => $inlineStackProcessor->getStructure(),
168  'inlineFirstPid' => (int)$inlineFirstPid,
169  ];
170  $childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
171  }
172  }
173 
174  $childData['inlineParentUid'] = (int)$parent['uid'];
175  $childData['renderType'] = 'inlineRecordContainer';
176  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
177  $childResult = $nodeFactory->create($childData)->render();
178 
179  $jsonArray = [
180  'data' => '',
181  'stylesheetFiles' => [],
182  'scriptCall' => [],
183  ];
184 
185  // The HTML-object-id's prefix of the dynamically created record
186  $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
187  $objectPrefix = $objectName . '-' . $child['table'];
188  $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
189  $expandSingle = $parentConfig['appearance']['expandSingle'];
190  if (!$child['uid']) {
191  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
192  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
193  } else {
194  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
195  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
196  }
197  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
198  if ($parentConfig['appearance']['useSortable']) {
199  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
200  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
201  }
202  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
203  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
204  }
205  // Fade out and fade in the new record in the browser view to catch the user's eye
206  $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
207 
208  $response->getBody()->write(json_encode($jsonArray));
209 
210  return $response;
211  }
212 
220  public function detailsAction(ServerRequestInterface $request, ResponseInterface $response)
221  {
222  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
223 
224  $domObjectId = $ajaxArguments[0];
225  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
226 
227  // Parse the DOM identifier, add the levels to the structure stack
229  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
230  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
231  $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
232 
233  // Parent, this table embeds the child table
234  $parent = $inlineStackProcessor->getStructureLevel(-1);
235  $parentFieldName = $parent['field'];
236 
237  $databaseRow = [
238  // TcaInlineExpandCollapseState needs this
239  'uid' => (int)$parent['uid'],
240  ];
241 
242  $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
243 
244  $formDataCompilerInputForParent = [
245  'vanillaUid' => (int)$parent['uid'],
246  'command' => 'edit',
247  'tableName' => $parent['table'],
248  'databaseRow' => $databaseRow,
249  'inlineFirstPid' => $inlineFirstPid,
250  'columnsToProcess' => array_merge(
251  [$parentFieldName],
252  array_keys($databaseRow)
253  ),
254  // @todo: still needed?
255  'inlineStructure' => $inlineStackProcessor->getStructure(),
256  // Do not resolve existing children, we don't need them now
257  'inlineResolveExistingChildren' => false,
258  ];
260  $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
262  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
263  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
264  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
265 
266  if ($parentConfig['type'] === 'flex') {
267  $parentConfig = $this->getParentConfigFromFlexForm($parentConfig, $domObjectId);
268  $parentData['processedTca']['columns'][$parentFieldName]['config'] = $parentConfig;
269  }
270 
271  // Set flag in config so that only the fields are rendered
272  // @todo: Solve differently / rename / whatever
273  $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
274 
275  // Child, a record from this table should be rendered
276  $child = $inlineStackProcessor->getUnstableStructure();
277 
278  $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
279 
280  $childData['inlineParentUid'] = (int)$parent['uid'];
281  $childData['renderType'] = 'inlineRecordContainer';
282  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
283  $childResult = $nodeFactory->create($childData)->render();
284 
285  $jsonArray = [
286  'data' => '',
287  'stylesheetFiles' => [],
288  'scriptCall' => [],
289  ];
290 
291  // The HTML-object-id's prefix of the dynamically created record
292  $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
293  $objectId = $objectPrefix . '-' . (int)$child['uid'];
294  $expandSingle = $parentConfig['appearance']['expandSingle'];
295  $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
296  if ($parentConfig['foreign_unique']) {
297  $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
298  }
299  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
300  if ($parentConfig['appearance']['useSortable']) {
301  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
302  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
303  }
304  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
305  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
306  }
307 
308  $response->getBody()->write(json_encode($jsonArray));
309 
310  return $response;
311  }
312 
321  public function synchronizeLocalizeAction(ServerRequestInterface $request, ResponseInterface $response)
322  {
323  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
324  $domObjectId = $ajaxArguments[0];
325  $type = $ajaxArguments[1];
326 
328  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
329  // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
330  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
331  $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
332  $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
333 
334  $jsonArray = false;
335  if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
336  // Parent, this table embeds the child table
337  $parent = $inlineStackProcessor->getStructureLevel(-1);
338  $parentFieldName = $parent['field'];
339 
340  // Child, a record from this table should be rendered
341  $child = $inlineStackProcessor->getUnstableStructure();
342 
343  $formDataCompilerInputForParent = [
344  'vanillaUid' => (int)$parent['uid'],
345  'command' => 'edit',
346  'tableName' => $parent['table'],
347  'inlineFirstPid' => $inlineFirstPid,
348  'columnsToProcess' => [
349  $parentFieldName
350  ],
351  // @todo: still needed? NO!
352  'inlineStructure' => $inlineStackProcessor->getStructure(),
353  // Do not compile existing children, we don't need them now
354  'inlineCompileExistingChildren' => false,
355  ];
356  // Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
358  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
360  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
361  $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
362  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
363  $parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
364  $parentLanguage = $parentData['databaseRow'][$parentLanguageField];
365  $oldItemList = $parentData['databaseRow'][$parentFieldName];
366 
367  // DataHandler cannot handle arrays as field value
368  if (is_array($parentLanguage)) {
369  $parentLanguage = implode(',', $parentLanguage);
370  }
371 
372  $cmd = [];
373  // Localize a single child element from default language of the parent element
375  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
376  'field' => $parent['field'],
377  'language' => $parentLanguage,
378  'ids' => [$type],
379  ];
380  // Either localize or synchronize all child elements from default language of the parent element
381  } else {
382  $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
383  'field' => $parent['field'],
384  'language' => $parentLanguage,
385  'action' => $type,
386  ];
387  }
388 
390  $tce = GeneralUtility::makeInstance(DataHandler::class);
391  $tce->stripslashes_values = false;
392  $tce->start([], $cmd);
393  $tce->process_cmdmap();
394 
395  $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
396 
397  $jsonArray = [
398  'data' => '',
399  'stylesheetFiles' => [],
400  'scriptCall' => [],
401  ];
402  $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
403  $nameObjectForeignTable = $nameObject . '-' . $child['table'];
404 
405  $oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
406  $newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
407 
408  // Set the items that should be removed in the forms view:
409  $removedItems = array_diff($oldItems, $newItems);
410  foreach ($removedItems as $childUid) {
411  $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
412  }
413 
414  $localizedItems = array_diff($newItems, $oldItems);
415  foreach ($localizedItems as $childUid) {
416  $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
417 
418  $childData['inlineParentUid'] = (int)$parent['uid'];
419  $childData['renderType'] = 'inlineRecordContainer';
420  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
421  $childResult = $nodeFactory->create($childData)->render();
422 
423  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
424 
425  // Get the name of the field used as foreign selector (if any):
426  $foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
427  $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
428  if (is_array($selectedValue)) {
429  $selectedValue = $selectedValue[0];
430  }
431  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
432  // Remove possible virtual records in the form which showed that a child records could be localized:
433  $transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
434  if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
435  $transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
436  if (is_array($transOrigPointerField)) {
437  $transOrigPointerField = $transOrigPointerField[0];
438  }
439  $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
440  }
441  }
442  // Tell JS to add new HTML of one or multiple (localize all) records to DOM
443  if (!empty($jsonArray['data'])) {
444  array_push(
445  $jsonArray['scriptCall'],
446  'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
447  . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
448  . ', json.data);'
449  );
450  }
451  }
452 
453  $response->getBody()->write(json_encode($jsonArray));
454 
455  return $response;
456  }
457 
465  public function expandOrCollapseAction(ServerRequestInterface $request, ResponseInterface $response)
466  {
467  $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
468  $domObjectId = $ajaxArguments[0];
469 
471  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
472  // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
473  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
474  $expand = $ajaxArguments[1];
475  $collapse = $ajaxArguments[2];
476 
477  $backendUser = $this->getBackendUserAuthentication();
478  // The current table - for this table we should add/import records
479  $currentTable = $inlineStackProcessor->getUnstableStructure();
480  $currentTable = $currentTable['table'];
481  // The top parent table - this table embeds the current table
482  $top = $inlineStackProcessor->getStructureLevel(0);
483  $topTable = $top['table'];
484  $topUid = $top['uid'];
485  $inlineView = $this->getInlineExpandCollapseStateArray();
486  // Only do some action if the top record and the current record were saved before
488  $expandUids = GeneralUtility::trimExplode(',', $expand);
489  $collapseUids = GeneralUtility::trimExplode(',', $collapse);
490  // Set records to be expanded
491  foreach ($expandUids as $uid) {
492  $inlineView[$topTable][$topUid][$currentTable][] = $uid;
493  }
494  // Set records to be collapsed
495  foreach ($collapseUids as $uid) {
496  $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
497  }
498  // Save states back to database
499  if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
500  $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
501  $backendUser->uc['inlineView'] = serialize($inlineView);
502  $backendUser->writeUC();
503  }
504  }
505 
506  $response->getBody()->write(json_encode([]));
507  return $response;
508  }
509 
522  protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
523  {
524  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
525 
527  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
528  $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
529  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
530 
531  // @todo: do not use stack processor here ...
532  $child = $inlineStackProcessor->getUnstableStructure();
533  $childTableName = $child['table'];
534 
536  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
538  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
539  $formDataCompilerInput = [
540  'command' => 'edit',
541  'tableName' => $childTableName,
542  'vanillaUid' => (int)$childUid,
543  'isInlineChild' => true,
544  'inlineStructure' => $inlineStructure,
545  'inlineFirstPid' => $parentData['inlineFirstPid'],
546  'inlineParentConfig' => $parentConfig,
547  'isInlineAjaxOpeningContext' => true,
548 
549  // values of the current parent element
550  // it is always a string either an id or new...
551  'inlineParentUid' => $parentData['databaseRow']['uid'],
552  'inlineParentTableName' => $parentData['tableName'],
553  'inlineParentFieldName' => $parentFieldName,
554 
555  // values of the top most parent element set on first level and not overridden on following levels
556  'inlineTopMostParentUid' => $parentData['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
557  'inlineTopMostParentTableName' => $parentData['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
558  'inlineTopMostParentFieldName' => $parentData['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
559  ];
560  // For foreign_selector with useCombination $mainChild is the mm record
561  // and $combinationChild is the child-child. For "normal" relations, $mainChild
562  // is just the normal child record and $combinationChild is empty.
563  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
564  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
565  // This kicks in if opening an existing mainChild that has a child-child set
566  $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
567  }
568  return $mainChild;
569  }
570 
580  protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
581  {
582  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
583  $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
584  // child-child table name is set in child tca "the selector field" foreign_table
585  $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
587  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
589  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
590  $formDataCompilerInput = [
591  'command' => 'edit',
592  'tableName' => $childChildTableName,
593  'vanillaUid' => (int)$childChildUid,
594  'isInlineChild' => true,
595  'isInlineAjaxOpeningContext' => true,
596  // @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
597  'inlineStructure' => $inlineStructure,
598  'inlineFirstPid' => $child['inlineFirstPid'],
599  // values of the top most parent element set on first level and not overridden on following levels
600  'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
601  'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
602  'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
603  ];
604  return $formDataCompiler->compile($formDataCompilerInput);
605  }
606 
615  protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
616  {
617  $jsonResult['data'] .= $childResult['html'];
618  $jsonResult['stylesheetFiles'] = $childResult['stylesheetFiles'];
619  if (!empty($childResult['inlineData'])) {
620  $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
621  }
622  if (!empty($childResult['additionalJavaScriptSubmit'])) {
623  $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
624  $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
625  $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
626  }
627  foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
628  $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
629  }
630  $jsonResult['scriptCall'][] = $childResult['extJSCODE'];
631  if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
632  $labels = [];
633  foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
635  $labels,
636  $this->addInlineLanguageLabelFile($additionalInlineLanguageLabelFile)
637  );
638  }
639  $javaScriptCode = [];
640  $javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
641  $javaScriptCode[] = ' TYPO3.lang = {}';
642  $javaScriptCode[] = '}';
643  $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
644  $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
645  $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
646  $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
647  $javaScriptCode[] = ' }';
648  $javaScriptCode[] = '}';
649 
650  $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
651  }
652  if (!empty($childResult['requireJsModules'])) {
653  foreach ($childResult['requireJsModules'] as $module) {
654  $moduleName = null;
655  $callback = null;
656  if (is_string($module)) {
657  // if $module is a string, no callback
658  $moduleName = $module;
659  $callback = null;
660  } elseif (is_array($module)) {
661  // if $module is an array, callback is possible
662  foreach ($module as $key => $value) {
663  $moduleName = $key;
664  $callback = $value;
665  break;
666  }
667  }
668  if ($moduleName !== null) {
669  $inlineCodeKey = $moduleName;
670  $javaScriptCode = 'require(["' . $moduleName . '"]';
671  if ($callback !== null) {
672  $inlineCodeKey .= sha1($callback);
673  $javaScriptCode .= ', ' . $callback;
674  }
675  $javaScriptCode .= ');';
676  $jsonResult['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
677  }
678  }
679  }
680  return $jsonResult;
681  }
682 
688  protected function addInlineLanguageLabelFile($file)
689  {
691  $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
692  $language = $GLOBALS['LANG']->lang;
693  $localizationArray = $languageFactory->getParsedData(
694  $file,
695  $language,
696  'utf-8',
697  1
698  );
699  if (is_array($localizationArray) && !empty($localizationArray)) {
700  if (!empty($localizationArray[$language])) {
701  $xlfLabelArray = $localizationArray['default'];
702  ArrayUtility::mergeRecursiveWithOverrule($xlfLabelArray, $localizationArray[$language], true, false);
703  } else {
704  $xlfLabelArray = $localizationArray['default'];
705  }
706  } else {
707  $xlfLabelArray = [];
708  }
709  $labelArray = [];
710  foreach ($xlfLabelArray as $key => $value) {
711  if (isset($value[0]['target'])) {
712  $labelArray[$key] = $value[0]['target'];
713  } else {
714  $labelArray[$key] = '';
715  }
716  }
717  return $labelArray;
718  }
719 
728  protected function getInlineRelatedRecordsUidArray($itemList)
729  {
730  $itemArray = GeneralUtility::trimExplode(',', $itemList, true);
731  // Perform modification of the selected items array:
732  foreach ($itemArray as &$value) {
733  $parts = explode('|', $value, 2);
734  $value = $parts[0];
735  }
736  unset($value);
737  return $itemArray;
738  }
739 
748  protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)
749  {
750  if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
751  $allowedFileExtensions = GeneralUtility::trimExplode(
752  ',',
753  $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
754  true
755  );
756  if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, true)) {
757  return false;
758  }
759  }
760  return true;
761  }
762 
771  {
772  $inlineView = $this->getInlineExpandCollapseStateArray();
773  $result = [];
775  if (!empty($inlineView[$table][$uid])) {
776  $result = $inlineView[$table][$uid];
777  }
778  }
779  return $result;
780  }
781 
788  {
789  $backendUser = $this->getBackendUserAuthentication();
790  if (!$this->backendUserHasUcInlineView($backendUser)) {
791  return [];
792  }
793 
794  $inlineView = unserialize($backendUser->uc['inlineView']);
795  if (!is_array($inlineView)) {
796  $inlineView = [];
797  }
798 
799  return $inlineView;
800  }
801 
809  protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
810  {
811  return !empty($backendUser->uc['inlineView']);
812  }
813 
822  protected function removeFromArray($needle, $haystack, $strict = null)
823  {
824  $pos = array_search($needle, $haystack, $strict);
825  if ($pos !== false) {
826  unset($haystack[$pos]);
827  }
828  return $haystack;
829  }
830 
837  protected function getErrorMessageForAJAX($message)
838  {
839  return [
840  'data' => $message,
841  'scriptCall' => [
842  'alert("' . $message . '");'
843  ],
844  ];
845  }
846 
853  protected function getInlineFirstPidFromDomObjectId($domObjectId)
854  {
855  // Substitute FlexForm addition and make parsing a bit easier
856  $domObjectId = str_replace('---', ':', $domObjectId);
857  // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
858  $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
859  if (preg_match($pattern, $domObjectId, $match)) {
860  return $match[1];
861  }
862  return null;
863  }
864 
868  protected function getBackendUserAuthentication()
869  {
870  return $GLOBALS['BE_USER'];
871  }
872 
883  protected function getParentConfigFromFlexForm(array $parentConfig, $domObjectId)
884  {
885  list($flexFormPath, $foreignTableName) = $this->splitDomObjectId($domObjectId);
886 
887  $childConfig = $parentConfig['ds']['sheets'];
888  $flexFormPath = explode(':', $flexFormPath);
889  foreach ($flexFormPath as $flexFormNode) {
890  // We are dealing with configuration information from a flexform,
891  // not value storage, identifiers that reference language or
892  // value nodes must be skipped.
893  if (!isset($childConfig[$flexFormNode]) && preg_match('/^[lv][[:alpha:]]+$/', $flexFormNode)) {
894  continue;
895  }
896  $childConfig = $childConfig[$flexFormNode];
897 
898  // Skip to the field configuration of a sheet
899  if (isset($childConfig['ROOT']) && $childConfig['ROOT']['type'] == 'array') {
900  $childConfig = $childConfig['ROOT']['el'];
901  }
902  }
903 
904  if (!isset($childConfig['config'])
905  || !is_array($childConfig['config'])
906  || $childConfig['config']['type'] !== 'inline'
907  || $childConfig['config']['foreign_table'] !== $foreignTableName
908  ) {
909  throw new \UnexpectedValueException(
910  'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
911  1446996319
912  );
913  }
914  return $childConfig['config'];
915  }
916 
927  protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
928  {
929  if (!isset($ajaxArguments['context'])) {
930  return $databaseRow;
931  }
932 
933  $context = json_decode($ajaxArguments['context'], true);
934  if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
935  return $databaseRow;
936  }
937 
938  if (isset($context['config']['flexDataStructurePointers'])
939  && is_array($context['config']['flexDataStructurePointers'])
940  ) {
941  $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);
942  }
943 
944  return $databaseRow;
945  }
946 
954  protected function splitDomObjectId($domObjectId)
955  {
956 
957  // Substitute FlexForm addition and make parsing a bit easier
958  $domObjectId = str_replace('---', ':', $domObjectId);
959  $pattern = '/:data:(?<flexformPath>.*?)-(?<tableName>[^-]+)(?:-(?:NEW)?\w+)?$/';
960 
961  /* EXPLANATION for the regex:
962  * according https://regex101.com/
963  *
964  * :data: matches the characters :data: literally (case sensitive)
965  * (?<flexformPath>.*?) Named capturing group flexformPath
966  * .*? matches any character (except newline)
967  * Quantifier: *? Between zero and unlimited times, as few times as possible, expanding as needed [lazy]
968  * - matches the character - literally
969  * (?<tableName>[^-]+) Named capturing group tableName
970  * [^-]+ match a single character not present in the list below
971  * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
972  * - the literal character -
973  * (?:-(?:NEW)?\w+)? Non-capturing group
974  * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
975  * - matches the character - literally
976  * (?:NEW)? Non-capturing group
977  * Quantifier: ? Between zero and one time, as many times as possible, giving back as needed [greedy]
978  * NEW matches the characters NEW literally (case sensitive)
979  * \w+ match any word character [a-zA-Z0-9_]
980  * Quantifier: + Between one and unlimited times, as many times as possible, giving back as needed [greedy]
981  * $ assert position at end of a line
982  */
983 
984  if (preg_match($pattern, $domObjectId, $match)) {
985  return [$match['flexformPath'], $match['tableName']];
986  }
987 
988  return [];
989  }
990 }
addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
static hmac($input, $additionalSecret='')
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
$uid
Definition: server.php:38
backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord)