determinePageId($parameters['table'], $parameters['row']) ?: 0; $pageTsConfig = BackendUtility::getPagesTSconfig($pageId); $identifiersToBeExcluded = []; if (isset($pageTsConfig['options.']['backendLayout.']['exclude'])) { $identifiersToBeExcluded = GeneralUtility::trimExplode(',', $pageTsConfig['options.']['backendLayout.']['exclude'], true); } $dataProviderContext = new DataProviderContext( pageId: $pageId, tableName: $parameters['table'], fieldName: $parameters['field'], data: $parameters['row'], pageTsConfig: $pageTsConfig, ); $backendLayoutCollections = $this->dataProviderCollection->getBackendLayoutCollections($dataProviderContext); foreach ($backendLayoutCollections as $backendLayoutCollection) { $combinedIdentifierPrefix = ''; if ($backendLayoutCollection->getIdentifier() !== 'default') { $combinedIdentifierPrefix = $backendLayoutCollection->getIdentifier() . '__'; } foreach ($backendLayoutCollection->getAll() as $backendLayout) { $combinedIdentifier = $combinedIdentifierPrefix . $backendLayout->getIdentifier(); if (in_array($combinedIdentifier, $identifiersToBeExcluded, true)) { continue; } $parameters['items'][] = [ 'label' => $backendLayout->getTitle(), 'value' => $combinedIdentifier, 'icon' => $backendLayout->getIconPath(), ]; } } } /** * Gets colPos items to be shown in form engine. This method is called * as "itemsProcFunc" with the accordant context for tt_content.colPos. */ public function colPosListItemProcFunc(array &$parameters): void { $pageId = $this->determinePageId($parameters['table'], $parameters['row']); if ($pageId !== false) { $layout = $this->getSelectedBackendLayout($pageId); if ($layout && !empty($layout['__items'])) { $parameters['items'] = $layout['__items']; } } } /** * Gets the selected backend layout structure as an array */ public function getSelectedBackendLayout(int $pageId): array { return $this->getBackendLayoutForPage($pageId)->getStructure(); } /** * Get the BackendLayout object and parse the structure based on the UserTSconfig */ public function getBackendLayoutForPage(int $pageId): BackendLayout { $selectedBackendLayoutsByPageId = $this->runtimeCache->get(self::SELECTED_BACKEND_LAYOUTS_CACHE_IDENTIFIER); if (($selectedBackendLayoutsByPageId[$pageId] ?? null) instanceof BackendLayout) { return $selectedBackendLayoutsByPageId[$pageId]; } if (!is_array($selectedBackendLayoutsByPageId)) { $selectedBackendLayoutsByPageId = []; } $selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId); if (empty($selectedCombinedIdentifier)) { // If no backend layout is selected, use default $selectedCombinedIdentifier = 'default'; } $backendLayout = $this->dataProviderCollection->getBackendLayout($selectedCombinedIdentifier, $pageId); if ($backendLayout === null) { // If backend layout is not found available anymore, use default $backendLayout = $this->dataProviderCollection->getBackendLayout('default', $pageId); } if ($backendLayout === null) { // The 'default' backend layout must *always* return something. We must have some layout at this // point and the method always returns a BackendLayout instance. throw new \RuntimeException('Fallback to default backend layout failed', 1768151069); } $selectedBackendLayoutsByPageId[$pageId] = $backendLayout; $this->runtimeCache->set(self::SELECTED_BACKEND_LAYOUTS_CACHE_IDENTIFIER, $selectedBackendLayoutsByPageId); return $backendLayout; } /** * This method is mainly used to retrieve the final allowed/disallowed content element configuration per colPos. It * is an implementation of what ext:content_defender provided as extension for a long time already. This method is * embedded in the overall rather convoluted handling around backend layouts. It is - at least for now - subject to change. * The method emits an event that is declared internal as well. * * When calling the method, the optional request argument should be hand whenever available to give event listeners * as much context as possible. * * @internal as the entire class. Note ManipulateBackendLayoutColPosConfigurationForPageEvent is declared internal * as well. The event is needed for extensions like ext:container, but may still change when backend layout * related code is consolidated. */ public function getColPosConfigurationForPage(BackendLayout $backendLayout, int $colPos, int $pageUid, ?ServerRequestInterface $request = null): array { $configuration = []; $backendLayoutStructure = $backendLayout->getStructure(); if (in_array($colPos, array_map('intval', $backendLayoutStructure['__colPosList']), true)) { foreach ($backendLayoutStructure['__config']['backend_layout.']['rows.'] as $row) { if (empty($row['columns.'])) { continue; } foreach ($row['columns.'] as $column) { if (isset($column['colPos']) && $column['colPos'] !== '' && $colPos === (int)$column['colPos']) { $configuration = $column; // Compatibility layer for ext:content_defender: allowed.CType is now allowedContentTypes and // disallowed.CType is now disallowedContentTypes. Copy the old content_defender settings to // the new names if new names are not set. // @todo: These fallbacks could potentially be deprecated at some point. Maybe v15? if (!empty($configuration['allowed.']['CType'] ?? '') && !isset($configuration['allowedContentTypes'])) { $configuration['allowedContentTypes'] = $configuration['allowed.']['CType']; } if (!empty($configuration['disallowed.']['CType'] ?? '') && !isset($configuration['disallowedContentTypes'])) { $configuration['disallowedContentTypes'] = $configuration['disallowed.']['CType']; } break 2; } } } } $event = new ManipulateBackendLayoutColPosConfigurationForPageEvent( configuration: $configuration, backendLayout: $backendLayout, colPos: $colPos, pageUid: $pageUid, request: $request, ); return $this->eventDispatcher->dispatch($event)->configuration; } /** * @internal */ public function parseStructure(BackendLayout $backendLayout): array { $typoScriptTree = $this->typoScriptStringFactory->parseFromStringWithIncludes('backend-layout', $backendLayout->getConfiguration()); $backendLayoutData = []; $backendLayoutData['config'] = $backendLayout->getConfiguration(); $backendLayoutData['__config'] = $typoScriptTree->toArray(); $backendLayoutData['__items'] = []; $backendLayoutData['__colPosList'] = []; $backendLayoutData['usedColumns'] = []; $backendLayoutData['colCount'] = (int)($backendLayoutData['__config']['backend_layout.']['colCount'] ?? 0); $backendLayoutData['rowCount'] = (int)($backendLayoutData['__config']['backend_layout.']['rowCount'] ?? 0); // create items and colPosList if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) { $rows = $backendLayoutData['__config']['backend_layout.']['rows.']; ksort($rows); foreach ($rows as $row) { if (!empty($row['columns.'])) { foreach ($row['columns.'] as $column) { if (!isset($column['colPos'])) { continue; } $backendLayoutData['__items'][] = [ 'label' => $column['name'], 'value' => $column['colPos'], 'icon' => null, ]; $backendLayoutData['__colPosList'][] = $column['colPos']; $backendLayoutData['usedColumns'][(int)$column['colPos']] = $column['name']; } } } } return $backendLayoutData; } public function isCTypeAllowedInColPosByPage(string $cType, int $colPos, int $pageUid): bool { $backendLayout = $this->getBackendLayoutForPage($pageUid); $colPosConfiguration = $this->getColPosConfigurationForPage($backendLayout, $colPos, $pageUid); if (!empty($colPosConfiguration['disallowedContentTypes'])) { $disallowedContentTypes = GeneralUtility::trimExplode(',', $colPosConfiguration['disallowedContentTypes']); if (in_array($cType, $disallowedContentTypes, true)) { return false; } } if (!empty($colPosConfiguration['allowedContentTypes'])) { $allowedContentTypes = GeneralUtility::trimExplode(',', $colPosConfiguration['allowedContentTypes']); if (!in_array($cType, $allowedContentTypes, true)) { return false; } } return true; } /** * Determines the page id for a given record of a database table. * * @return int|false Returns page id or false on error */ protected function determinePageId(string $tableName, array $data): int|false { if ($data === []) { return false; } if (str_starts_with((string)$data['uid'], 'NEW')) { // Negative uid_pid values of content elements indicate that the element // has been inserted after an existing element so there is no pid to get // the backendLayout for, and we have to get that first. if ($data['pid'] < 0) { $queryBuilder = $this->connectionPool ->getQueryBuilderForTable($tableName); $queryBuilder->getRestrictions() ->removeAll(); $pageId = $queryBuilder ->select('pid') ->from($tableName) ->where( $queryBuilder->expr()->eq( 'uid', $queryBuilder->createNamedParameter(abs($data['pid']), Connection::PARAM_INT) ) ) ->executeQuery() ->fetchOne(); } else { $pageId = $data['pid']; } } elseif ($tableName === 'pages') { $pageId = $data['uid']; } else { $pageId = $data['pid']; } return (int)$pageId; } /** * Returns the backend layout which should be used for this page. * * @return false|string Identifier of the backend layout to be used, or FALSE if none */ protected function getSelectedCombinedIdentifier(int $pageId): string|false { $selectedCombinedIdentifiers = $this->runtimeCache->get(self::SELECTED_COMBINED_CACHE_IDENTIFIER); if (is_array($selectedCombinedIdentifiers) && array_key_exists($pageId, $selectedCombinedIdentifiers)) { return $selectedCombinedIdentifiers[$pageId]; } if (!is_array($selectedCombinedIdentifiers)) { $selectedCombinedIdentifiers = []; } // If it not set check the rootline for a layout on next level and use this: Rootline // starts with current page and has page "0" at the end. $rootLine = BackendUtility::BEgetRootLine($pageId, '', true); if ($rootLine === []) { // Return for invalid rootline return false; } // Use first element as current page and remove last element (root page / pid=0) $page = reset($rootLine); array_pop($rootLine); $selectedLayout = $this->pageLayoutResolver->getLayoutIdentifierForPage($page, $rootLine); if ($selectedLayout === 'none') { // If it is set to "none" - don't use any $selectedLayout = false; } elseif ($selectedLayout === 'default') { $selectedLayout = '0'; } $selectedCombinedIdentifiers[$pageId] = $selectedLayout; $this->runtimeCache->set(self::SELECTED_COMBINED_CACHE_IDENTIFIER, $selectedCombinedIdentifiers); return $selectedLayout; } }