Files
cms-frontend/Classes/Page/PageInformationFactory.php

714 lines
36 KiB
PHP

<?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\Frontend\Page;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Error\Http\LinkedPageNotResolvableException;
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
use TYPO3\CMS\Core\Error\Http\StatusException;
use TYPO3\CMS\Core\Exception\Page\CircularPageReferenceChainException;
use TYPO3\CMS\Core\Exception\Page\PageReferenceResolvingReachedIterationLimitException;
use TYPO3\CMS\Core\Exception\Page\RootLineException;
use TYPO3\CMS\Core\LinkHandling\PageTypeLinkResolver;
use TYPO3\CMS\Core\Page\PageLayoutResolver;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\NullSite;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent;
use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent;
use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent;
/**
* Create the PageInformation object. This is typically fired by a
* middleware. It does all the heavy lifting, page access checks,
* resolves shortcuts, workspaces, languages and similar.
*
* Possible results:
* - The fully set up PageInformation object is returned.
* - A PageInformationCreationFailedException is thrown that contains
* an early Response from the ErrorController
* - A StatusException is thrown when ErrorController itself failed
*
* @todo: This class also sets / resets the Context language aspect
* as not directly obvious side effect. This can't be refactored
* currently due to the dependency of stateful PageRepository to
* the stateful singleton Context.
*
* @internal
*/
final readonly class PageInformationFactory
{
public function __construct(
private Context $context,
private EventDispatcherInterface $eventDispatcher,
private LoggerInterface $logger,
private RecordAccessVoter $accessVoter,
private ErrorController $errorController,
private SysTemplateRepository $sysTemplateRepository,
private PageLayoutResolver $pageLayoutResolver,
private TcaSchemaFactory $tcaSchemaFactory,
private PageTypeLinkResolver $pageTypeLinkResolver,
private PageDoktypeRegistry $pageDoktypeRegistry,
private PageRepository $pageRepository,
) {}
/**
* Set up proper PageInformation object later available as
* 'frontend.page.information' Request attribute.
*
* At this point, the Context object already contains relevant preview
* settings, for instance if a backend user is logged in.
*
* As a not obvious side effect, this class also *sets* the
*
* @internal Extensions should not call themselves, use events.
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
public function create(ServerRequestInterface $request): PageInformation
{
// Set Initial, not yet validated values from routing.
$pageInformation = new PageInformation();
$pageInformation->setId($request->getAttribute('routing')->getPageId());
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids'] ?? false) {
$mountPoint = (string)($request->getAttribute('routing')->getArguments()['MP'] ?? '');
// Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
$pageInformation->setMountPoint(preg_replace('/[^0-9,-]/', '', $mountPoint));
}
$event = $this->eventDispatcher->dispatch(new BeforePageIsResolvedEvent($request, $pageInformation));
$pageInformation = $event->getPageInformation();
$pageInformation = $this->setPageAndRootline($request, $pageInformation);
$this->checkCrossDomainWithDirectId($request, $pageInformation);
$event = $this->eventDispatcher->dispatch(new AfterPageWithRootLineIsResolvedEvent($request, $pageInformation));
if ($event->getResponse()) {
throw new PageInformationCreationFailedException($event->getResponse(), 1705419743);
}
$pageInformation = $event->getPageInformation();
$pageInformation = $this->settingLanguage($request, $pageInformation);
$pageInformation = $this->setContentFromPid($request, $pageInformation);
$pageInformation = $this->setPageLayout($pageInformation);
$this->checkBackendUserAccess($request, $pageInformation);
$event = $this->eventDispatcher->dispatch(new AfterPageAndLanguageIsResolvedEvent($request, $pageInformation));
if ($event->getResponse()) {
throw new PageInformationCreationFailedException($event->getResponse(), 1705420010);
}
$pageInformation = $event->getPageInformation();
$pageInformation = $this->setSysTemplateRows($request, $pageInformation);
$pageInformation = $this->setLocalRootLine($request, $pageInformation);
$this->verifySiteOrSysTemplateRowExists($request, $pageInformation);
return $pageInformation;
}
/**
* Main lifting. Final page and the matching root line are determined and loaded.
*
* Note this methods may be called a second time in case of 'content_from_pid'.
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function setPageAndRootline(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
{
$id = $pageInformation->getId();
$mountPoint = $pageInformation->getMountPoint();
$pageRecord = $this->pageRepository->getPage($id);
if (empty($pageRecord)) {
// @todo: This logic could be streamlined is general. The idea of PageRepository->getPage() is
// to have *one* query that does all the access checks already, to save queries. Only
// if this goes wrong more queries are fired to find out details.
// This is ugly. It would be better to query the page without checks and return it, probably
// only doing the deleted=1 check. If that does not resolve: 404. Then apply the hidden
// and groups checks in PHP on the page record without further queries, to error out
// whenever a single step goes wrong. Or have a single method that follows this strategy
// and throws dedicated exceptions like a PageHiddenException or similar to catch it here
// and act accordingly.
// This strategy would turn the guesswork logic below around and would be easier to
// maintain and follow in general. It would also clean up the various methods with their
// subtle differences: getPage($id), getPage($id, true) and getPage_noCheck($id).
// PageRepository->getPage() did not return a page. This can have
// different reasons. We want to error out with different status codes.
$schema = $this->tcaSchemaFactory->get('pages');
$includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) && !$includeHiddenPages) {
// Page is hidden, user has no access. 404. This is deliberately done in default language
// since language overlays should not be rendered when default language is hidden.
$rawPageRecord = $this->pageRepository->getPage_noCheck($id);
$hiddenField = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
if ($rawPageRecord === [] || $rawPageRecord[$hiddenField]) {
$response = $this->errorController->pageNotFoundAction(
$request,
'The requested page does not exist!',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1674144383);
}
}
$requestedPageRowWithoutGroupCheck = $this->pageRepository->getPage($id, true);
if (!empty($requestedPageRowWithoutGroupCheck)) {
// We know now the page could not be received, but the reason is *not* that the
// page is hidden and the user has no hidden access. So group access failed? 403.
$response = $this->errorController->accessDeniedAction(
$request,
'ID was not an accessible page',
[
'code' => PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED,
'direct_access' => [
0 => $requestedPageRowWithoutGroupCheck,
],
],
);
throw new PageInformationCreationFailedException($response, 1705325336);
}
// Else 404 for 'record not exists' or similar.
$response = $this->errorController->pageNotFoundAction(
$request,
'The requested page does not exist!',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1533931330);
}
$pageInformation->setPageRecord($pageRecord);
$pageDoktype = (int)($pageRecord['doktype']);
// Spacer and sysfolders are not accessible in frontend
if (!$this->pageDoktypeRegistry->isPageTypeViewable($pageDoktype)) {
$response = $this->errorController->pageNotFoundAction(
$request,
'The requested page does not exist!',
['code' => PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE]
);
throw new PageInformationCreationFailedException($response, 1533931343);
}
if ($pageDoktype === PageRepository::DOKTYPE_LINK || $pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
// Resolve shortcut page target.
// Clear mount point if page is a shortcut: If the shortcut goes to
// another page, we leave the rootline which the MP expects.
$mountPoint = '';
$pageInformation->setMountPoint($mountPoint);
// Saving the page so that we can check later - when we know about languages - whether we took the correct shortcut
// or if a translation of the page overwrites the shortcut target, and we need to follow the new target.
$pageInformation = $this->settingLanguage($request, $pageInformation);
// Reset vars to new state that may have been created by settingLanguage()
$pageRecord = $pageInformation->getPageRecord();
if ($pageDoktype === PageRepository::DOKTYPE_LINK) {
$pageInformation->setOriginalShortcutPageRecord($pageRecord);
$typolinkInformation = $this->pageTypeLinkResolver->resolveTypolinkParts($pageRecord);
// The link destination was a page, we have to prevent infinitive loops
if ($typolinkInformation['type'] === 'page') {
try {
$pageRecord = $this->pageRepository->resolveLinkPage($pageRecord);
} catch (ShortcutTargetPageNotFoundException|LinkedPageNotResolvableException|CircularPageReferenceChainException|PageReferenceResolvingReachedIterationLimitException) {
$response = $this->errorController->pageNotFoundAction(
$request,
'ID was not an accessible page',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1705335066);
}
$pageInformation->setPageRecord($pageRecord);
$id = (int)$pageRecord['uid'];
$pageInformation->setId($id);
$pageDoktype = (int)($pageRecord['doktype'] ?? 0);
}
}
if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
$pageInformation->setOriginalShortcutPageRecord($pageRecord);
try {
$pageRecord = $this->pageRepository->resolveShortcutPage($pageRecord);
} catch (ShortcutTargetPageNotFoundException|LinkedPageNotResolvableException|CircularPageReferenceChainException|PageReferenceResolvingReachedIterationLimitException) {
$response = $this->errorController->pageNotFoundAction(
$request,
'ID was not an accessible page',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1705335065);
}
$pageInformation->setPageRecord($pageRecord);
$id = (int)$pageRecord['uid'];
$pageInformation->setId($id);
$pageDoktype = (int)($pageRecord['doktype'] ?? 0);
}
}
if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $pageRecord['mount_pid_ol']) {
// If the page is a mount point which should be overlaid with the contents of the mounted page,
// it must never be accessible directly, but only in the mount point context.
// We thus change the current page id.
$originalMountPointPageRecord = $pageRecord;
$pageInformation->setOriginalMountPointPageRecord($pageRecord);
$pageRecord = $this->pageRepository->getPage((int)$originalMountPointPageRecord['mount_pid']);
if (empty($pageRecord)) {
// Target mount point page not accessible for some reason.
$response = $this->errorController->pageNotFoundAction(
$request,
'The requested page does not exist!',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1705425523);
}
$pageInformation->setPageRecord($pageRecord);
if ($mountPoint === '' || !empty($pageInformation->getOriginalShortcutPageRecord())) {
// If the current page is a shortcut, the MP parameter will be replaced
$mountPoint = $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
} else {
$mountPoint = $mountPoint . ',' . $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
}
$pageInformation->setMountPoint($mountPoint);
$id = (int)$pageRecord['uid'];
$pageInformation->setId($id);
}
// Get rootLine and error out if it can not be retrieved.
$pageInformation->setRootLine($this->getRootlineOrThrow($request, $id, $mountPoint));
// Check 'extendToSubpages' in rootLine and backend user section.
$this->checkRootlineForIncludeSection($request, $pageInformation);
return $pageInformation;
}
/**
* Determine the final Context language aspect, page record based on
* language settings, existing page overlay and its rootLine.
*
* May reset:
* $pageInformation->pageRecord
* $pageInformation->pageRepository
* $pageInformation->rootLine
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function settingLanguage(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
{
$site = $request->getAttribute('site');
$language = $request->getAttribute('language', $site->getDefaultLanguage());
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($language);
$languageId = $languageAspect->getId();
$languageContentId = $languageAspect->getContentId();
$pageRecord = $pageInformation->getPageRecord();
$pageTranslationVisibility = new PageTranslationVisibility((int)($pageRecord['l18n_cfg'] ?? 0));
if ($languageAspect->getId() > 0) {
// If the incoming language is set to another language than default
$olRec = $this->pageRepository->getPageOverlay($pageRecord, $languageAspect);
$overlaidLanguageId = (int)($olRec['language_tag'] ?? 0);
if ($overlaidLanguageId !== $languageAspect->getId()) {
// If requested translation is not available
if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
$response = $this->errorController->pageNotFoundAction(
$request,
'Page is not available in the requested language.',
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
);
throw new PageInformationCreationFailedException($response, 1533931388);
}
switch ($languageAspect->getLegacyLanguageMode()) {
case 'strict':
$response = $this->errorController->pageNotFoundAction(
$request,
'Page is not available in the requested language (strict).',
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
);
throw new PageInformationCreationFailedException($response, 1533931395);
case 'content_fallback':
// Setting content uid (but leaving the sys_language_uid) when a content_fallback value was found.
foreach ($languageAspect->getFallbackChain() as $orderValue) {
if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
$languageContentId = 0;
break;
}
if (MathUtility::canBeInterpretedAsInteger($orderValue) && $overlaidLanguageId === (int)$orderValue) {
$languageContentId = (int)$orderValue;
break;
}
if ($orderValue === 'pageNotFound') {
// The existing fallbacks have not been found, but instead of continuing page rendering
// with default language, a "page not found" message should be shown instead.
$response = $this->errorController->pageNotFoundAction(
$request,
'Page is not available in the requested language (fallbacks did not apply).',
['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
);
throw new PageInformationCreationFailedException($response, 1533931402);
}
}
break;
default:
// Default is that everything defaults to the default language.
$languageId = ($languageContentId = 0);
}
}
// Define the language aspect again now
$languageAspect = new LanguageAspect(
$languageId,
$languageContentId,
$languageAspect->getOverlayType(),
$languageAspect->getFallbackChain()
);
// Setting localized page record if an overlay record was found (which it is only if a language is used)
// Doing this ensures that page properties like the page title are resolved in the correct language.
$pageInformation->setPageRecord($olRec);
}
// Set the final language aspect!
$this->context->setAspect('language', $languageAspect);
if ((!$languageAspect->getContentId() || !$languageAspect->getId())
&& $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
) {
// If default language is not available
$response = $this->errorController->pageNotFoundAction(
$request,
'Page is not available in default language.',
['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
);
throw new PageInformationCreationFailedException($response, 1533931423);
}
if ($languageAspect->getId() > 0) {
// Updating rootLine with translations if the language key is set.
$pageInformation->setRootLine(
$this->getRootlineOrThrow($request, $pageInformation->getId(), $pageInformation->getMountPoint())
);
}
return $pageInformation;
}
/**
* Check the value of 'content_from_pid' of the current page record, to see if the
* current request should actually show content from another page.
* If so, PageInformation->getContentFromPid() is set to the page id of the content
* page, while PageInformation->getId() is kept as the original page id.
* If there is no 'content_from_pid', PageInformation->getId() and
* PageInformation->getContentFromPid() end up carrying the same page ids.
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function setContentFromPid(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
{
$contentFromPid = (int)($pageInformation->getPageRecord()['content_from_pid'] ?? 0);
if ($contentFromPid === 0) {
// $pageInformation->contentFromPid is always initialized, usually identical with id.
$pageInformation->setContentFromPid($pageInformation->getId());
return $pageInformation;
}
// Verify content target pid is good and resolves all access restrictions and similar.
$targetPageInformation = new PageInformation();
$targetPageInformation->setId($contentFromPid);
$targetPageInformation = $this->setPageAndRootline($request, $targetPageInformation);
// Above call did not throw. Set the verified id.
$pageInformation->setContentFromPid($targetPageInformation->getId());
return $pageInformation;
}
/**
* Resolve the selected backend layout for the current page and add it to the page information
*/
private function setPageLayout(PageInformation $pageInformation): PageInformation
{
$pageLayout = $this->pageLayoutResolver->getLayoutForPage(
$pageInformation->getPageRecord(),
$pageInformation->getRootLine()
);
if ($pageLayout !== null) {
$pageInformation->setPageLayout($pageLayout);
}
return $pageInformation;
}
/**
* Checks if visibility of the page is blocked upwards in the root line.
*
* The blocking feature of a page must be turned on by setting the page
* record field 'extendToSubpages' to 1 for 'hidden', 'starttime', 'endtime'
* 'fe_group' restrictions to bubble down in rootLine.
*
* Additionally, this method checks for backend user sections in root line
* and if found, evaluates if a backend user is logged in and has access.
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function checkRootlineForIncludeSection(ServerRequestInterface $request, PageInformation $pageInformation): void
{
$rootLine = $pageInformation->getRootLine();
for ($a = 0; $a < count($rootLine); $a++) {
$rootLineEntry = $rootLine[$a];
if (!$this->accessVoter->accessGrantedForPageInRootLine($rootLineEntry, $this->context)) {
// accessGrantedForPageInRootLine() does the main check for 'extendToSubpages'.
$response = $this->errorController->accessDeniedAction(
$request,
'Subsection was found and not accessible',
[
'code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED,
'sub_section' => [
0 => $rootLineEntry,
],
],
);
throw new PageInformationCreationFailedException($response, 1705337296);
}
if ((int)$rootLineEntry['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
// Only logged in backend users with 'PAGE_SHOW' permissions are allowed to FE render backend user sections.
$isBackendUserLoggedIn = $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
if (!$isBackendUserLoggedIn) {
$response = $this->errorController->accessDeniedAction(
$request,
'Subsection was found and not accessible',
['code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED]
);
throw new PageInformationCreationFailedException($response, 1705416744);
}
if (!$this->getBackendUser()->doesUserHaveAccess($pageInformation->getPageRecord(), Permission::PAGE_SHOW)) {
$response = $this->errorController->accessDeniedAction(
$request,
'Subsection was found and not accessible',
['code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED]
);
throw new PageInformationCreationFailedException($response, 1705337701);
}
}
}
}
/**
* When calling a page with a direct id 'https://my.domain/?id=123',
* this the site object of 'my.domain' is determined from an earlier middleware.
* If now '123' is *not* a (sub) page of the 'my.domain' site, we error out since
* we don't want to directly render content of a different site page within "our" site.
* Except if '123' is a shortcut, we still allow it, since that will trigger a
* redirect to the url with the shortcut target domain later.
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function checkCrossDomainWithDirectId(ServerRequestInterface $request, PageInformation $pageInformation): void
{
$directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0);
$shortcutId = (int)($pageInformation->getOriginalShortcutPageRecord()['uid'] ?? 0);
if ($directlyRequestedId && $shortcutId !== $directlyRequestedId) {
$rootLine = $pageInformation->getRootLine();
$siteRootPageId = $request->getAttribute('site')->getRootPageId();
$siteRootWithinRootlineFound = false;
foreach ($rootLine as $pageInRootLine) {
if ((int)$pageInRootLine['uid'] === $siteRootPageId) {
$siteRootWithinRootlineFound = true;
break;
}
}
if (!$siteRootWithinRootlineFound) {
$response = $this->errorController->pageNotFoundAction(
$request,
'ID was outside the domain',
['code' => PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH]
);
throw new PageInformationCreationFailedException($response, 1705397417);
}
}
}
/**
* When a backend user is logged in, it needs at least 'show' permissions.
*
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function checkBackendUserAccess(ServerRequestInterface $request, PageInformation $pageInformation): void
{
// No backend user was logged in, nothing to check
if (!$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
return;
}
// PreviewSimulator did not detect anything
if (!$this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
return;
}
// Editor has no show permission for this page PLUS regular user is not allowed to see the page? 403.
if (!$GLOBALS['BE_USER']->doesUserHaveAccess($pageInformation->getPageRecord(), Permission::PAGE_SHOW)
&& !$this->accessVoter->accessGranted('pages', $pageInformation->getPageRecord(), $this->context)
) {
$response = $this->errorController->accessDeniedAction(
$request,
'ID was not an accessible page',
['code' => PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED]
);
throw new PageInformationCreationFailedException($response, 1705422293);
}
}
/**
* Determine relevant sys_template rows and set to PageInformation object.
*
* @todo: Even though all rootline sys_template records are fetched with only one query
* in below implementation, we could potentially join or sub select sys_template
* records already when pages rootline is queried. This will save one query.
* This could be done when we manage to switch PageRepository / RootlineUtility to a CTE.
* @see \TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::getTypoScriptSetup()
* for similar usage.
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function setSysTemplateRows(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
{
$site = $request->getAttribute('site');
$rootLine = $pageInformation->getRootLine();
if ($site instanceof Site && $site->isTypoScriptRoot()) {
$rootLineUntilSite = [];
foreach ($rootLine as $index => $rootlinePage) {
$rootLineUntilSite[$index] = $rootlinePage;
$pageId = (int)($rootlinePage['uid'] ?? 0);
if ($pageId === $site->getRootPageId()) {
break;
}
}
$rootLine = $rootLineUntilSite;
}
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLine, $request);
$pageInformation->setSysTemplateRows($sysTemplateRows);
return $pageInformation;
}
/**
* Calculate "local" rootLine that stops at first root=1 template.
*/
private function setLocalRootLine(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
{
$site = $request->getAttribute('site');
$sysTemplateRows = $pageInformation->getSysTemplateRows();
$rootLine = $pageInformation->getRootLine();
$sysTemplateRowsIndexedByPid = array_combine(array_column($sysTemplateRows, 'pid'), $sysTemplateRows);
$localRootline = [];
foreach ($rootLine as $rootlinePage) {
array_unshift($localRootline, $rootlinePage);
$pageId = (int)($rootlinePage['uid'] ?? 0);
if ($pageId === $site->getRootPageId() && $site->isTypoScriptRoot()) {
break;
}
if ($pageId > 0 && (int)($sysTemplateRowsIndexedByPid[$pageId]['root'] ?? 0) === 1) {
break;
}
}
$pageInformation->setLocalRootLine($localRootline);
return $pageInformation;
}
/**
* @return non-empty-array
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function getRootlineOrThrow(ServerRequestInterface $request, int $pageId, string $mountPoint): array
{
$rootLine = [];
try {
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, $mountPoint)->get();
} catch (RootLineException) {
// Empty / broken rootline handled below.
}
if (!empty($rootLine)) {
// All good.
return $rootLine;
}
// Error case: Log and render error.
$message = 'The requested page did not have a proper connection to the tree root!';
$context = [
'pageId' => $pageId,
'requestUrl' => $request->getAttribute('normalizedParams')->getRequestUrl(),
];
$this->logger->error($message, $context);
try {
$response = $this->errorController->internalErrorAction(
$request,
$message,
['code' => PageAccessFailureReasons::ROOTLINE_BROKEN]
);
throw new PageInformationCreationFailedException($response, 1533931350);
} catch (StatusException $up) {
$this->logger->error($message, ['exception' => $up]);
throw $up;
}
}
/**
* @throws PageInformationCreationFailedException
* @throws StatusException
*/
private function verifySiteOrSysTemplateRowExists(ServerRequestInterface $request, PageInformation $pageInformation): void
{
$site = $request->getAttribute('site');
if ((!$site instanceof NullSite && !$site->isTypoScriptRoot()) && $pageInformation->getSysTemplateRows() === []) {
// @todo: The above check for NullSite is done for ext:redirects to not explode on "not existing" sites here.
// This is of course a hack that should vanish when the early url creation of ext:redirects and its fragile
// bootstrap strategy is resolved.
// This check should be: "if (!$site->isTypoScriptRoot() && $pageInformation->getSysTemplateRows() === []) {"
// Early exception if there is no typoscript definition in current site and no sys_template at all.
$message = 'No site configuration or TypoScript template record found!';
$this->logger->error($message);
try {
$response = $this->errorController->internalErrorAction(
$request,
$message,
['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
);
throw new PageInformationCreationFailedException($response, 1705656657);
} catch (StatusException $up) {
$this->logger->error($message, ['exception' => $up]);
throw $up;
}
}
}
private function getBackendUser(): ?FrontendBackendUserAuthentication
{
return $GLOBALS['BE_USER'] ?? null;
}
}