‪TYPO3CMS  9.5
SiteInlineAjaxController.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
31 
38 {
42  public function ‪__construct()
43  {
44  // Bring site TCA into global scope.
45  // @todo: We might be able to get rid of that later
46  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
47  }
48 
56  public function ‪newInlineChildAction(ServerRequestInterface $request): ResponseInterface
57  {
58  $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
59  $parentConfig = $this->‪extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
60  $domObjectId = $ajaxArguments[0];
61  $inlineFirstPid = $this->‪getInlineFirstPidFromDomObjectId($domObjectId);
62  $childChildUid = null;
63  if (isset($ajaxArguments[1]) && ‪MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
64  $childChildUid = (int)$ajaxArguments[1];
65  }
66  // Parse the DOM identifier, add the levels to the structure stack
67  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
68  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
69  $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
70  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
71  // Parent, this table embeds the child table
72  $parent = $inlineStackProcessor->getStructureLevel(-1);
73  // Child, a record from this table should be rendered
74  $child = $inlineStackProcessor->getUnstableStructure();
75  if (‪MathUtility::canBeInterpretedAsInteger($child['uid'])) {
76  // If uid comes in, it is the id of the record neighbor record "create after"
77  $childVanillaUid = -1 * abs((int)$child['uid']);
78  } else {
79  // Else inline first Pid is the storage pid of new inline records
80  $childVanillaUid = (int)$inlineFirstPid;
81  }
82  $childTableName = $parentConfig['foreign_table'];
83 
84  $defaultDatabaseRow = [];
85  if ($childTableName === 'site_language') {
86  // Feed new site_language row with data from sys_language record if possible
87  if ($childChildUid > 0) {
88  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
89  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
90  $row = $queryBuilder->select('*')->from('sys_language')
91  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($childChildUid, \PDO::PARAM_INT)))
92  ->execute()->fetch();
93  if (empty($row)) {
94  throw new \RuntimeException('Referenced sys_language row not found', 1521783937);
95  }
96  if (!empty($row['language_isocode'])) {
97  $defaultDatabaseRow['iso-639-1'] = $row['language_isocode'];
98  $defaultDatabaseRow['base'] = '/' . $row['language_isocode'] . '/';
99  }
100  if (!empty($row['flag']) && $row['flag'] === 'multiple') {
101  $defaultDatabaseRow['flag'] = 'global';
102  } elseif (!empty($row)) {
103  $defaultDatabaseRow['flag'] = $row['flag'];
104  }
105  if (!empty($row['title'])) {
106  $defaultDatabaseRow['title'] = $row['title'];
107  }
108  }
109  }
110 
111  $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
112  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
113  $formDataCompilerInput = [
114  'command' => 'new',
115  'tableName' => $childTableName,
116  'vanillaUid' => $childVanillaUid,
117  'databaseRow' => $defaultDatabaseRow,
118  'isInlineChild' => true,
119  'inlineStructure' => $inlineStackProcessor->getStructure(),
120  'inlineFirstPid' => $inlineFirstPid,
121  'inlineParentUid' => $parent['uid'],
122  'inlineParentTableName' => $parent['table'],
123  'inlineParentFieldName' => $parent['field'],
124  'inlineParentConfig' => $parentConfig,
125  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
126  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
127  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
128  ];
129  if ($childChildUid) {
130  $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
131  }
132  $childData = $formDataCompiler->compile($formDataCompilerInput);
133 
134  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
135  throw new \RuntimeException('useCombination not implemented in sites module', 1522493094);
136  }
137 
138  $childData['inlineParentUid'] = (int)$parent['uid'];
139  $childData['renderType'] = 'inlineRecordContainer';
140  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
141  $childResult = $nodeFactory->create($childData)->render();
142 
143  $jsonArray = [
144  'data' => '',
145  'stylesheetFiles' => [],
146  'scriptCall' => [],
147  ];
148 
149  // The HTML-object-id's prefix of the dynamically created record
150  $objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
151  $objectPrefix = $objectName . '-' . $child['table'];
152  $objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
153  $expandSingle = $parentConfig['appearance']['expandSingle'];
154  if (!$child['uid']) {
155  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
156  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
157  } else {
158  $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
159  $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
160  }
161  $jsonArray = $this->‪mergeChildResultIntoJsonResult($jsonArray, $childResult);
162  if ($parentConfig['appearance']['useSortable']) {
163  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
164  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
165  }
166  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
167  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
168  }
169  // Fade out and fade in the new record in the browser view to catch the user's eye
170  $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
171 
172  return new ‪JsonResponse($jsonArray);
173  }
174 
182  public function ‪openInlineChildAction(ServerRequestInterface $request): ResponseInterface
183  {
184  $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
185 
186  $domObjectId = $ajaxArguments[0];
187  $inlineFirstPid = $this->‪getInlineFirstPidFromDomObjectId($domObjectId);
188  $parentConfig = $this->‪extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
189 
190  // Parse the DOM identifier, add the levels to the structure stack
191  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
192  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
193  $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
194 
195  // Parent, this table embeds the child table
196  $parent = $inlineStackProcessor->getStructureLevel(-1);
197  $parentFieldName = $parent['field'];
198 
199  // Set flag in config so that only the fields are rendered
200  // @todo: Solve differently / rename / whatever
201  $parentConfig['renderFieldsOnly'] = true;
202 
203  $parentData = [
204  'processedTca' => [
205  'columns' => [
206  $parentFieldName => [
207  'config' => $parentConfig,
208  ],
209  ],
210  ],
211  'tableName' => $parent['table'],
212  'inlineFirstPid' => $inlineFirstPid,
213  // Hand over given original return url to compile stack. Needed if inline children compile links to
214  // another view (eg. edit metadata in a nested inline situation like news with inline content element image),
215  // so the back link is still the link from the original request. See issue #82525. This is additionally
216  // given down in TcaInline data provider to compiled children data.
217  'returnUrl' => $parentConfig['originalReturnUrl'],
218  ];
219 
220  // Child, a record from this table should be rendered
221  $child = $inlineStackProcessor->getUnstableStructure();
222 
223  $childData = $this->‪compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
224 
225  $childData['inlineParentUid'] = (int)$parent['uid'];
226  $childData['renderType'] = 'inlineRecordContainer';
227  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
228  $childResult = $nodeFactory->create($childData)->render();
229 
230  $jsonArray = [
231  'data' => '',
232  'stylesheetFiles' => [],
233  'scriptCall' => [],
234  ];
235 
236  // The HTML-object-id's prefix of the dynamically created record
237  $objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
238  $objectId = $objectPrefix . '-' . (int)$child['uid'];
239  $expandSingle = $parentConfig['appearance']['expandSingle'];
240  $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
241  if ($parentConfig['foreign_unique']) {
242  $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
243  }
244  $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
245  if ($parentConfig['appearance']['useSortable']) {
246  $inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
247  $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
248  }
249  if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
250  $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
251  }
252 
253  return new JsonResponse($jsonArray);
254  }
255 
269  protected function compileChild(array $parentData, string $parentFieldName, int $childUid, array $inlineStructure): array
270  {
271  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
272 
273  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
274  $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
275  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
276 
277  // @todo: do not use stack processor here ...
278  $child = $inlineStackProcessor->getUnstableStructure();
279  $childTableName = $child['table'];
280 
281  $formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
282  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
283  $formDataCompilerInput = [
284  'command' => 'edit',
285  'tableName' => $childTableName,
286  'vanillaUid' => (int)$childUid,
287  'returnUrl' => $parentData['returnUrl'],
288  'isInlineChild' => true,
289  'inlineStructure' => $inlineStructure,
290  'inlineFirstPid' => $parentData['inlineFirstPid'],
291  'inlineParentConfig' => $parentConfig,
292  'isInlineAjaxOpeningContext' => true,
293 
294  // values of the current parent element
295  // it is always a string either an id or new...
296  'inlineParentUid' => $parentData['databaseRow']['uid'],
297  'inlineParentTableName' => $parentData['tableName'],
298  'inlineParentFieldName' => $parentFieldName,
299 
300  // values of the top most parent element set on first level and not overridden on following levels
301  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
302  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
303  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
304  ];
305  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
306  throw new \RuntimeException('useCombination not implemented in sites module', 1522493095);
307  }
308  return $formDataCompiler->compile($formDataCompilerInput);
309  }
310 
319  protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array
320  {
321  $jsonResult['data'] .= $childResult['html'];
322  $jsonResult['stylesheetFiles'] = [];
323  foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
324  $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
325  }
326  if (!empty($childResult['inlineData'])) {
327  $jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
328  }
329  if (!empty($childResult['additionalJavaScriptSubmit'])) {
330  $additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
331  $additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
332  $jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
333  }
334  foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
335  $jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
336  }
337  if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
338  $labels = [];
339  foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
340  ArrayUtility::mergeRecursiveWithOverrule(
341  $labels,
342  $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
343  );
344  }
345  $javaScriptCode = [];
346  $javaScriptCode[] = 'if (typeof ‪TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
347  $javaScriptCode[] = ' TYPO3.lang = {}';
348  $javaScriptCode[] = '}';
349  $javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
350  $javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
351  $javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
352  $javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
353  $javaScriptCode[] = ' }';
354  $javaScriptCode[] = '}';
355 
356  $jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
357  }
358  $requireJsModule = $this->‪createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
359  $jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
360 
361  return $jsonResult;
362  }
363 
374  protected function ‪extractSignedParentConfigFromRequest(string $contextString): array
375  {
376  if ($contextString === '') {
377  throw new \RuntimeException('Empty context string given', 1522771624);
378  }
379  $context = json_decode($contextString, true);
380  if (empty($context['config'])) {
381  throw new \RuntimeException('Empty context config section given', 1522771632);
382  }
383  if (!hash_equals(GeneralUtility::hmac((string)$context['config'], 'InlineContext'), (string)$context['hmac'])) {
384  throw new \RuntimeException('Hash does not validate', 1522771640);
385  }
386  return json_decode($context['config'], true);
387  }
388 
395  protected function ‪getInlineFirstPidFromDomObjectId(string $domObjectId): ?int
396  {
397  // Substitute FlexForm addition and make parsing a bit easier
398  $domObjectId = str_replace('---', ':', $domObjectId);
399  // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
400  $pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
401  if (preg_match($pattern, $domObjectId, $match)) {
402  return (int)$match[1];
403  }
404  return null;
405  }
406 }
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:25
‪TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration
Definition: SiteTcaConfiguration.php:32
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:73
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\compileChild
‪array compileChild(array $parentData, string $parentFieldName, int $childUid, array $inlineStructure)
Definition: SiteInlineAjaxController.php:269
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\extractSignedParentConfigFromRequest
‪array extractSignedParentConfigFromRequest(string $contextString)
Definition: SiteInlineAjaxController.php:374
‪TYPO3
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\mergeChildResultIntoJsonResult
‪array mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
Definition: SiteInlineAjaxController.php:319
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\getInlineFirstPidFromDomObjectId
‪int null getInlineFirstPidFromDomObjectId(string $domObjectId)
Definition: SiteInlineAjaxController.php:395
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\openInlineChildAction
‪ResponseInterface openInlineChildAction(ServerRequestInterface $request)
Definition: SiteInlineAjaxController.php:182
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController
Definition: SiteInlineAjaxController.php:38
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController\createExecutableStringRepresentationOfRegisteredRequireJsModules
‪array createExecutableStringRepresentationOfRegisteredRequireJsModules(array $result)
Definition: AbstractFormEngineAjaxController.php:39
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:36
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:23
‪TYPO3\CMS\Core\Http\JsonResponse
Definition: JsonResponse.php:25
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:21
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController
Definition: AbstractFormEngineAjaxController.php:31
‪TYPO3\CMS\Backend\Form\InlineStackProcessor
Definition: InlineStackProcessor.php:29
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Backend\Form\FormDataCompiler
Definition: FormDataCompiler.php:24
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\__construct
‪__construct()
Definition: SiteInlineAjaxController.php:42
‪TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup
Definition: SiteConfigurationDataGroup.php:31
‪TYPO3\CMS\Backend\Controller
Definition: AbstractFormEngineAjaxController.php:3
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\newInlineChildAction
‪ResponseInterface newInlineChildAction(ServerRequestInterface $request)
Definition: SiteInlineAjaxController.php:56