typoScriptService->convertTypoScriptArrayToPlainArray($configuration); $typoscriptSetup = $this->getTypoScriptSetup($request); $frameworkConfiguration = []; if (isset($typoscriptSetup['config.']['tx_extbase.'])) { $frameworkConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($typoscriptSetup['config.']['tx_extbase.']); } if (!isset($frameworkConfiguration['persistence']['storagePid'])) { $currentPageId = $this->getCurrentPageId($request); $frameworkConfiguration['persistence']['storagePid'] = $currentPageId; } // only merge $configuration and override controller configuration when retrieving configuration of the current plugin if ($extensionName === null || $extensionName === $extensionNameFromConfig && $pluginName === $pluginNameFromConfig) { $pluginConfiguration = $this->getPluginConfiguration($request, (string)$extensionNameFromConfig, (string)$pluginNameFromConfig); ArrayUtility::mergeRecursiveWithOverrule($pluginConfiguration, $configuration); $pluginConfiguration['controllerConfiguration'] = []; } else { $pluginConfiguration = $this->getPluginConfiguration($request, $extensionName, (string)$pluginName); $pluginConfiguration['controllerConfiguration'] = []; } ArrayUtility::mergeRecursiveWithOverrule($frameworkConfiguration, $pluginConfiguration); if (!empty($frameworkConfiguration['persistence']['storagePid'])) { if (is_array($frameworkConfiguration['persistence']['storagePid'])) { // We simulate the frontend to enable the use of cObjects in // stdWrap. We then convert the configuration to normal TypoScript // and apply the stdWrap to the storagePid // Use makeInstance here since extbase Bootstrap always setContentObject(null) in Backend, no need to call getContentObject(). $conf = $this->typoScriptService->convertPlainArrayToTypoScriptArray($frameworkConfiguration['persistence']); $frameworkConfiguration['persistence']['storagePid'] = GeneralUtility::makeInstance(ContentObjectRenderer::class)->stdWrapValue('storagePid', $conf); } if (!empty($frameworkConfiguration['persistence']['recursive'])) { $storagePids = $this->getRecursiveStoragePids( GeneralUtility::intExplode(',', (string)($frameworkConfiguration['persistence']['storagePid'] ?? '')), (int)$frameworkConfiguration['persistence']['recursive'] ); $frameworkConfiguration['persistence']['storagePid'] = implode(',', $storagePids); } } return $frameworkConfiguration; } /** * Returns TypoScript Setup array from current Environment. * * @return array the raw TypoScript setup */ public function getTypoScriptSetup(ServerRequestInterface $request): array { $currentPageId = $this->getCurrentPageId($request); $cacheIdentifier = 'extbase-backend-typoscript-pageId-' . $currentPageId; $setupArray = $this->runtimeCache->get($cacheIdentifier); if (is_array($setupArray)) { return $setupArray; } $site = $request->getAttribute('site'); if (($site === null || $site instanceof NullSite) && $currentPageId > 0) { // Due to the weird magic of getting the pid of the first root template when // not having a pageId (extbase BE modules without page tree / no page selected), // we also have no proper site in this case. // So we try to get the site for this pageId. This way, site settings for this // first TS page are turned into constants and can be used in setup and setup // conditions. try { $site = $this->siteFinder->getSiteByPageId($currentPageId); } catch (SiteNotFoundException) { // Keep null / NullSite when no site could be determined for whatever reason. } } if ($site === null) { // If still no site object, have NullSite (usually pid 0). $site = new NullSite(); } $rootLine = []; $sysTemplateRows = []; if ($currentPageId > 0) { $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $currentPageId)->get(); // When the site acts as a TypoScript root, limit sys_template lookup to // pages within this site by truncating the rootline at the site root page. // This mirrors the frontend behavior and prevents sys_template records from // parent sites from leaking into the backend TypoScript evaluation. // @see \TYPO3\CMS\Frontend\Page\PageInformationFactory::setSysTemplateRows() $rootLineForSysTemplates = $rootLine; if ($site instanceof Site && $site->isTypoScriptRoot()) { $rootLineForSysTemplates = []; foreach ($rootLine as $index => $rootlinePage) { $rootLineForSysTemplates[$index] = $rootlinePage; if ((int)($rootlinePage['uid'] ?? 0) === $site->getRootPageId()) { break; } } } $sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLineForSysTemplates, $request); ksort($rootLine); } $sets = $site instanceof Site ? $this->setRegistry->getSets(...$site->getSets()) : []; if (empty($sysTemplateRows) && $sets === []) { // If no page with sys_template rows or site sets could be derived, we // "fake" a row to trigger inclusion of 'global' TypoScript only. $sysTemplateFakeRow = [ 'uid' => 0, 'pid' => 0, 'title' => 'Fake sys_template row to force global TypoScript loading', 'root' => 1, 'clear' => 3, 'include_static_file' => '', 'basedOn' => '', 'includeStaticAfterBasedOn' => 0, 'static_file_mode' => false, 'constants' => '', 'config' => '', 'deleted' => 0, 'hidden' => 0, 'starttime' => 0, 'endtime' => 0, 'sorting' => 0, ]; $sysTemplateRows[] = $sysTemplateFakeRow; } $expressionMatcherVariables = [ 'request' => $request, 'pageId' => $currentPageId, 'page' => !empty($rootLine) ? array_first($rootLine) : [], 'fullRootLine' => $rootLine, 'site' => $site, ]; $typoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions($site, $sysTemplateRows, $expressionMatcherVariables, $this->typoScriptCache); $typoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(true, $typoScript, $site, $sysTemplateRows, $expressionMatcherVariables, '0', $this->typoScriptCache, null); $setupArray = $typoScript->getSetupArray(); $this->runtimeCache->set($cacheIdentifier, $setupArray); return $setupArray; } /** * Returns the TypoScript configuration found in module.tx_yourextension_yourmodule * merged with the global configuration of your extension from module.tx_yourextension * * @param string|null $pluginName in BE mode this is actually the module signature. But we're using it just like the plugin name in FE */ private function getPluginConfiguration(ServerRequestInterface $request, string $extensionName, ?string $pluginName = null): array { $setup = $this->getTypoScriptSetup($request); $pluginConfiguration = []; if (is_array($setup['module.']['tx_' . strtolower($extensionName) . '.'] ?? false)) { $pluginConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['module.']['tx_' . strtolower($extensionName) . '.']); } if ($pluginName !== null) { $pluginSignature = strtolower($extensionName . '_' . $pluginName); if (is_array($setup['module.']['tx_' . $pluginSignature . '.'] ?? false)) { $overruleConfiguration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($setup['module.']['tx_' . $pluginSignature . '.']); ArrayUtility::mergeRecursiveWithOverrule($pluginConfiguration, $overruleConfiguration); } } return $pluginConfiguration; } /** * Get page id from the request, accessing POST / GET 'id' */ private function getCurrentPageId(ServerRequestInterface $request): int { // @todo: This misuses 'id' as a broken convention for pages-uid. The filelist module for instance // uses 'id' as "storage-uid:path", which is only mitigated here by testing the argument // with MU:canBeInterpretedAsInteger(). // This is in-line with a similar misuse in BackendModuleValidator. $id = 0; $potentialId = $request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0; if (MathUtility::canBeInterpretedAsInteger($potentialId) && $potentialId > 0) { $id = (int)$potentialId; } return $id; } /** * Returns an array of storagePIDs that are below a list of storage pids. * * @param int[] $storagePids Storage PIDs to start at; multiple PIDs possible as comma-separated list * @param int $recursionDepth Maximum number of levels to search, 0 to disable recursive lookup * @return int[] Uid list including the start $storagePids */ private function getRecursiveStoragePids(array $storagePids, int $recursionDepth = 0): array { if ($recursionDepth <= 0) { return $storagePids; } $permsClause = QueryHelper::stripLogicalOperatorPrefix( $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) ); $recursiveStoragePids = []; foreach ($storagePids as $startPid) { $startPid = abs($startPid); $recursiveStoragePids = array_merge( $recursiveStoragePids, [ $startPid ], $this->getPageChildrenRecursive($startPid, $recursionDepth, 0, $permsClause) ); } return array_unique($recursiveStoragePids); } /** * Recursively fetch all children of a given page * * @return int[] List of child row $uid's */ private function getPageChildrenRecursive(int $pid, int $depth, int $begin, string $permsClause): array { $children = []; if ($pid && $depth > 0) { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $statement = $queryBuilder->select('uid') ->from('pages') ->where( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)), $queryBuilder->expr()->eq('language_tag', 0), $permsClause ) ->orderBy('uid') ->executeQuery(); while ($row = $statement->fetchAssociative()) { if ($begin <= 0) { $children[] = (int)$row['uid']; } if ($depth > 1) { $theSubList = $this->getPageChildrenRecursive((int)$row['uid'], $depth - 1, $begin - 1, $permsClause); $children = array_merge($children, $theSubList); } } } return $children; } private function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }