getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); $domObjectId = $ajaxArguments[0]; $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); $childChildUid = null; if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) { $childChildUid = (int)$ajaxArguments[1]; } $siteTca = $this->siteTcaConfiguration->getTca(); $fullTca = array_merge($GLOBALS['TCA'], $siteTca); $tcaSchemata = $this->tcaSchemaBuilder->buildFromStructure($fullTca); // Parse the DOM identifier, add the levels to the structure stack $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $tcaSchemata); $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); // Parent, this table embeds the child table $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); // Child, a record from this table should be rendered $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); if (MathUtility::canBeInterpretedAsInteger($child['uid'] ?? false)) { // If uid comes in, it is the id of the record neighbor record "create after" $childVanillaUid = -1 * abs((int)$child['uid']); } else { // Else inline first Pid is the storage pid of new inline records $childVanillaUid = (int)$inlineFirstPid; } $childTableName = $parentConfig['foreign_table']; $defaultDatabaseRow = []; if ($childTableName === 'site_language') { if ($childChildUid !== null) { $language = $this->getLanguageById($childChildUid); if ($language !== null) { $defaultDatabaseRow['languageId'] = $language->getLanguageId(); $defaultDatabaseRow['locale'] = $language->getLocale()->posixFormatted(); if ($language->getTitle() !== '') { $defaultDatabaseRow['title'] = $language->getTitle(); } if ($language->getBase()->getPath() !== '/') { $defaultDatabaseRow['base'] = '/' . strtolower($language->getLocale()->getName()) . '/'; } if ($language->getHreflang(true) !== '') { $defaultDatabaseRow['hreflang'] = $language->getHreflang(); } if ($language->getNavigationTitle() !== '') { $defaultDatabaseRow['navigationTitle'] = $language->getNavigationTitle(); } if (str_starts_with($language->getFlagIdentifier(), 'flags-')) { $flagIdentifier = str_replace('flags-', '', $language->getFlagIdentifier()); $defaultDatabaseRow['flag'] = ($flagIdentifier === 'multiple') ? 'global' : $flagIdentifier; } } elseif ($childChildUid !== 0) { // In case no language could be found for $childChildUid and // its value is not "0", which is a special case as the default // language is added automatically, throw a custom exception. throw new \RuntimeException('Referenced language not found', 1521783937); } } else { // Set new childs' UID to PHP_INT_MAX, as this is the placeholder UID for // new records, created with the "Create new" button. This is necessary // as we use the "inline selector" mode which usually does not allow // to create new records besides the ones, defined in the selector. // The correct UID will then be calculated by the controller. $childChildUid = PHP_INT_MAX; if (!empty($ajaxArguments[2])) { $defaultDatabaseRow = $this->siteLanguagePresets->getPresetDetailsForLanguage($ajaxArguments[2]) ?? []; } } } $formDataCompilerInput = [ 'request' => $request, 'command' => 'new', 'tableName' => $childTableName, 'vanillaUid' => $childVanillaUid, 'databaseRow' => $defaultDatabaseRow, 'isInlineChild' => true, 'inlineStructure' => $inlineStructure, 'inlineFirstPid' => $inlineFirstPid, 'inlineParentUid' => $inlineParent['uid'], 'inlineParentTableName' => $inlineParent['table'], 'inlineParentFieldName' => $inlineParent['field'], 'inlineParentConfig' => $parentConfig, 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], 'tcaSchemata' => $tcaSchemata, 'fullTca' => $fullTca, ]; if ($childChildUid) { $formDataCompilerInput['inlineChildChildUid'] = $childChildUid; } $childData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)); if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { throw new \RuntimeException('useCombination not implemented in sites module', 1522493094); } $childData['inlineParentUid'] = (int)$inlineParent['uid']; $childData['renderType'] = 'inlineRecordContainer'; $childResult = $this->nodeFactory->create($childData)->render(); $jsonArray = [ 'data' => '', 'stylesheetFiles' => [], 'scriptItems' => new JavaScriptItems(), 'compilerInput' => [ 'uid' => $childData['databaseRow']['uid'], 'childChildUid' => $childChildUid, ], ]; $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); return new JsonResponse($jsonArray); } /** * Show the details of site configuration child records. * * @throws \RuntimeException */ public function openInlineChildAction(ServerRequestInterface $request): ResponseInterface { $ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax']; $domObjectId = $ajaxArguments[0]; $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); $siteTca = $this->siteTcaConfiguration->getTca(); $fullTca = array_merge($GLOBALS['TCA'], $siteTca); // Parse the DOM identifier, add the levels to the structure stack $inlineStructure = $this->inlineStackProcessor->getStructureFromString($domObjectId, $this->tcaSchemaBuilder->buildFromStructure($fullTca)); $inlineStructure = $this->inlineStackProcessor->addAjaxConfigurationToStructure($inlineStructure, $parentConfig); // Parent, this table embeds the child table $inlineParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, -1); $parentFieldName = $inlineParent['field']; // Set flag in config so that only the fields are rendered // @todo: Solve differently / rename / whatever $parentConfig['renderFieldsOnly'] = true; $parentData = [ 'processedTca' => [ 'columns' => [ $parentFieldName => [ 'config' => $parentConfig, ], ], ], 'uid' => $inlineParent['uid'], 'tableName' => $inlineParent['table'], 'inlineFirstPid' => $inlineFirstPid, // Hand over given original return url to compile stack. Needed if inline children compile links to // another view (eg. edit metadata in a nested inline situation like news with inline content element image), // so the back link is still the link from the original request. See issue #82525. This is additionally // given down in TcaInline data provider to compiled children data. 'returnUrl' => $parentConfig['originalReturnUrl'], ]; // Child, a record from this table should be rendered $child = $this->inlineStackProcessor->getUnstableStructureFromStructure($inlineStructure); $childData = $this->compileChild($request, $parentData, $parentFieldName, (int)$child['uid'], $inlineStructure, $siteTca); $childData['inlineParentUid'] = (int)$inlineParent['uid']; $childData['renderType'] = 'inlineRecordContainer'; $childResult = $this->nodeFactory->create($childData)->render(); $jsonArray = [ 'data' => '', 'stylesheetFiles' => [], 'scriptItems' => new JavaScriptItems(), ]; $jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult); return new JsonResponse($jsonArray); } /** * Compile a full child record * * @param array $parentData Result array of parent * @param string $parentFieldName Name of parent field * @param int $childUid Uid of child to compile * @param array $inlineStructure Current inline structure * @return array Full result array * @throws \RuntimeException * * @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction * @todo: to also encapsulate the more complex scenarios with combination child and friends. */ protected function compileChild(ServerRequestInterface $request, array $parentData, string $parentFieldName, int $childUid, array $inlineStructure, array $siteTca): array { $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config']; $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0); $childTableName = $inlineStructure['unstable']['table'] ?? null; if (!$childTableName) { throw new \RuntimeException('No unstable inline structure found', 1733754246); } $fullTca = array_merge($GLOBALS['TCA'], $siteTca); $formDataCompilerInput = [ 'request' => $request, 'command' => 'edit', 'tableName' => $childTableName, 'vanillaUid' => (int)$childUid, 'returnUrl' => $parentData['returnUrl'], 'isInlineChild' => true, 'inlineStructure' => $inlineStructure, 'inlineFirstPid' => $parentData['inlineFirstPid'], 'inlineParentConfig' => $parentConfig, 'isInlineAjaxOpeningContext' => true, 'tcaSchemata' => $this->tcaSchemaBuilder->buildFromStructure($fullTca), 'fullTca' => $fullTca, // values of the current parent element // it is always a string either an id or new... 'inlineParentUid' => $parentData['uid'], 'inlineParentTableName' => $parentData['tableName'], 'inlineParentFieldName' => $parentFieldName, // values of the top most parent element set on first level and not overridden on following levels 'inlineTopMostParentUid' => $inlineTopMostParent['uid'], 'inlineTopMostParentTableName' => $inlineTopMostParent['table'], 'inlineTopMostParentFieldName' => $inlineTopMostParent['field'], ]; if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { throw new \RuntimeException('useCombination not implemented in sites module', 1522493095); } return $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)); } /** * Merge stuff from child array into json array. * This method is needed since ajax handling methods currently need to put scriptCalls before and after child code. * * @param array $jsonResult Given json result * @param array $childResult Given child result * @return array Merged json array */ protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array { /** @var JavaScriptItems $scriptItems */ $scriptItems = $jsonResult['scriptItems']; $jsonResult['data'] .= $childResult['html']; $jsonResult['stylesheetFiles'] = []; foreach ($childResult['stylesheetFiles'] as $stylesheetFile) { $jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile); } if (!empty($childResult['inlineData'])) { $jsonResult['inlineData'] = $childResult['inlineData']; } if (!empty($childResult['additionalInlineLanguageLabelFiles'])) { $labels = []; foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) { ArrayUtility::mergeRecursiveWithOverrule( $labels, $this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile) ); } $scriptItems->addGlobalAssignment(['TYPO3' => ['lang' => $labels]]); } $this->addJavaScriptModulesToJavaScriptItems($childResult['javaScriptModules'] ?? [], $scriptItems); return $jsonResult; } /** * Inline ajax helper method. * * Validates the config that is transferred over the wire to provide the * correct TCA config for the parent table * * @param string $contextString * @throws \RuntimeException */ protected function extractSignedParentConfigFromRequest(string $contextString): array { if ($contextString === '') { throw new \RuntimeException('Empty context string given', 1522771624); } $context = json_decode($contextString, true); if (empty($context['config'])) { throw new \RuntimeException('Empty context config section given', 1522771632); } $config = json_decode($context['config'], true); // encode JSON again to ensure same `json_encode()` settings as used when generating original hash // (side-note: JSON encoded literals differ for target scenarios, e.g. HTML attr, JS string, ...) $encodedConfig = (string)json_encode($config); if (!hash_equals($this->hashService->hmac($encodedConfig, 'InlineContext'), (string)$context['hmac'])) { throw new \RuntimeException('Hash does not validate', 1522771640); } return $config; } /** * Get inlineFirstPid from a given objectId string * * @param string $domObjectId The id attribute of an element * @return int|null Pid or null */ protected function getInlineFirstPidFromDomObjectId(string $domObjectId): ?int { // Substitute FlexForm addition and make parsing a bit easier $domObjectId = str_replace('---', ':', $domObjectId); // The starting pattern of an object identifier (e.g. "data--) $pattern = '/^data-(.+?)-(.+)$/'; if (preg_match($pattern, $domObjectId, $match)) { return (int)$match[1]; } return null; } /** * Find a site language by id. This will return the first occurrence of a * language, even if the same language is used in other site configurations. */ protected function getLanguageById(int $languageId): ?SiteLanguage { foreach ($this->siteFinder->getAllSites() as $site) { foreach ($site->getAllLanguages() as $language) { if ($languageId === $language->getLanguageId()) { return $language; } } } return null; } }