‪TYPO3CMS  ‪main
SiteInlineAjaxController.php
Go to the documentation of this file.
1 <?php
2 
3 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 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
30 use TYPO3\CMS\Core\Page\JavaScriptItems;
37 
44 #[AsController]
46 {
50  public function ‪__construct(
51  private readonly ‪FormDataCompiler $formDataCompiler,
52  private readonly ‪SiteLanguagePresets $siteLanguagePresets,
53  private readonly ‪HashService $hashService,
54  ) {
55  // Bring site TCA into global scope.
56  // @todo: We might be able to get rid of that later
57  ‪$GLOBALS['TCA'] = array_merge(‪$GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
58  }
59 
65  public function ‪newInlineChildAction(ServerRequestInterface $request): ResponseInterface
66  {
67  $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
68  $parentConfig = $this->‪extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
69  $domObjectId = $ajaxArguments[0];
70  $inlineFirstPid = $this->‪getInlineFirstPidFromDomObjectId($domObjectId);
71  $childChildUid = null;
72  if (isset($ajaxArguments[1]) && ‪MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
73  $childChildUid = (int)$ajaxArguments[1];
74  }
75  // Parse the DOM identifier, add the levels to the structure stack
76  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
77  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
78  $inlineStackProcessor->setAjaxConfiguration($parentConfig);
79  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
80  // Parent, this table embeds the child table
81  $parent = $inlineStackProcessor->getStructureLevel(-1);
82  // Child, a record from this table should be rendered
83  $child = $inlineStackProcessor->getUnstableStructure();
84  if (‪MathUtility::canBeInterpretedAsInteger($child['uid'] ?? false)) {
85  // If uid comes in, it is the id of the record neighbor record "create after"
86  $childVanillaUid = -1 * abs((int)$child['uid']);
87  } else {
88  // Else inline first Pid is the storage pid of new inline records
89  $childVanillaUid = (int)$inlineFirstPid;
90  }
91  $childTableName = $parentConfig['foreign_table'];
92  $defaultDatabaseRow = [];
93 
94  if ($childTableName === 'site_language') {
95  if ($childChildUid !== null) {
96  $language = $this->‪getLanguageById($childChildUid);
97  if ($language !== null) {
98  $defaultDatabaseRow['languageId'] = $language->getLanguageId();
99  $defaultDatabaseRow['locale'] = $language->getLocale()->posixFormatted();
100  if ($language->getTitle() !== '') {
101  $defaultDatabaseRow['title'] = $language->getTitle();
102  }
103  if ($language->getBase()->getPath() !== '/') {
104  $defaultDatabaseRow['base'] = '/' . strtolower($language->getLocale()->getName()) . '/';
105  }
106  if ($language->getHreflang(true) !== '') {
107  $defaultDatabaseRow['hreflang'] = $language->getHreflang();
108  }
109  if ($language->getNavigationTitle() !== '') {
110  $defaultDatabaseRow['navigationTitle'] = $language->getNavigationTitle();
111  }
112  if (str_starts_with($language->getFlagIdentifier(), 'flags-')) {
113  $flagIdentifier = str_replace('flags-', '', $language->getFlagIdentifier());
114  $defaultDatabaseRow['flag'] = ($flagIdentifier === 'multiple') ? 'global' : $flagIdentifier;
115  }
116  } elseif ($childChildUid !== 0) {
117  // In case no language could be found for $childChildUid and
118  // its value is not "0", which is a special case as the default
119  // language is added automatically, throw a custom exception.
120  throw new \RuntimeException('Referenced language not found', 1521783937);
121  }
122  } else {
123  // Set new childs' UID to PHP_INT_MAX, as this is the placeholder UID for
124  // new records, created with the "Create new" button. This is necessary
125  // as we use the "inline selector" mode which usually does not allow
126  // to create new records besides the ones, defined in the selector.
127  // The correct UID will then be calculated by the controller.
128  $childChildUid = PHP_INT_MAX;
129 
130  if (!empty($ajaxArguments[2])) {
131  $defaultDatabaseRow = $this->siteLanguagePresets->getPresetDetailsForLanguage($ajaxArguments[2]) ?? [];
132  }
133  }
134  }
135 
136  $formDataCompilerInput = [
137  'request' => $request,
138  'command' => 'new',
139  'tableName' => $childTableName,
140  'vanillaUid' => $childVanillaUid,
141  'databaseRow' => $defaultDatabaseRow,
142  'isInlineChild' => true,
143  'inlineStructure' => $inlineStackProcessor->getStructure(),
144  'inlineFirstPid' => $inlineFirstPid,
145  'inlineParentUid' => $parent['uid'],
146  'inlineParentTableName' => $parent['table'],
147  'inlineParentFieldName' => $parent['field'],
148  'inlineParentConfig' => $parentConfig,
149  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
150  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
151  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
152  ];
153  if ($childChildUid) {
154  $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
155  }
156  $childData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class));
157 
158  if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) {
159  throw new \RuntimeException('useCombination not implemented in sites module', 1522493094);
160  }
161 
162  $childData['inlineParentUid'] = (int)$parent['uid'];
163  $childData['renderType'] = 'inlineRecordContainer';
164  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
165  $childResult = $nodeFactory->create($childData)->render();
166 
167  $jsonArray = [
168  'data' => '',
169  'stylesheetFiles' => [],
170  'scriptItems' => GeneralUtility::makeInstance(JavaScriptItems::class),
171  'compilerInput' => [
172  'uid' => $childData['databaseRow']['uid'],
173  'childChildUid' => $childChildUid,
174  'parentConfig' => $parentConfig,
175  ],
176  ];
177 
178  $jsonArray = $this->‪mergeChildResultIntoJsonResult($jsonArray, $childResult);
179 
180  return new ‪JsonResponse($jsonArray);
181  }
182 
188  public function ‪openInlineChildAction(ServerRequestInterface $request): ResponseInterface
189  {
190  $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
191 
192  $domObjectId = $ajaxArguments[0];
193  $inlineFirstPid = $this->‪getInlineFirstPidFromDomObjectId($domObjectId);
194  $parentConfig = $this->‪extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
195 
196  // Parse the DOM identifier, add the levels to the structure stack
197  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
198  $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
199  $inlineStackProcessor->setAjaxConfiguration($parentConfig);
200 
201  // Parent, this table embeds the child table
202  $parent = $inlineStackProcessor->getStructureLevel(-1);
203  $parentFieldName = $parent['field'];
204 
205  // Set flag in config so that only the fields are rendered
206  // @todo: Solve differently / rename / whatever
207  $parentConfig['renderFieldsOnly'] = true;
208 
209  $parentData = [
210  'processedTca' => [
211  'columns' => [
212  $parentFieldName => [
213  'config' => $parentConfig,
214  ],
215  ],
216  ],
217  'uid' => $parent['uid'],
218  'tableName' => $parent['table'],
219  'inlineFirstPid' => $inlineFirstPid,
220  // Hand over given original return url to compile stack. Needed if inline children compile links to
221  // another view (eg. edit metadata in a nested inline situation like news with inline content element image),
222  // so the back link is still the link from the original request. See issue #82525. This is additionally
223  // given down in TcaInline data provider to compiled children data.
224  'returnUrl' => $parentConfig['originalReturnUrl'],
225  ];
226 
227  // Child, a record from this table should be rendered
228  $child = $inlineStackProcessor->getUnstableStructure();
229 
230  $childData = $this->‪compileChild($request, $parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
231 
232  $childData['inlineParentUid'] = (int)$parent['uid'];
233  $childData['renderType'] = 'inlineRecordContainer';
234  $nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
235  $childResult = $nodeFactory->create($childData)->render();
236 
237  $jsonArray = [
238  'data' => '',
239  'stylesheetFiles' => [],
240  'scriptItems' => GeneralUtility::makeInstance(JavaScriptItems::class),
241  ];
242 
243  $jsonArray = $this->‪mergeChildResultIntoJsonResult($jsonArray, $childResult);
244 
245  return new ‪JsonResponse($jsonArray);
246  }
247 
261  protected function ‪compileChild(ServerRequestInterface $request, array $parentData, string $parentFieldName, int $childUid, array $inlineStructure): array
262  {
263  $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
264 
265  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
266  $inlineStackProcessor->initializeByGivenStructure($inlineStructure);
267  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
268 
269  // @todo: do not use stack processor here ...
270  $child = $inlineStackProcessor->getUnstableStructure();
271  $childTableName = $child['table'];
272 
273  $formDataCompilerInput = [
274  'request' => $request,
275  'command' => 'edit',
276  'tableName' => $childTableName,
277  'vanillaUid' => (int)$childUid,
278  'returnUrl' => $parentData['returnUrl'],
279  'isInlineChild' => true,
280  'inlineStructure' => $inlineStructure,
281  'inlineFirstPid' => $parentData['inlineFirstPid'],
282  'inlineParentConfig' => $parentConfig,
283  'isInlineAjaxOpeningContext' => true,
284 
285  // values of the current parent element
286  // it is always a string either an id or new...
287  'inlineParentUid' => $parentData['uid'],
288  'inlineParentTableName' => $parentData['tableName'],
289  'inlineParentFieldName' => $parentFieldName,
290 
291  // values of the top most parent element set on first level and not overridden on following levels
292  'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
293  'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
294  'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
295  ];
296  if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) {
297  throw new \RuntimeException('useCombination not implemented in sites module', 1522493095);
298  }
299  return $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class));
300  }
301 
310  protected function ‪mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array
311  {
313  $scriptItems = $jsonResult['scriptItems'];
314 
315  $jsonResult['data'] .= $childResult['html'];
316  $jsonResult['stylesheetFiles'] = [];
317  foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
318  $jsonResult['stylesheetFiles'][] = $this->‪getRelativePathToStylesheetFile($stylesheetFile);
319  }
320  if (!empty($childResult['inlineData'])) {
321  $jsonResult['inlineData'] = $childResult['inlineData'];
322  }
323  if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
324  $labels = [];
325  foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
326  ArrayUtility::mergeRecursiveWithOverrule(
327  $labels,
328  $this->‪getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
329  );
330  }
331  $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]);
332  }
333  $this->‪addJavaScriptModulesToJavaScriptItems($childResult['javaScriptModules'] ?? [], $scriptItems);
334 
335  return $jsonResult;
336  }
337 
347  protected function ‪extractSignedParentConfigFromRequest(string $contextString): array
348  {
349  if ($contextString === '') {
350  throw new \RuntimeException('Empty context string given', 1522771624);
351  }
352  $context = json_decode($contextString, true);
353  if (empty($context['config'])) {
354  throw new \RuntimeException('Empty context config section given', 1522771632);
355  }
356  $config = json_decode($context['config'], true);
357  // encode JSON again to ensure same `json_encode()` settings as used when generating original hash
358  // (side-note: JSON encoded literals differ for target scenarios, e.g. HTML attr, JS string, ...)
359  $encodedConfig = (string)json_encode($config);
360  if (!hash_equals($this->hashService->hmac($encodedConfig, 'InlineContext'), (string)$context['hmac'])) {
361  throw new \RuntimeException('Hash does not validate', 1522771640);
362  }
363  return $config;
364  }
365 
372  protected function ‪getInlineFirstPidFromDomObjectId(string $domObjectId): ?int
373  {
374  // Substitute FlexForm addition and make parsing a bit easier
375  $domObjectId = str_replace('---', ':', $domObjectId);
376  // The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
377  $pattern = '/^data-(.+?)-(.+)$/';
378  if (preg_match($pattern, $domObjectId, $match)) {
379  return (int)$match[1];
380  }
381  return null;
382  }
383 
388  protected function ‪getLanguageById(int $languageId): ?‪SiteLanguage
389  {
390  foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
391  foreach ($site->getAllLanguages() as $language) {
392  if ($languageId === $language->getLanguageId()) {
393  return $language;
394  }
395  }
396  }
397 
398  return null;
399  }
400 }
‪TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration
Definition: SiteTcaConfiguration.php:34
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\extractSignedParentConfigFromRequest
‪extractSignedParentConfigFromRequest(string $contextString)
Definition: SiteInlineAjaxController.php:347
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\newInlineChildAction
‪newInlineChildAction(ServerRequestInterface $request)
Definition: SiteInlineAjaxController.php:65
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\mergeChildResultIntoJsonResult
‪array mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
Definition: SiteInlineAjaxController.php:310
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\getInlineFirstPidFromDomObjectId
‪int null getInlineFirstPidFromDomObjectId(string $domObjectId)
Definition: SiteInlineAjaxController.php:372
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController
Definition: SiteInlineAjaxController.php:46
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController\getLabelsFromLocalizationFile
‪array getLabelsFromLocalizationFile(string $file)
Definition: AbstractFormEngineAjaxController.php:80
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController\getRelativePathToStylesheetFile
‪string getRelativePathToStylesheetFile(string $stylesheetFile)
Definition: AbstractFormEngineAjaxController.php:60
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController\addJavaScriptModulesToJavaScriptItems
‪addJavaScriptModulesToJavaScriptItems(array $modules, JavaScriptItems $items)
Definition: AbstractFormEngineAjaxController.php:37
‪TYPO3\CMS\Backend\Form\NodeFactory
Definition: NodeFactory.php:40
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\Http\JsonResponse
Definition: JsonResponse.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Site\SiteLanguagePresets
Definition: SiteLanguagePresets.php:25
‪TYPO3\CMS\Backend\Attribute\AsController
Definition: AsController.php:25
‪TYPO3\CMS\Backend\Controller\AbstractFormEngineAjaxController
Definition: AbstractFormEngineAjaxController.php:36
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\compileChild
‪array compileChild(ServerRequestInterface $request, array $parentData, string $parentFieldName, int $childUid, array $inlineStructure)
Definition: SiteInlineAjaxController.php:261
‪TYPO3\CMS\Backend\Form\InlineStackProcessor
Definition: InlineStackProcessor.php:32
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\openInlineChildAction
‪openInlineChildAction(ServerRequestInterface $request)
Definition: SiteInlineAjaxController.php:188
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Form\FormDataCompiler
Definition: FormDataCompiler.php:26
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\getLanguageById
‪getLanguageById(int $languageId)
Definition: SiteInlineAjaxController.php:388
‪TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup
Definition: SiteConfigurationDataGroup.php:33
‪TYPO3\CMS\Backend\Controller\SiteInlineAjaxController\__construct
‪__construct(private readonly FormDataCompiler $formDataCompiler, private readonly SiteLanguagePresets $siteLanguagePresets, private readonly HashService $hashService,)
Definition: SiteInlineAjaxController.php:50
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Backend\Controller
Definition: AboutController.php:18