TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Context;
|
||||
|
||||
use TYPO3\CMS\Backend\Domain\Model\Language\PageLanguageInformation;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
|
||||
/**
|
||||
* Generic page context for all backend modules working with pages ("id" parameter and page-tree navigation component).
|
||||
*
|
||||
* This context is added to the backend request by the PSR-15 "PageContextInitialization" middleware.
|
||||
* Replaces the module-specific duplication of language handling, page information, and site context.
|
||||
*
|
||||
* Contains shared data needed across Layout Module, Records Module, etc.
|
||||
* Does NOT contain module-specific rendering configuration.
|
||||
*
|
||||
* This is a DOMAIN object and should NOT contain HTTP infrastructure concerns like ServerRequestInterface.
|
||||
*
|
||||
* Access Handling:
|
||||
* If the user has no access to the requested page, pageRecord will be null.
|
||||
* Controllers should check $pageContext->isAccessible() before processing.
|
||||
*
|
||||
* Usage:
|
||||
* $pageContext = $request->getAttribute('pageContext');
|
||||
* if (!$pageContext->isAccessible()) {
|
||||
* // Show no access page
|
||||
* return $view->renderResponse('NoAccess');
|
||||
* }
|
||||
* $selectedLanguages = $pageContext->selectedLanguageIds;
|
||||
* $languageInfo = $pageContext->languageInformation;
|
||||
* $rootLine = $pageContext->rootLine;
|
||||
* $pageTsConfig = $pageContext->pageTsConfig;
|
||||
* $moduleTsConfig = $pageContext->getModuleTsConfig('web_layout');
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class PageContext
|
||||
{
|
||||
/**
|
||||
* @param int $pageId Page ID (always preserved, even if no access)
|
||||
* @param ?array $pageRecord Page record from readPageAccess (null if no access)
|
||||
* @param int[] $selectedLanguageIds Selected language IDs (resolved and validated)
|
||||
* @param PageLanguageInformation $languageInformation Complete language information for this page
|
||||
* @param array $rootLine Page rootline including the page itself (empty array if no access)
|
||||
* @param array $pageTsConfig PageTSconfig array (dots removed, overlaid by user permissions, falls back to page 0 if no access)
|
||||
* @param Permission $pagePermissions User's permissions for this page (calculated from backendUser->calcPerms)
|
||||
*/
|
||||
public function __construct(
|
||||
public int $pageId,
|
||||
public ?array $pageRecord,
|
||||
public SiteInterface $site,
|
||||
public array $rootLine,
|
||||
public array $pageTsConfig,
|
||||
public array $selectedLanguageIds,
|
||||
public PageLanguageInformation $languageInformation,
|
||||
public Permission $pagePermissions,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check if user has access to the page.
|
||||
*
|
||||
* Returns false if user has no access to the requested page.
|
||||
* Controllers should check this before processing page-specific operations.
|
||||
*/
|
||||
public function isAccessible(): bool
|
||||
{
|
||||
return $this->pageRecord !== null && $this->pagePermissions->showPagePermissionIsGranted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary selected language for single-language views.
|
||||
*
|
||||
* Logic:
|
||||
* - If exactly 1 non-default language is selected → use that translation
|
||||
* - If 0 or 2+ non-default languages are selected → use default (0)
|
||||
*
|
||||
* This ensures that when switching from multi-language to single-language view,
|
||||
* the user's focused translation is preserved (when they had one selected).
|
||||
*
|
||||
* @return int Primary language ID
|
||||
*/
|
||||
public function getPrimaryLanguageId(): int
|
||||
{
|
||||
$nonDefaultLanguages = array_filter($this->selectedLanguageIds, static fn(int $id): bool => $id > 0);
|
||||
if (count($nonDefaultLanguages) === 1) {
|
||||
return reset($nonDefaultLanguages);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if multiple languages are currently selected.
|
||||
*
|
||||
* This is useful for determining if comparison/multi-column view should be shown.
|
||||
*/
|
||||
public function hasMultipleLanguagesSelected(): bool
|
||||
{
|
||||
return count($this->selectedLanguageIds) > 1;
|
||||
}
|
||||
|
||||
public function isLanguageSelected(int $languageId): bool
|
||||
{
|
||||
return in_array($languageId, $this->selectedLanguageIds, true);
|
||||
}
|
||||
|
||||
public function isDefaultLanguageSelected(): bool
|
||||
{
|
||||
return $this->isLanguageSelected(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title (localized if translation exists).
|
||||
*
|
||||
* @param int|null $languageId Language ID (null = primary selected language)
|
||||
*/
|
||||
public function getPageTitle(?int $languageId = null): string
|
||||
{
|
||||
$languageId ??= $this->getPrimaryLanguageId();
|
||||
|
||||
if ($languageId === 0) {
|
||||
return $this->pageRecord['title'] ?? '';
|
||||
}
|
||||
|
||||
$translation = $this->languageInformation->getTranslationRecord($languageId);
|
||||
return $translation['title'] ?? $this->pageRecord['title'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a convenience method to easily access mod.{module}.* configuration.
|
||||
*/
|
||||
public function getModuleTsConfig(string $module): array
|
||||
{
|
||||
return is_array($this->pageTsConfig['mod'][$module] ?? false) ? $this->pageTsConfig['mod'][$module] : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Context;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Module\ModuleData;
|
||||
use TYPO3\CMS\Backend\Service\PageLanguageInformationService;
|
||||
use TYPO3\CMS\Backend\User\SharedUserPreferences;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Factory for creating PageContext instances.
|
||||
*
|
||||
* This is the SINGLE entry point for creating page contexts across all backend modules.
|
||||
* It centralizes the logic for:
|
||||
* - Resolving language selection with fallback chain
|
||||
* - Validating languages against available languages
|
||||
* - Permission checks
|
||||
* - Fetching language information
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class PageContextFactory
|
||||
{
|
||||
public function __construct(
|
||||
private SharedUserPreferences $sharedPreferences,
|
||||
private PageLanguageInformationService $languageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create PageContext from request and page ID.
|
||||
*
|
||||
* This method:
|
||||
* 1. Validates page access (returns context with null pageRecord if no access)
|
||||
* 2. Fetches language information for the page
|
||||
* 3. Resolves selected languages with fallback chain
|
||||
* 4. Validates selected languages against existing translations on this page
|
||||
* 5. Falls back to default language if no valid languages selected
|
||||
* 6. Stores preference if explicitly changed via request (preserves across pages)
|
||||
* 7. Creates and returns the PageContext
|
||||
*
|
||||
* Language validation ensures that only languages with actual translations on
|
||||
* the current page are included in selectedLanguageIds. This guarantees that
|
||||
* getPrimaryLanguageId() always returns a valid language for the current page.
|
||||
*
|
||||
* User preferences are preserved: selecting L=1 on PageA stores the preference,
|
||||
* navigating to PageB without L=1 shows L=0, returning to PageA restores L=1.
|
||||
*
|
||||
* Access Handling:
|
||||
* If the user has no access to the requested page or pid=0, a PageContext is still returned,
|
||||
* while pageRecord mit be null if no access. Controllers should check isAccessible().
|
||||
*
|
||||
* @param int $pageId Page ID to create context for
|
||||
*/
|
||||
public function createFromRequest(
|
||||
ServerRequestInterface $request,
|
||||
int $pageId,
|
||||
BackendUserAuthentication $backendUser
|
||||
): PageContext {
|
||||
$site = $request->getAttribute('site');
|
||||
if (!$site instanceof SiteInterface) {
|
||||
throw new SiteNotFoundException('No site found in request', 1731234567);
|
||||
}
|
||||
|
||||
// Check page access
|
||||
$pageRecord = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: null;
|
||||
if ($pageId === 0 || !$pageRecord) {
|
||||
// Either root page (pid=0) which has no real page record or no access.
|
||||
// Return context with preserved pageId.
|
||||
// pageRecord might be ['path' => '/'] for admins or NULL if no access or non-admin
|
||||
// Still calculate permissions (admins have access to pid=0, editors don't).
|
||||
return new PageContext(
|
||||
pageId: $pageId,
|
||||
pageRecord: $pageRecord,
|
||||
site: $site,
|
||||
rootLine: [],
|
||||
pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig(0)),
|
||||
selectedLanguageIds: [0],
|
||||
languageInformation: $this->languageService->getLanguageInformationForPage(0, $site, $backendUser),
|
||||
pagePermissions: new Permission($backendUser->calcPerms($pageRecord ?: ['uid' => 0])),
|
||||
);
|
||||
}
|
||||
|
||||
// Get language information FIRST (needed for validation)
|
||||
$languageInformation = $this->languageService->getLanguageInformationForPage($pageId, $site, $backendUser);
|
||||
|
||||
// Resolve languages with fallback chain
|
||||
$languagesFromRequest = $request->getQueryParams()['languages'] ?? $request->getParsedBody()['languages'] ?? null;
|
||||
|
||||
// Extract ModuleData languages (with backward compat for old 'language' parameter)
|
||||
$moduleData = $request->getAttribute('moduleData');
|
||||
$moduleDataLanguages = null;
|
||||
if ($moduleData instanceof ModuleData) {
|
||||
$moduleDataLanguages = $moduleData->get('languages');
|
||||
// Backward compatibility: convert old 'language' (single int) to 'languages' (array)
|
||||
if ($moduleDataLanguages === null) {
|
||||
$oldLanguage = $moduleData->get('language');
|
||||
if ($oldLanguage !== null) {
|
||||
$moduleDataLanguages = [(int)$oldLanguage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use SharedUserPreferences fallback chain (page-specific > ModuleData > default)
|
||||
// This ensures page-specific preferences are shared across modules
|
||||
$resolvedLanguages = $this->sharedPreferences->resolveLanguages(
|
||||
$backendUser,
|
||||
$languagesFromRequest,
|
||||
$pageId,
|
||||
$moduleDataLanguages
|
||||
);
|
||||
|
||||
// Validate against existing translations on this page (ensures getPrimaryLanguageId() is valid)
|
||||
// Preference is preserved across navigation (only stored when explicitly changed via request)
|
||||
$existingLanguageIds = $languageInformation->getAllExistingLanguageIds();
|
||||
$validLanguages = array_intersect($resolvedLanguages, $existingLanguageIds);
|
||||
|
||||
// Ensure at least default language if none are valid
|
||||
if (empty($validLanguages)) {
|
||||
$validLanguages = [0];
|
||||
}
|
||||
|
||||
$validLanguages = array_values($validLanguages);
|
||||
|
||||
// Store preference in SharedUserPreferences when explicitly changed via request
|
||||
if ($languagesFromRequest !== null) {
|
||||
$this->sharedPreferences->setPageLanguages($backendUser, $pageId, $validLanguages);
|
||||
}
|
||||
|
||||
// Also update ModuleData if present (for backward compatibility and UI state)
|
||||
if ($moduleData instanceof ModuleData) {
|
||||
$moduleData->set('languages', $validLanguages);
|
||||
}
|
||||
|
||||
// Create full PageContext for resolved page record
|
||||
return new PageContext(
|
||||
pageId: $pageId,
|
||||
pageRecord: $pageRecord,
|
||||
site: $site,
|
||||
rootLine: BackendUtility::BEgetRootLine($pageId),
|
||||
pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig($pageId)),
|
||||
selectedLanguageIds: $validLanguages,
|
||||
languageInformation: $languageInformation,
|
||||
pagePermissions: new Permission($backendUser->calcPerms($pageRecord)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PageContext with specific languages (no fallback resolution).
|
||||
*
|
||||
* This is useful for testing or to explicitly set languages
|
||||
* without going through the fallback chain.
|
||||
*
|
||||
* Access Handling:
|
||||
* If the user has no access to the requested page or pid=0, a PageContext is still returned,
|
||||
* while pageRecord mit be null if no access. Controllers should check isAccessible().
|
||||
*/
|
||||
public function createWithLanguages(
|
||||
ServerRequestInterface $request,
|
||||
int $pageId,
|
||||
array $languageIds,
|
||||
BackendUserAuthentication $backendUser
|
||||
): PageContext {
|
||||
$site = $request->getAttribute('site');
|
||||
if (!$site instanceof SiteInterface) {
|
||||
throw new SiteNotFoundException('No site found in request', 1731234569);
|
||||
}
|
||||
|
||||
$pageRecord = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: null;
|
||||
if ($pageId === 0 || !$pageRecord) {
|
||||
// Either root page (pid=0) which has no real page record or no access.
|
||||
// Return context with preserved pageId.
|
||||
// pageRecord might be ['path' => '/'] for admins or NULL if no access or non-admin
|
||||
// Still calculate permissions (admins have access to pid=0, editors don't).
|
||||
return new PageContext(
|
||||
pageId: $pageId,
|
||||
pageRecord: $pageRecord,
|
||||
site: $site,
|
||||
rootLine: [],
|
||||
pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig(0)),
|
||||
selectedLanguageIds: array_map('intval', $languageIds),
|
||||
languageInformation: $this->languageService->getLanguageInformationForPage(0, $site, $backendUser),
|
||||
pagePermissions: new Permission($backendUser->calcPerms($pageRecord ?: ['uid' => 0])),
|
||||
);
|
||||
}
|
||||
|
||||
// Create full PageContext for resolved page record
|
||||
return new PageContext(
|
||||
pageId: $pageId,
|
||||
pageRecord: $pageRecord,
|
||||
site: $site,
|
||||
rootLine: BackendUtility::BEgetRootLine($pageId),
|
||||
pageTsConfig: GeneralUtility::removeDotsFromTS(BackendUtility::getPagesTSconfig($pageId)),
|
||||
selectedLanguageIds: array_map('intval', $languageIds),
|
||||
languageInformation: $this->languageService->getLanguageInformationForPage($pageId, $site, $backendUser),
|
||||
pagePermissions: new Permission($backendUser->calcPerms($pageRecord)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user