TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,756 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Backend\Context\PageContext;
|
||||
use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
|
||||
use TYPO3\CMS\Backend\Module\ModuleData;
|
||||
use TYPO3\CMS\Backend\Module\ModuleProvider;
|
||||
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Service\PageLinkMessageProvider;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\LanguageSelectorBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\Buttons\LanguageSelectorMode;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Backend\View\Drawing\BackendLayoutRenderer;
|
||||
use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
|
||||
use TYPO3\CMS\Backend\View\PageLayoutContext;
|
||||
use TYPO3\CMS\Backend\View\PageViewMode;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\SchemaLabelResolver;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* The Content > Layout module.
|
||||
*
|
||||
* @internal This class is not part of the TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
class PageLayoutController
|
||||
{
|
||||
protected PageContext $pageContext;
|
||||
protected ?TcaSchema $schema = null;
|
||||
protected ?ModuleData $moduleData = null;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ComponentFactory $componentFactory,
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly PageRenderer $pageRenderer,
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly PageRepository $pageRepository,
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly ModuleProvider $moduleProvider,
|
||||
protected readonly BackendLayoutRenderer $backendLayoutRenderer,
|
||||
protected readonly BackendLayoutView $backendLayoutView,
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
protected readonly ConnectionPool $connectionPool,
|
||||
protected readonly LanguageSelectorBuilder $languageSelectorBuilder,
|
||||
protected readonly PageLinkMessageProvider $pageLinkMessageProvider,
|
||||
private readonly SchemaLabelResolver $schemaLabelResolver,
|
||||
) {}
|
||||
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$pageContext = $request->getAttribute('pageContext');
|
||||
if (!$pageContext instanceof PageContext) {
|
||||
throw new \RuntimeException('Required PageContext not available', 1731415237);
|
||||
}
|
||||
$this->pageContext = $pageContext;
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
if (!$this->pageContext->isAccessible() || $this->pageContext->pageId === 0) {
|
||||
// In case page could not be resolved or we are on pid=0, show info to select a valid page in the tree
|
||||
$view->setTitle($languageService->translate('title', 'backend.modules.layout'));
|
||||
$view->assignMultiple([
|
||||
'pageId' => $this->pageContext->pageId,
|
||||
'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
|
||||
]);
|
||||
$view->getDocHeaderComponent()->disableAutomaticReloadButton();
|
||||
return $view->renderResponse('PageLayout/PageModuleNoAccess');
|
||||
}
|
||||
|
||||
$this->moduleData = $request->getAttribute('moduleData');
|
||||
$this->schema = $this->tcaSchemaFactory->get('pages');
|
||||
|
||||
$pageLayoutContext = $this->createPageLayoutContext($request);
|
||||
|
||||
$this->updateModuleData();
|
||||
$this->createViewModeSelection($view);
|
||||
$this->addButtonsToButtonBar($view, $request);
|
||||
$this->initializeClipboard($request);
|
||||
$event = $this->eventDispatcher->dispatch(new ModifyPageLayoutContentEvent($request, $view));
|
||||
|
||||
$mainLayoutHtml = $this->backendLayoutRenderer->drawContent($request, $pageLayoutContext);
|
||||
$primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
|
||||
$pageLocalizationRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId);
|
||||
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
|
||||
|
||||
$view->setTitle($languageService->translate('title', 'backend.modules.layout'), $this->pageContext->getPageTitle());
|
||||
$view->getDocHeaderComponent()->setPageBreadcrumb($this->pageContext->pageRecord);
|
||||
$view->assignMultiple([
|
||||
'pageId' => $this->pageContext->pageId,
|
||||
'localizedPageId' => $pageLocalizationRecord['uid'] ?? 0,
|
||||
'pageLayoutContext' => $pageLayoutContext,
|
||||
'infoBoxes' => $this->generateMessagesForCurrentPage($request),
|
||||
'isPageEditable' => $this->isPageEditable($primaryLanguageId),
|
||||
'localizedPageTitle' => $this->pageContext->getPageTitle($primaryLanguageId),
|
||||
'eventContentHtmlTop' => $event->getHeaderContent(),
|
||||
'mainContentHtml' => $mainLayoutHtml,
|
||||
'eventContentHtmlBottom' => $event->getFooterContent(),
|
||||
]);
|
||||
return $view->renderResponse('PageLayout/PageModule');
|
||||
}
|
||||
|
||||
protected function updateModuleData(): void
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) === null) {
|
||||
// Invalid function, reset to default
|
||||
$this->moduleData->set('viewMode', PageViewMode::LayoutView->value);
|
||||
}
|
||||
if ($backendUser->workspace !== 0) {
|
||||
// In draft workspaces, always show all elements (including hidden)
|
||||
$this->moduleData->set('showHidden', true);
|
||||
}
|
||||
|
||||
// Store selected languages in module data for persistence
|
||||
$this->moduleData->set('languages', $this->pageContext->selectedLanguageIds);
|
||||
|
||||
// Write module data
|
||||
$backendUser->pushModuleData($this->moduleData->getModuleIdentifier(), $this->moduleData->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the menu dropdown for switching between view modes.
|
||||
*
|
||||
* Available actions:
|
||||
* - LayoutView: Single-language content editing (always available)
|
||||
* - LanguageComparisonView: Multi-language comparison (only if translations exist)
|
||||
*
|
||||
* Smart Language Selection:
|
||||
* When building URLs for mode switching, this method implements smart language selection:
|
||||
* - Switching to layout mode with 1 translation selected → keep that translation
|
||||
* - Switching to layout mode with 2+ translations selected → show default
|
||||
*/
|
||||
protected function createViewModeSelection(ModuleTemplate $view): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$modes = [
|
||||
PageViewMode::LayoutView->value => $languageService->sL(PageViewMode::LayoutView->getLabel()),
|
||||
];
|
||||
|
||||
// Only show comparison mode if page has translations
|
||||
if (!empty($this->pageContext->languageInformation->existingTranslations)) {
|
||||
$modes[PageViewMode::LanguageComparisonView->value] = $languageService->sL(PageViewMode::LanguageComparisonView->getLabel());
|
||||
}
|
||||
|
||||
// Apply TSconfig blinding
|
||||
$moduleTsConfig = $this->pageContext->getModuleTsConfig('web_layout');
|
||||
$blindActions = $moduleTsConfig['menu']['functions'] ?? [];
|
||||
foreach ($blindActions as $key => $value) {
|
||||
if (!$value && array_key_exists($key, $modes)) {
|
||||
unset($modes[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Only create menu if there are multiple actions to choose from
|
||||
if (count($modes) <= 1) {
|
||||
if (count($modes) === 1) {
|
||||
$this->moduleData->set('viewMode', array_key_first($modes));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$selectedMode = (int)$this->moduleData->get('viewMode');
|
||||
if (!array_key_exists($selectedMode, $modes)) {
|
||||
// Current function is not in available modes - reset to first available mode
|
||||
$this->moduleData->set('viewMode', array_key_first($modes));
|
||||
$selectedMode = (int)array_key_first($modes);
|
||||
}
|
||||
|
||||
$actionMenu = $this->componentFactory->createMenu();
|
||||
$actionMenu->setIdentifier('actionMenu');
|
||||
$actionMenu->setLabel(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pagelayout.moduleMenu.dropdown.label')
|
||||
);
|
||||
|
||||
foreach ($modes as $modeValue => $label) {
|
||||
$urlParams = $this->buildViewModeSwitchParams($modeValue);
|
||||
$menuItem = $this->componentFactory->createMenuItem()
|
||||
->setTitle($label)
|
||||
->setHref((string)$this->uriBuilder->buildUriFromRoute('web_layout', $urlParams));
|
||||
|
||||
if ($selectedMode === $modeValue) {
|
||||
$menuItem->setActive(true);
|
||||
}
|
||||
$actionMenu->addMenuItem($menuItem);
|
||||
}
|
||||
$view->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URL parameters for switching to a specific view mode.
|
||||
*
|
||||
* Implements smart language selection when switching modes:
|
||||
*
|
||||
* FROM comparison TO layout:
|
||||
* - 1 translation selected → keep that translation (user focused on it)
|
||||
* - 2+ translations selected → show default language
|
||||
*
|
||||
* FROM layout TO comparison:
|
||||
* - Keep the current language selection to preserve context
|
||||
*
|
||||
* @return array{id: int, viewMode: int, languages?: int[]}
|
||||
*/
|
||||
private function buildViewModeSwitchParams(int $targetModeValue): array
|
||||
{
|
||||
$params = ['id' => $this->pageContext->pageId, 'viewMode' => $targetModeValue];
|
||||
$targetMode = PageViewMode::tryFrom($targetModeValue);
|
||||
|
||||
// Smart language selection when switching to layout mode FROM comparison mode
|
||||
if ($targetMode === PageViewMode::LayoutView && $this->pageContext->hasMultipleLanguagesSelected()) {
|
||||
$nonDefaultLanguages = array_filter($this->pageContext->selectedLanguageIds, static fn($id) => $id > 0);
|
||||
$params['languages'] = [
|
||||
count($nonDefaultLanguages) === 1
|
||||
? reset($nonDefaultLanguages) // Single translation: keep it
|
||||
: 0, // Multiple: show default
|
||||
];
|
||||
}
|
||||
|
||||
// When switching to comparison mode, preserve current language selection
|
||||
// The comparison mode will auto-add default language if needed
|
||||
if ($targetMode === PageViewMode::LanguageComparisonView) {
|
||||
$params['languages'] = $this->pageContext->selectedLanguageIds;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
protected function createPageLayoutContext(
|
||||
ServerRequestInterface $request
|
||||
): PageLayoutContext {
|
||||
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($this->pageContext->pageId);
|
||||
$viewMode = count($this->pageContext->languageInformation->availableLanguages) > 1
|
||||
? PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView
|
||||
: PageViewMode::LayoutView;
|
||||
$configuration = DrawingConfiguration::create($backendLayout, $this->pageContext->pageTsConfig, $viewMode);
|
||||
$configuration->setShowHidden((bool)$this->moduleData->get('showHidden'));
|
||||
|
||||
// Build language columns map from available languages
|
||||
$languageColumns = [];
|
||||
foreach ($this->pageContext->languageInformation->availableLanguages as $language) {
|
||||
$languageColumns[$language->getLanguageId()] = $language->getTitle();
|
||||
}
|
||||
// @todo Check if this is still used at all.
|
||||
$configuration->setLanguageColumns($languageColumns);
|
||||
|
||||
$configuration->setSelectedLanguageIds($this->pageContext->selectedLanguageIds);
|
||||
return GeneralUtility::makeInstance(PageLayoutContext::class, $this->pageContext, $backendLayout, $configuration, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of various messages for the current page record,
|
||||
* such as if the page has a special doktype, that can be rendered as info boxes.
|
||||
*/
|
||||
protected function generateMessagesForCurrentPage(ServerRequestInterface $request): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
$infoBoxes = [];
|
||||
$currentDocumentType = (int)($this->pageContext->pageRecord['doktype'] ?? 0);
|
||||
if ($currentDocumentType === PageRepository::DOKTYPE_SYSFOLDER && $this->moduleProvider->accessGranted('records', $backendUser)) {
|
||||
$infoBoxes[] = [
|
||||
'title' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToRecordsModule'),
|
||||
'message' => '<p>' . $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToListModuleMessage') . '</p>'
|
||||
. '<button type="button" class="btn btn-primary" data-dispatch-action="TYPO3.ModuleMenu.showModule" data-dispatch-args-list="records">'
|
||||
. $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToRecordsModule')
|
||||
. '</button>',
|
||||
'state' => ContextualFeedbackSeverity::INFO,
|
||||
];
|
||||
}
|
||||
if ($currentDocumentType === PageRepository::DOKTYPE_SHORTCUT) {
|
||||
$shortcutMode = (int)($this->pageContext->pageRecord['shortcut_mode'] ?? 0);
|
||||
$targetPage = [];
|
||||
$state = ContextualFeedbackSeverity::ERROR;
|
||||
if ($shortcutMode || ($this->pageContext->pageRecord['shortcut'] ?? false)) {
|
||||
switch ($shortcutMode) {
|
||||
case PageRepository::SHORTCUT_MODE_NONE:
|
||||
$targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['shortcut'] ?? 0), true));
|
||||
$message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : '';
|
||||
break;
|
||||
case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
|
||||
$menuOfPages = $this->pageRepository->getMenu((int)($this->pageContext->pageRecord['uid'] ?? 0), '*', 'sorting', 'AND hidden = 0', true, true);
|
||||
$targetPage = reset($menuOfPages) ?: [];
|
||||
$message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredFirstSubpageMessage') : '';
|
||||
break;
|
||||
case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
|
||||
$targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['pid'] ?? 0), true));
|
||||
$message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredParentPageMessage') : '';
|
||||
break;
|
||||
default:
|
||||
$message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage'));
|
||||
break;
|
||||
}
|
||||
$message = htmlspecialchars($message);
|
||||
if ($targetPage !== []) {
|
||||
$linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $targetPage['uid']]);
|
||||
$path = BackendUtility::getRecordPath($targetPage['uid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW), 1000);
|
||||
$linkedPath = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($path) . '</a>';
|
||||
$message .= sprintf(htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsInternalLinkMessage')), $linkedPath);
|
||||
$message .= ' (' . htmlspecialchars($languageService->sL($this->schemaLabelResolver->getLabelForFieldValue('pages', 'shortcut_mode', (string)$shortcutMode, $this->pageContext->pageRecord))) . ')';
|
||||
$state = ContextualFeedbackSeverity::INFO;
|
||||
}
|
||||
} else {
|
||||
$message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage'));
|
||||
}
|
||||
$infoBoxes[] = [
|
||||
'message' => $message,
|
||||
'state' => $state,
|
||||
];
|
||||
}
|
||||
if ($currentDocumentType === PageRepository::DOKTYPE_LINK) {
|
||||
$primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
|
||||
$pageRecord = ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->existingTranslations[$primaryLanguageId] ?? []) !== [])
|
||||
? $overlayRecord
|
||||
: $this->pageContext->pageRecord;
|
||||
$infoBoxes[] = $this->pageLinkMessageProvider->generateMessagesForPageTypeLink($pageRecord, $request);
|
||||
}
|
||||
if ($this->pageContext->pageRecord['content_from_pid'] ?? false) {
|
||||
// If content from different pid is displayed
|
||||
$contentPage = BackendUtility::getRecord('pages', (int)$this->pageContext->pageRecord['content_from_pid']);
|
||||
if ($contentPage === null) {
|
||||
$infoBoxes[] = [
|
||||
'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_invalid_title'), $this->pageContext->pageRecord['content_from_pid']),
|
||||
'state' => ContextualFeedbackSeverity::ERROR,
|
||||
];
|
||||
} else {
|
||||
$linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $this->pageContext->pageRecord['content_from_pid']]);
|
||||
$title = BackendUtility::getRecordTitle('pages', $contentPage);
|
||||
$title = BackendUtility::cropToTitleLength($title);
|
||||
$link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageContext->pageRecord['content_from_pid'] . ')</a>';
|
||||
$infoBoxes[] = [
|
||||
'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_title'), $link),
|
||||
'state' => ContextualFeedbackSeverity::INFO,
|
||||
];
|
||||
}
|
||||
} elseif ($this->pageContext->pageId > 0) {
|
||||
$links = $this->getPageLinksWhereContentIsAlsoShownOn($this->pageContext->pageId);
|
||||
if (!empty($links)) {
|
||||
$infoBoxes[] = [
|
||||
'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_on_pid_title'), $links),
|
||||
'state' => ContextualFeedbackSeverity::INFO,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $infoBoxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages with links where the content of a page $pageId is also shown on.
|
||||
*/
|
||||
protected function getPageLinksWhereContentIsAlsoShownOn(int $pageId): string
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$queryBuilder->select('*')
|
||||
->from('pages')
|
||||
->where($queryBuilder->expr()->eq('content_from_pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)));
|
||||
$links = [];
|
||||
$rows = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
if (!empty($rows)) {
|
||||
foreach ($rows as $row) {
|
||||
$linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $row['uid']]);
|
||||
$title = BackendUtility::getRecordTitle('pages', $row);
|
||||
$title = BackendUtility::cropToTitleLength($title);
|
||||
$link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')</a>';
|
||||
$links[] = $link;
|
||||
}
|
||||
}
|
||||
return implode(', ', $links);
|
||||
}
|
||||
|
||||
protected function addButtonsToButtonBar(ModuleTemplate $view, ServerRequestInterface $request): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
// Close button (show only if returnUrl is set)
|
||||
$returnUrl = GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request);
|
||||
if ($returnUrl) {
|
||||
// use button group -1 so that close button is to the left of other buttons
|
||||
$view->addButtonToButtonBar($this->componentFactory->createCloseButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, -1);
|
||||
}
|
||||
|
||||
// Language selector
|
||||
$this->createLanguageSelector($view);
|
||||
|
||||
// View
|
||||
if ($viewButton = $this->makeViewButton()) {
|
||||
$view->addButtonToButtonBar($viewButton);
|
||||
}
|
||||
|
||||
// Edit
|
||||
if ($editButton = $this->makeEditButton($request)) {
|
||||
$view->addButtonToButtonBar($editButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
|
||||
}
|
||||
|
||||
// Cache
|
||||
$clearCacheButton = $this->componentFactory->createGenericButton()
|
||||
->setTag('button')
|
||||
->setLabel($languageService->sL('core.cache:page.label'))
|
||||
->setClasses('t3js-clear-page-cache')
|
||||
->setAttributes([
|
||||
'type' => 'button',
|
||||
'data-id' => (string)($this->pageContext->pageRecord['uid'] ?? 0),
|
||||
])
|
||||
->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', IconSize::SMALL));
|
||||
$view->addButtonToButtonBar($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
|
||||
|
||||
// View settings
|
||||
if ($this->getBackendUser()->check('tables_select', 'tt_content')) {
|
||||
$viewSettingsButton = $this->componentFactory->createDropDownButton()
|
||||
->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view'))
|
||||
->setIcon($this->iconFactory->getIcon('actions-cog'))
|
||||
->setShowLabelText(true);
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/layout-module/toggle-hidden-element.js');
|
||||
$toggleHidden = $this->componentFactory->createDropDownGeneric();
|
||||
$toggleHidden->setTag('typo3-backend-page-layout-toggle-hidden');
|
||||
$toggleHiddenAttributes = ['count' => (string)$this->getNumberOfHiddenElements()];
|
||||
if ((bool)$this->moduleData->get('showHidden')) {
|
||||
$toggleHiddenAttributes['active'] = 'active';
|
||||
}
|
||||
$toggleHidden->setAttributes($toggleHiddenAttributes);
|
||||
$viewSettingsButton->addItem($toggleHidden);
|
||||
$view->addButtonToButtonBar($viewSettingsButton, ButtonBar::BUTTON_POSITION_RIGHT, 0);
|
||||
}
|
||||
|
||||
// Shortcut
|
||||
$view->getDocHeaderComponent()->setShortcutContext(
|
||||
'web_layout',
|
||||
sprintf(
|
||||
'%s: %s [%d]',
|
||||
$this->getLanguageService()->translate('short_description', 'backend.modules.layout'),
|
||||
$this->pageContext->getPageTitle(),
|
||||
$this->pageContext->pageId
|
||||
),
|
||||
[
|
||||
'id' => $this->pageContext->pageId,
|
||||
'showHidden' => (bool)$this->moduleData->get('showHidden'),
|
||||
'viewMode' => (int)$this->moduleData->get('viewMode'),
|
||||
'languages' => $this->pageContext->selectedLanguageIds,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function initializeClipboard(ServerRequestInterface $request): void
|
||||
{
|
||||
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipboard->initializeClipboard($request);
|
||||
$clipboard->lockToNormal();
|
||||
$clipboard->cleanCurrent();
|
||||
$clipboard->endClipboard();
|
||||
$elFromTable = $clipboard->elFromTable('tt_content');
|
||||
if (!empty($elFromTable) && $this->isContentEditable($this->pageContext->getPrimaryLanguageId())) {
|
||||
$pasteItem = (int)substr((string)key($elFromTable), 11);
|
||||
$pasteRecord = BackendUtility::getRecordWSOL('tt_content', $pasteItem);
|
||||
$pasteTitle = BackendUtility::getRecordTitle('tt_content', $pasteRecord);
|
||||
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/layout-module/paste.js')
|
||||
->instance([
|
||||
'itemOnClipboardUid' => $pasteItem,
|
||||
'itemOnClipboardTitle' => $pasteTitle,
|
||||
'copyMode' => $clipboard->clipData['normal']['mode'] ?? '',
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View Button
|
||||
*/
|
||||
protected function makeViewButton(): ?ButtonInterface
|
||||
{
|
||||
// Do not create a "View webpage" button if
|
||||
// * Multiple languages are selected
|
||||
// * record is a placeholder
|
||||
// * not in "Columns" view,
|
||||
if (
|
||||
$this->pageContext->hasMultipleLanguagesSelected()
|
||||
|| VersionState::tryFrom($this->pageContext->pageRecord['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER
|
||||
|| PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$previewUriBuilder = PreviewUriBuilder::create($this->pageContext->pageRecord);
|
||||
if (!$previewUriBuilder->isPreviewable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->componentFactory->createViewButton($previewUriBuilder
|
||||
->withRootLine($this->pageContext->rootLine)
|
||||
->withLanguage($this->pageContext->getPrimaryLanguageId())
|
||||
->buildDispatcherDataAttributes() ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit Button
|
||||
*/
|
||||
protected function makeEditButton(ServerRequestInterface $request): ?ButtonInterface
|
||||
{
|
||||
$primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
|
||||
if (!$this->isPageEditable($primaryLanguageId)
|
||||
|| PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pageUid = $this->pageContext->pageId;
|
||||
if ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId)) !== null) {
|
||||
$pageUid = $overlayRecord['uid'];
|
||||
}
|
||||
|
||||
$editParams = [
|
||||
'edit' => ['pages' => [$pageUid => 'edit']],
|
||||
'module' => 'web_layout',
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
return $this->componentFactory->createGenericButton()
|
||||
->setTag('typo3-backend-contextual-record-edit-trigger')
|
||||
->setAttributes([
|
||||
'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit_contextual', $editParams),
|
||||
'edit-url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', $editParams),
|
||||
])
|
||||
->setLabel($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editPageProperties'))
|
||||
->setShowLabelText(true)
|
||||
->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the language selector dropdown in the module toolbar.
|
||||
*/
|
||||
protected function createLanguageSelector(ModuleTemplate $view): void
|
||||
{
|
||||
if (count($this->pageContext->languageInformation->availableLanguages) <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$viewMode = PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView;
|
||||
$isComparisonMode = $viewMode === PageViewMode::LanguageComparisonView;
|
||||
$mode = $isComparisonMode ? LanguageSelectorMode::MULTI_SELECT : LanguageSelectorMode::SINGLE_SELECT;
|
||||
|
||||
$languageSelector = $this->languageSelectorBuilder->build(
|
||||
$this->pageContext,
|
||||
$mode,
|
||||
fn(array $languageIds): string => (string)$this->uriBuilder->buildUriFromRoute('web_layout', [
|
||||
'id' => $this->pageContext->pageId,
|
||||
'viewMode' => $viewMode->value,
|
||||
'languages' => $languageIds,
|
||||
]),
|
||||
$isComparisonMode && !empty($this->pageContext->languageInformation->existingTranslations)
|
||||
);
|
||||
|
||||
$view->getDocHeaderComponent()->setLanguageSelector($languageSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of hidden elements (including those hidden by start/end times)
|
||||
* on the current page (for the current site language)
|
||||
*/
|
||||
protected function getNumberOfHiddenElements(): int
|
||||
{
|
||||
$isComparisonView = (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView) === PageViewMode::LanguageComparisonView;
|
||||
$andWhere = [];
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
||||
$queryBuilder
|
||||
->count('uid')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($this->pageContext->pageId, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
$languageField = $this->schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
|
||||
// Build list of language IDs to include: always include -1 (all languages) and all selected languages
|
||||
$languageIds = [-1];
|
||||
foreach ($this->pageContext->selectedLanguageIds as $languageId) {
|
||||
$languageIds[] = $languageId;
|
||||
// In comparison mode, also include default language (0) if not already selected
|
||||
if ($isComparisonView && $languageId > 0 && !$this->pageContext->isDefaultLanguageSelected()) {
|
||||
$languageIds[] = 0;
|
||||
}
|
||||
}
|
||||
$languageIds = array_unique($languageIds);
|
||||
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->in(
|
||||
$languageField,
|
||||
$queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
);
|
||||
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
|
||||
$andWhere[] = $queryBuilder->expr()->neq(
|
||||
$this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(),
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
);
|
||||
}
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
|
||||
$starttimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
|
||||
$andWhere[] = $queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->neq(
|
||||
$starttimeField,
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->gt(
|
||||
$starttimeField,
|
||||
$queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
|
||||
$endtimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
|
||||
$andWhere[] = $queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->neq(
|
||||
$endtimeField,
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->lte(
|
||||
$endtimeField,
|
||||
$queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($andWhere !== []) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->or(...$andWhere)
|
||||
);
|
||||
}
|
||||
$count = $queryBuilder
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
return (int)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page can be edited by current user.
|
||||
*/
|
||||
protected function isPageEditable(int $languageId): bool
|
||||
{
|
||||
if (empty($this->pageContext->pageRecord)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
$backendUser = $this->getBackendUser();
|
||||
if ($backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
|
||||
return false;
|
||||
}
|
||||
$isEditLocked = false;
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) {
|
||||
$isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false;
|
||||
}
|
||||
if ($isEditLocked) {
|
||||
return false;
|
||||
}
|
||||
return $backendUser->doesUserHaveAccess($this->pageContext->pageRecord, Permission::PAGE_EDIT)
|
||||
&& $backendUser->checkLanguageAccess($languageId)
|
||||
&& $backendUser->check('tables_modify', 'pages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content can be edited by current user
|
||||
*/
|
||||
protected function isContentEditable(int $languageId): bool
|
||||
{
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
$isEditLocked = false;
|
||||
if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) {
|
||||
$isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false;
|
||||
}
|
||||
if ($isEditLocked) {
|
||||
return false;
|
||||
}
|
||||
return $this->getBackendUser()->doesUserHaveAccess($this->pageContext->pageRecord, Permission::CONTENT_EDIT)
|
||||
&& $this->getBackendUser()->check('tables_modify', 'tt_content')
|
||||
&& $this->getBackendUser()->checkLanguageAccess($languageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target page if visible
|
||||
*/
|
||||
protected function getTargetPageIfVisible(array $targetPage): array
|
||||
{
|
||||
$fieldName = $this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
|
||||
return !($targetPage[$fieldName] ?? false) ? $targetPage : [];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user