diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 320f276bd25f8c6ac626437f21f26ff07e4dc29d..1be0c876421e0a18879f1d77be5a8d9b60e08831 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -89,6 +89,7 @@ namespace PHPSTORM_META { 'frontend.controller', 'frontend.typoscript', 'frontend.cache.instruction', + 'frontend.page.information', ); override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([ 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, @@ -101,6 +102,7 @@ namespace PHPSTORM_META { 'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, 'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class, 'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class, + 'frontend.page.information' => \TYPO3\CMS\Frontend\Page\PageInformation::class, ])); expectedArguments( diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst index cfc78ed5eb03ca8b16c14c33110561cf78185ef9..cc84c05a3104366d8b9d45a00a9d92a9f44e626e 100644 --- a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst @@ -46,13 +46,13 @@ marked :php:`@internal`, which the core will deprecate with a compatibility laye The following public class properties have been marked "read only": -* :php:`TypoScriptFrontendController->id` -* :php:`TypoScriptFrontendController->rootLine` -* :php:`TypoScriptFrontendController->page` -* :php:`TypoScriptFrontendController->contentPid` -* :php:`TypoScriptFrontendController->sys_page` -* :php:`TypoScriptFrontendController->config` - Reading :php:`$tsfe->config['config']` - and :php:`$tsfe->config['rootLine']` is allowed +* :php:`TypoScriptFrontendController->id` - Use :php:`$request->getAttribute('frontend.page.information')->getId()` instead +* :php:`TypoScriptFrontendController->rootLine` - Use :php:`$request->getAttribute('frontend.page.information')->getRootLine()` instead +* :php:`TypoScriptFrontendController->page` - Use :php:`$request->getAttribute('frontend.page.information')->getPageRecord()` instead +* :php:`TypoScriptFrontendController->contentPid` - Avoid usages altogether, available as :php:`@internal` call using + :php:`$request->getAttribute('frontend.page.information')->getContentFromPid()` +* :php:`TypoScriptFrontendController->sys_page` - Avoid usages altogether, create own instances when needed +* :php:`TypoScriptFrontendController->config` - Reading :php:`$tsfe->config['config']` and :php:`$tsfe->config['rootLine']` is allowed * :php:`TypoScriptFrontendController->absRefPrefix` * :php:`TypoScriptFrontendController->cObj` diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst new file mode 100644 index 0000000000000000000000000000000000000000..615286ebe602020ca4ea1a4037e8116c402f49ee --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst @@ -0,0 +1,60 @@ +.. include:: /Includes.rst.txt + +.. _breaking-102715-1703254781: + +=================================================================== +Breaking: #102715 - Frontend "determineId()" related events changed +=================================================================== + +See :issue:`102715` + +Description +=========== + +With the continued refactoring of :php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController`, +the following events have been adapted: + +* :php:`TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent` +* :php:`TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent` +* :php:`TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent` + +The three events no longer retrieve an instance of :php:`TypoScriptFrontendController`, the +getter methods :php:`getController()` have been removed: The controller is instantiated +*after* the events have been dispatched, event listeners can no longer work with this +object. + +Instead, the events now contain an instance of the new DTO +:php:`TYPO3\CMS\Frontend\Page\PageInformation`, which can be retrieved, +manipulated by event listeners if necessary. + +Impact +====== + +Calling :php:`getController()` by consumers of above events will raise a fatal +PHP error. + +Also note the events may not be dispatched anymore when the middleware +:php:`TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization` creates +early responses. + + +Affected installations +====================== + +Those events are in place for a couple of special cases during early frontend rendering. +Most instances will not be affected, but some extensions may register event listeners. + + +Migration +========= + +Use method :php:`getPageInformation()` instead to retrieve calculated page state at +this point in the Frontend rendering chain. Event listeners that manipulate that +object should set it again within the event using :php:`setPageInformation()`. + +In case middleware :php:`TypoScriptFrontendInitialization` no longer dispatches an event +when it created an early response on its own, an own middleware can be added around +that middleware to retrieve and further manipulate a response if needed. + + +.. index:: Frontend, PHP-API, PartiallyScanned, ext:frontend diff --git a/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst new file mode 100644 index 0000000000000000000000000000000000000000..209d358e32bc52b7606bcb62ccd251bf587fbb11 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst @@ -0,0 +1,43 @@ +.. include:: /Includes.rst.txt + +.. _feature-102715-1703261072: + +================================================================== +Feature: #102715 - New frontend.page.information Request attribute +================================================================== + +See :issue:`102715` + +Description +=========== + +TYPO3 v13 introduces the new Frontend related Request attribute :php:`frontend.page.information` +implemented by class :php:`TYPO3\CMS\Frontend\Page\PageInformation`. The object aims to replace +various page related properties of :php:`TYPO3\CMS\Frontend\Controller\TyposcriptFrontendController`. + +Note the class is currently still marked as experimental. Extension authors are however encouraged +to use information from this Request attribute instead of the :php:`TyposcriptFrontendController` +properties already: TYPO3 core v13 will try to not break especially the getters / properties not +marked as :php:`@internal`. + + +Impact +====== + +There are three properties in :php:`TyposcriptFrontendController` frequently used by extensions, +which are now modeled in :php:`TYPO3\CMS\Frontend\Page\PageInformation`. The attribute is +attached to the PSR-7 frontend Request by middleware :php:`TypoScriptFrontendInitialization`, +middlewares below can rely on existence of that attribute. Examples: + +.. code-block:: php + + $pageInformation = $request->getAttribute('frontend.page.information'); + // Formerly $tsfe->id + $id = $pageInformation->getId(); + // Formerly $tsfe->page + $page = $pageInformation->getPageRecord(); + // Formerly $tsfe->rootLine + $rootLine = $pageInformation->getRootLine(); + + +.. index:: Frontend, PHP-API, ext:frontend diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index bbbd9aa6b9c1cef4b43d66cdaa42e23ce0067832..10c0ef276bc8cb5cb4ec5a9e8b11734c2c5e5576 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -26,20 +26,11 @@ use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\LanguageAspect; -use TYPO3\CMS\Core\Context\LanguageAspectFactory; use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter; use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException; -use TYPO3\CMS\Core\Error\Http\PageNotFoundException; -use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException; -use TYPO3\CMS\Core\Exception\Page\RootLineException; -use TYPO3\CMS\Core\Exception\SiteNotFoundException; -use TYPO3\CMS\Core\Http\ImmediateResponseException; -use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Localization\LanguageServiceFactory; @@ -52,10 +43,7 @@ use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Site\Entity\SiteLanguage; -use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\TimeTracker\TimeTracker; -use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility; -use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Type\DocType; use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode; use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; @@ -71,16 +59,11 @@ use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConst use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; -use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Core\Utility\PathUtility; -use TYPO3\CMS\Core\Utility\RootlineUtility; use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent; -use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent; -use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent; -use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent; use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent; use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent; use TYPO3\CMS\Frontend\Page\CacheHashCalculator; @@ -155,34 +138,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ public int $contentPid = 0; - /** - * Gets set when we are processing a page of type mountpoint with enabled overlay in getPageAndRootline() - * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user - * should be redirected to. - */ - protected ?array $originalMountPointPage = null; - - /** - * Gets set when we are processing a page of type shortcut in the early stages - * of the request, used later in the request to resolve the shortcut and redirect again. - */ - protected ?array $originalShortcutPage = null; - /** * Read-only! Extensions may read but never write this property! */ public ?PageRepository $sys_page = null; - /** - * Is set to > 0 if the page could not be resolved. This will then result in early returns when resolving the page. - */ - protected int $pageNotFound = 0; - - /** - * Array containing a history of why a requested page was not accessible. - */ - protected array $pageAccessFailureHistory = []; - /** * @internal */ @@ -388,7 +348,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->context = $context; $this->site = $site; $this->language = $siteLanguage; - $this->setPageArguments($pageArguments); + $this->pageArguments = $pageArguments; + $this->id = $pageArguments->getPageId(); $this->uniqueString = md5(microtime()); $this->initPageRenderer(); $cacheManager = GeneralUtility::makeInstance(CacheManager::class); @@ -420,482 +381,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->contentType = $contentType; } - /** - * Resolves the page id and sets up several related properties. - * - * At this point, the Context object already contains relevant preview - * settings (if a backend user is logged in etc). - * - * If $this->id is not set at all, the method does its best to set the - * value to an integer. Resolving is based on this options: - * - * - Finding the domain record start page - * - First visible page - * - Relocating the id below the site if outside the site / domain - * - * The following properties may be set up or updated: - * - * - id - * - sys_page - * - sys_page->where_groupAccess - * - sys_page->where_hid_del - * - register['SYS_LASTCHANGED'] - * - pageNotFound - * - * Via getPageAndRootline() - * - * - rootLine - * - page - * - MP - * - originalShortcutPage - * - originalMountPointPage - * - pageAccessFailureHistory['direct_access'] - * - pageNotFound - * - * @internal - */ - public function determineId(ServerRequestInterface $request): ?ResponseInterface - { - $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context); - - $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); - $eventDispatcher->dispatch(new BeforePageIsResolvedEvent($this, $request)); - - $timeTracker = $this->getTimeTracker(); - $timeTracker->push('determineId rootLine/'); - try { - // Sets ->page and ->rootline information based on ->id. ->id may change during this operation. - // If the found Page ID is not within the site, then pageNotFound is set. - $this->getPageAndRootline($request); - // Checks if the rootPageId of the site is in the resolved rootLine. - // This is necessary so that references to page-id's via ?id=123 from other sites are not possible. - $siteRootWithinRootlineFound = false; - foreach ($this->rootLine as $pageInRootLine) { - if ((int)$pageInRootLine['uid'] === $this->site->getRootPageId()) { - $siteRootWithinRootlineFound = true; - break; - } - } - // Page is 'not found' in case the id was outside the domain, code 3 - // This can only happen if there was a shortcut. So $this->page is now the shortcut target - // But the original page is in $this->originalShortcutPage. - // This only happens if people actually call TYPO3 with index.php?id=123 where 123 is in a different - // page tree. This is not allowed. - $directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0); - if (!$siteRootWithinRootlineFound && $directlyRequestedId && (int)($this->originalShortcutPage['uid'] ?? 0) !== $directlyRequestedId) { - $this->pageNotFound = 3; - $this->id = $this->site->getRootPageId(); - // re-get the page and rootline if the id was not found. - $this->getPageAndRootline($request); - } - } catch (ShortcutTargetPageNotFoundException) { - $this->pageNotFound = 1; - } - $timeTracker->pull(); - - $event = new AfterPageWithRootLineIsResolvedEvent($this, $request); - $event = $eventDispatcher->dispatch($event); - if ($event->getResponse()) { - return $event->getResponse(); - } - - $response = null; - try { - $this->evaluatePageNotFound($this->pageNotFound, $request); - - // Setting language and fetch translated page - $this->settingLanguage($request); - // Check the "content_from_pid" field of the resolved page - $this->contentPid = $this->resolveContentPid($request); - - // Update SYS_LASTCHANGED at the very last, when $this->page might be changed - // by settingLanguage() and the $this->page was finally resolved - $this->setRegisterValueForSysLastChanged($this->page); - } catch (PropagateResponseException $e) { - $response = $e->getResponse(); - } - - $event = new AfterPageAndLanguageIsResolvedEvent($this, $request, $response); - $eventDispatcher->dispatch($event); - return $event->getResponse(); - } - - /** - * If $this->pageNotFound is set, then throw an exception to stop further page generation process - */ - protected function evaluatePageNotFound(int $pageNotFoundNumber, ServerRequestInterface $request): void - { - if (!$pageNotFoundNumber) { - return; - } - $response = match ($pageNotFoundNumber) { - 1 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( - $request, - 'ID was not an accessible page', - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED) - ), - 2 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( - $request, - 'Subsection was found and not accessible', - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED) - ), - 3 => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'ID was outside the domain', - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH) - ), - default => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Unspecified error', - $this->getPageAccessFailureReasons() - ), - }; - throw new PropagateResponseException($response, 1533931329); - } - - /** - * Loads the page and root line records based on $this->id - * - * A final page and the matching root line are determined and loaded by - * the algorithm defined by this method. - * - * First it loads the initial page from the page repository for $this->id. - * If that can't be loaded directly, it gets the root line for $this->id. - * It walks up the root line towards the root page until the page - * repository can deliver a page record. (The loading restrictions of - * the root line records are more liberal than that of the page record.) - * - * Now the page type is evaluated and handled if necessary. If the page is - * a short cut, it is replaced by the target page. If the page is a mount - * point in overlay mode, the page is replaced by the mounted page. - * - * After this potential replacements are done, the root line is loaded - * (again) for this page record. It walks up the root line up to - * the first viewable record. - * - * (While upon the first accessibility check of the root line it was done - * by loading page by page from the page repository, this time the method - * checkRootlineForIncludeSection() is used to find the most distant - * accessible page within the root line.) - * - * Having found the final page id, the page record and the root line are - * loaded for last time by this method. - * - * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records - * or root lines. - * - * May set or update these properties: - * - * @see TypoScriptFrontendController::$id - * @see TypoScriptFrontendController::$MP - * @see TypoScriptFrontendController::$page - * @see TypoScriptFrontendController::$pageNotFound - * @see TypoScriptFrontendController::$pageAccessFailureHistory - * @see TypoScriptFrontendController::$originalMountPointPage - * @see TypoScriptFrontendController::$originalShortcutPage - * - * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException - * @throws PageNotFoundException - * @throws ShortcutTargetPageNotFoundException - */ - protected function getPageAndRootline(ServerRequestInterface $request): void - { - $requestedPageRowWithoutGroupCheck = []; - $this->page = $this->sys_page->getPage($this->id); - if (empty($this->page)) { - // If no page, we try to find the page above in the rootLine. - // Page is 'not found' in case the id itself was not an accessible page. code 1 - $this->pageNotFound = 1; - $requestedPageIsHidden = false; - try { - $hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? ''; - $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false); - if (!empty($hiddenField) && !$includeHiddenPages) { - // Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays) - $rawPageRecord = $this->sys_page->getPage_noCheck($this->id); - - // If page record could not be resolved throw exception - if ($rawPageRecord === []) { - $message = 'The requested page does not exist!'; - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - $message, - $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) - ); - throw new PropagateResponseException($response, 1674144383); - } catch (PageNotFoundException $e) { - throw new PageNotFoundException($message, 1674539331); - } - } - - $requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField]; - } - - $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true); - if (!empty($requestedPageRowWithoutGroupCheck)) { - $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck; - } - $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); - if (!empty($this->rootLine)) { - $c = count($this->rootLine) - 1; - while ($c > 0) { - // Add to page access failure history: - $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c]; - // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value. - $c--; - $this->id = (int)$this->rootLine[$c]['uid']; - $this->page = $this->sys_page->getPage($this->id); - if (!empty($this->page)) { - break; - } - } - } - } catch (RootLineException) { - $this->rootLine = []; - } - // If still no page... - if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($this->page))) { - $message = 'The requested page does not exist!'; - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - $message, - $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) - ); - throw new PropagateResponseException($response, 1533931330); - } catch (PageNotFoundException $e) { - throw new PageNotFoundException($message, 1301648780); - } - } - } - // Spacer and sysfolders is not accessible in frontend - $pageDoktype = (int)($this->page['doktype'] ?? 0); - $isSpacerOrSysfolder = $pageDoktype === PageRepository::DOKTYPE_SPACER || $pageDoktype === PageRepository::DOKTYPE_SYSFOLDER; - // Page itself is not accessible, but the parent page is a spacer/sysfolder - if ($isSpacerOrSysfolder && !empty($requestedPageRowWithoutGroupCheck)) { - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( - $request, - 'Subsection was found and not accessible', - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED) - ); - throw new PropagateResponseException($response, 1633171038); - } catch (PageNotFoundException $e) { - throw new PageNotFoundException('Subsection was found and not accessible', 1633171172); - } - } - - if ($isSpacerOrSysfolder) { - $message = 'The requested page does not exist!'; - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - $message, - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE) - ); - throw new PropagateResponseException($response, 1533931343); - } catch (PageNotFoundException $e) { - throw new PageNotFoundException($message, 1301648781); - } - } - // Is the ID a link to another page?? - if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) { - // We need to clear MP if the page is a shortcut. Reason is if the shortcut goes to another page, then we LEAVE the rootline which the MP expects. - $this->MP = ''; - // saving the page so that we can check later - when we know - // about languages - whether we took the correct shortcut or - // whether a translation of the page overwrites the shortcut - // target and we need to follow the new target - $this->settingLanguage($request); - $this->originalShortcutPage = $this->page; - $this->page = $this->sys_page->resolveShortcutPage($this->page, true); - $this->id = (int)$this->page['uid']; - $pageDoktype = (int)($this->page['doktype'] ?? 0); - } - // If the page is a mountpoint which should be overlaid with the contents of the mounted page, - // it must never be accessible directly, but only in the mountpoint context. Therefore we change - // the current ID and the user is redirected by checkPageForMountpointRedirect(). - if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) { - $this->originalMountPointPage = $this->page; - $this->page = $this->sys_page->getPage($this->page['mount_pid']); - if (empty($this->page)) { - $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and ' - . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').'; - throw new PageNotFoundException($message, 1402043263); - } - // If the current page is a shortcut, the MP parameter will be replaced - if ($this->MP === '' || !empty($this->originalShortcutPage)) { - $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid']; - } else { - $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid']; - } - $this->id = (int)$this->page['uid']; - } - // Gets the rootLine - try { - $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); - } catch (RootLineException $e) { - $this->rootLine = []; - } - // If not rootline we're off... - if (empty($this->rootLine)) { - $message = 'The requested page didn\'t have a proper connection to the tree-root!'; - $this->logPageAccessFailure($message, $request); - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( - $request, - $message, - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN) - ); - throw new PropagateResponseException($response, 1533931350); - } catch (AbstractServerErrorException $e) { - $this->logger->error($message, ['exception' => $e]); - $exceptionClass = get_class($e); - throw new $exceptionClass($message, 1301648167); - } - } - // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!) - if ($this->checkRootlineForIncludeSection()) { - if (empty($this->rootLine)) { - $message = 'The requested page does not exist!'; - try { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - $message, - $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) - ); - throw new PropagateResponseException($response, 1533931351); - } catch (AbstractServerErrorException $e) { - $this->logger->warning($message); - $exceptionClass = get_class($e); - throw new $exceptionClass($message, 1301648234); - } - } else { - $el = reset($this->rootLine); - $this->id = (int)$el['uid']; - $this->page = $this->sys_page->getPage($this->id); - try { - $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); - } catch (RootLineException $e) { - $this->rootLine = []; - } - } - } - } - - /** - * Checks if visibility of the page is blocked upwards in the root line. - * - * If any page in the root line is blocking visibility, true is returned. - * - * All pages from the blocking page downwards are removed from the root - * line, so that the remaining pages can be used to relocate the page up - * to lowest visible page. - * - * The blocking feature of a page must be turned on by setting the page - * record field 'extendToSubpages' to 1 in case of hidden, starttime, - * endtime or fe_group restrictions. - * - * 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. - * - * Recyclers are also checked and trigger page not found if found in root - * line. - * - * @todo Find a better name, i.e. checkVisibilityByRootLine - * @todo Invert boolean return value. Return true if visible. - */ - protected function checkRootlineForIncludeSection(): bool - { - $c = count($this->rootLine); - $removeTheRestFlag = false; - $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class); - for ($a = 0; $a < $c; $a++) { - if (!$accessVoter->accessGrantedForPageInRootLine($this->rootLine[$a], $this->context)) { - // Add to page access failure history and mark the page as not found - // Keep the rootline however to trigger an access denied error instead of a service unavailable error - $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a]; - $this->pageNotFound = 2; - } - - if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) { - // If there is a backend user logged in, check if they have read access to the page: - if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) { - // If there was no page selected, the user apparently did not have read access to the - // current page (not position in rootline) and we set the remove-flag... - if (!$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) { - $removeTheRestFlag = true; - } - } else { - // Don't go here, if there is no backend user logged in. - $removeTheRestFlag = true; - } - } - if ($removeTheRestFlag) { - // Page is 'not found' in case a subsection was found and not accessible, code 2 - $this->pageNotFound = 2; - unset($this->rootLine[$a]); - } - } - return $removeTheRestFlag; - } - - /** - * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. - * That data can be used inside a page-not-found handler - * - * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details - * @return array Summary of why page access was not allowed. - * @internal - */ - public function getPageAccessFailureReasons(string $failureReasonCode = null): array - { - $output = []; - if ($failureReasonCode) { - $output['code'] = $failureReasonCode; - } - $combinedRecords = array_merge( - is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]], - is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : [] - ); - if (!empty($combinedRecords)) { - $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class); - foreach ($combinedRecords as $k => $pagerec) { - // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course - // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the - // extendToSubpages flag set, hence checked only then! - if (!$k || $pagerec['extendToSubpages']) { - if ($pagerec['hidden'] ?? false) { - $output['hidden'][$pagerec['uid']] = true; - } - if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) { - $output['starttime'][$pagerec['uid']] = $pagerec['starttime']; - } - if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) { - $output['endtime'][$pagerec['uid']] = $pagerec['endtime']; - } - if (!$accessVoter->groupAccessGranted('pages', $pagerec, $this->context)) { - $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group']; - } - } - } - } - return $output; - } - - protected function setPageArguments(PageArguments $pageArguments): void - { - $this->pageArguments = $pageArguments; - $this->id = $pageArguments->getPageId(); - // We store the originally requested id - if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) { - $this->MP = (string)($pageArguments->getArguments()['MP'] ?? ''); - // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/") - $this->MP = preg_replace('/[^0-9,-]/', '', $this->MP); - } - } - /** * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object. * Excluded parameters are not taken into account when calculating the hash base. @@ -1409,121 +894,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface return $this->id . '_' . sha1(serialize($hashParameters)); } - /** - * Setting the language key that will be used by the current page. - * In this function it should be checked, 1) that this language exists, 2) that a page_overlay_record exists, .. and if not the default language, 0 (zero), should be set. - */ - protected function settingLanguage(ServerRequestInterface $request): void - { - // Get values from site language - $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language); - - $languageId = $languageAspect->getId(); - $languageContentId = $languageAspect->getContentId(); - - $pageTranslationVisibility = new PageTranslationVisibility((int)($this->page['l18n_cfg'] ?? 0)); - // If the incoming language is set to another language than default - if ($languageAspect->getId() > 0) { - // Request the translation for the requested language - $olRec = $this->sys_page->getPageOverlay($this->page, $languageAspect); - $overlaidLanguageId = (int)($olRec['sys_language_uid'] ?? 0); - if ($overlaidLanguageId !== $languageAspect->getId()) { - // If requested translation is not available - if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page is not available in the requested language.', - ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE] - ); - throw new PropagateResponseException($response, 1533931388); - } - switch ($languageAspect->getLegacyLanguageMode()) { - case 'strict': - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page is not available in the requested language (strict).', - ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE] - ); - throw new PropagateResponseException($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 = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page is not available in the requested language (fallbacks did not apply).', - ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE] - ); - throw new PropagateResponseException($response, 1533931402); - } - } - break; - default: - // Default is that everything defaults to the default language... - $languageId = ($languageContentId = 0); - } - } - - // Define the language aspect again now - $languageAspect = GeneralUtility::makeInstance( - LanguageAspect::class, - $languageId, - $languageContentId, - $languageAspect->getOverlayType(), - $languageAspect->getFallbackChain() - ); - - // Setting the $this->page 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 - $this->page = $olRec; - } - - // Set the language aspect - $this->context->setAspect('language', $languageAspect); - - // Setting sys_language_uid inside sys-page by creating a new page repository - $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context); - // If default language is not available - if ((!$languageAspect->getContentId() || !$languageAspect->getId()) - && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() - ) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page is not available in default language.', - ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE] - ); - throw new PropagateResponseException($response, 1533931423); - } - - if ($languageAspect->getId() > 0) { - $this->updateRootLinesWithTranslations(); - } - } - - /** - * Updating content of the two rootLines IF the language key is set! - */ - protected function updateRootLinesWithTranslations(): void - { - try { - $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); - } catch (RootLineException $e) { - $this->rootLine = []; - } - } - /** * Calculates and sets the internal linkVars based upon the current request parameters * and the setting "config.linkVars". @@ -1542,63 +912,19 @@ class TypoScriptFrontendController implements LoggerAwareInterface } /** - * Returns URI of target page, if the current page is an overlaid mountpoint. - * - * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page - * and is accessed directly, the user will be redirected to the mountpoint context. - * @internal - */ - public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string - { - if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) { - return $this->getUriToCurrentPageForRedirect($request); - } - - return null; - } - - /** - * Returns URI of target page, if the current page is a Shortcut. - * - * If the current page is of type shortcut and accessed directly via its URL, - * the user will be redirected to shortcut target. + * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL * * @internal */ - public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string - { - if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) { - // Check if the shortcut page is actually on the current site, if not, this is a "page not found" - // because the request was www.mydomain.com/?id=23 where page ID 23 (which is a shortcut) is on another domain/site. - if ((int)($request->getQueryParams()['id'] ?? 0) > 0) { - try { - $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($this->originalShortcutPage['l10n_parent'] ?: $this->originalShortcutPage['uid']); - } catch (SiteNotFoundException $e) { - $site = null; - } - if ($site !== $this->site) { - $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'ID was outside the domain', - $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH) - ); - throw new ImmediateResponseException($response, 1638022483); - } - } - return $this->getUriToCurrentPageForRedirect($request); - } - - return null; - } - - /** - * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL - */ - protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string + public function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string { $this->calculateLinkVars($request->getQueryParams()); - $parameter = $this->page['uid']; - $type = (int)($this->pageArguments->getPageType() ?: 0); + $pageInformation = $request->getAttribute('frontend.page.information'); + $pageRecord = $pageInformation->getPageRecord(); + $parameter = $pageRecord['uid']; + /** @var PageArguments $pageArguments */ + $pageArguments = $request->getAttribute('routing'); + $type = (int)($pageArguments->getPageType() ?: 0); if ($type) { $parameter .= ',' . $type; } @@ -1688,20 +1014,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface } } - /** - * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use, - * so the register reflects the state of the translated page, not the page in the default language. - * - * @see setSysLastChanged() - */ - protected function setRegisterValueForSysLastChanged(array $page): void - { - $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp']; - if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) { - $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED']; - } - } - /** * Adds tags to this page's cache entry, you can then f.e. remove cache * entries by tag @@ -1716,30 +1028,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface return $this->pageCacheTags; } - /** - * Check the value of "content_from_pid" of the current page record, and see if the current request - * should actually show content from another page. - * - * By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages) - * are evaluated as well. - * - * @param ServerRequestInterface $request - * @return int the current page ID or another one if resolved properly - usually set to $this->contentPid - */ - protected function resolveContentPid(ServerRequestInterface $request): int - { - if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) { - return $this->id; - } - // make REAL copy of TSFE object - not reference! - $temp_copy_TSFE = clone $this; - // Set ->id to the content_from_pid value - we are going to evaluate this pid as was it a given id for a page-display! - $temp_copy_TSFE->id = (int)$this->page['content_from_pid']; - $temp_copy_TSFE->MP = ''; - $temp_copy_TSFE->getPageAndRootline($request); - return $temp_copy_TSFE->id; - } - /** * Sets up TypoScript "config." options and set properties in $TSFE. * @@ -2354,18 +1642,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface return $additionalHeaders; } - /** - * Log the page access failure with additional request information - */ - protected function logPageAccessFailure(string $message, ServerRequestInterface $request): void - { - $context = ['pageId' => $this->id]; - if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) { - $context['requestUrl'] = $normalizedParams->getRequestUrl(); - } - $this->logger->error($message, $context); - } - protected function getBackendUser(): ?FrontendBackendUserAuthentication { return $GLOBALS['BE_USER'] ?? null; diff --git a/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php index 4b48bb928f435e4ac79e3dbfb3c42e176401fc97..8019199f0256e9ec1b8fc02b4facf65e389bc328 100644 --- a/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php +++ b/typo3/sysext/frontend/Classes/Event/AfterPageAndLanguageIsResolvedEvent.php @@ -19,7 +19,7 @@ namespace TYPO3\CMS\Frontend\Event; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Page\PageInformation; /** * A PSR-14 event fired in the frontend process after a given page has been resolved including @@ -31,19 +31,24 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; final class AfterPageAndLanguageIsResolvedEvent { public function __construct( - private TypoScriptFrontendController $controller, - private ServerRequestInterface $request, + private readonly ServerRequestInterface $request, + private PageInformation $pageInformation, private ?ResponseInterface $response ) {} - public function getController(): TypoScriptFrontendController + public function getRequest(): ServerRequestInterface { - return $this->controller; + return $this->request; } - public function getRequest(): ServerRequestInterface + public function getPageInformation(): PageInformation { - return $this->request; + return $this->pageInformation; + } + + public function setPageInformation(PageInformation $pageInformation): void + { + $this->pageInformation = $pageInformation; } public function getResponse(): ?ResponseInterface diff --git a/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php index 9cfbae08d909ccc29347644d8be01cee92b1e370..69e4e9d31c400608ab5f23c63368d37edfb63418 100644 --- a/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php +++ b/typo3/sysext/frontend/Classes/Event/AfterPageWithRootLineIsResolvedEvent.php @@ -19,7 +19,7 @@ namespace TYPO3\CMS\Frontend\Event; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Page\PageInformation; /** * A PSR-14 event fired in the frontend process after a given page has been resolved with permissions, rootline etc. @@ -32,15 +32,10 @@ final class AfterPageWithRootLineIsResolvedEvent private ?ResponseInterface $response = null; public function __construct( - private TypoScriptFrontendController $controller, - private ServerRequestInterface $request + private readonly ServerRequestInterface $request, + private PageInformation $pageInformation, ) {} - public function getController(): TypoScriptFrontendController - { - return $this->controller; - } - public function getRequest(): ServerRequestInterface { return $this->request; @@ -55,4 +50,14 @@ final class AfterPageWithRootLineIsResolvedEvent { return $this->response; } + + public function getPageInformation(): PageInformation + { + return $this->pageInformation; + } + + public function setPageInformation(PageInformation $pageInformation): void + { + $this->pageInformation = $pageInformation; + } } diff --git a/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php b/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php index e93e7e4dac5606721d587f54bc3ca1fffc2f1b22..b216d436e56fffbecc8d918a703749891ed025df 100644 --- a/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php +++ b/typo3/sysext/frontend/Classes/Event/BeforePageIsResolvedEvent.php @@ -18,7 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Frontend\Event; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Page\PageInformation; /** * A PSR-14 event fired before the frontend process is trying to fully resolve a given page @@ -30,17 +30,22 @@ use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; final class BeforePageIsResolvedEvent { public function __construct( - private TypoScriptFrontendController $controller, - private ServerRequestInterface $request + private readonly ServerRequestInterface $request, + private PageInformation $pageInformation, ) {} - public function getController(): TypoScriptFrontendController + public function getRequest(): ServerRequestInterface { - return $this->controller; + return $this->request; } - public function getRequest(): ServerRequestInterface + public function getPageInformation(): PageInformation { - return $this->request; + return $this->pageInformation; + } + + public function setPageInformation(PageInformation $pageInformation): void + { + $this->pageInformation = $pageInformation; } } diff --git a/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php b/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php index 8d6bc78729ada1d4f902c442967c34d3f04f1deb..24b7a139def2bb46902d7f346a51fdd1245b1228 100644 --- a/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php +++ b/typo3/sysext/frontend/Classes/Middleware/ShortcutAndMountPointRedirect.php @@ -24,9 +24,11 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\ImmediateResponseException; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; @@ -86,7 +88,7 @@ class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareI return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( $request, 'Page of type "External URL" could not be resolved properly', - $controller->getPageAccessFailureReasons(PageAccessFailureReasons::INVALID_EXTERNAL_URL) + ['code' => PageAccessFailureReasons::INVALID_EXTERNAL_URL] ); } @@ -95,12 +97,68 @@ class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareI protected function getRedirectUri(ServerRequestInterface $request): ?string { - $controller = $request->getAttribute('frontend.controller'); - $redirectToUri = $controller->getRedirectUriForShortcut($request); + $redirectToUri = $this->getRedirectUriForShortcut($request); if ($redirectToUri !== null) { return $redirectToUri; } - return $controller->getRedirectUriForMountPoint($request); + return $this->getRedirectUriForMountPoint($request); + } + + /** + * Returns URI of target page, if the current page is a Shortcut. + * + * If the current page is of type shortcut and accessed directly via its URL, + * the user will be redirected to shortcut target. + */ + protected function getRedirectUriForShortcut(ServerRequestInterface $request): ?string + { + $pageInformation = $request->getAttribute('frontend.page.information'); + $originalShortcutPageRecord = $pageInformation->getOriginalShortcutPageRecord(); + if (!empty($originalShortcutPageRecord) + && $originalShortcutPageRecord['doktype'] == PageRepository::DOKTYPE_SHORTCUT + ) { + // Check if the shortcut page is actually on the current site, if not, this is a "page not found" + // because the request was www.mydomain.com/?id=23 where page ID 23 (which is a shortcut) is on another domain/site. + if ((int)($request->getQueryParams()['id'] ?? 0) > 0) { + try { + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $targetSite = $siteFinder->getSiteByPageId($originalShortcutPageRecord['l10n_parent'] ?: $originalShortcutPageRecord['uid']); + } catch (SiteNotFoundException) { + $targetSite = null; + } + $site = $request->getAttribute('site'); + if ($targetSite !== $site) { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'ID was outside the domain', + ['code' => PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH] + ); + throw new ImmediateResponseException($response, 1638022483); + } + } + $controller = $request->getAttribute('frontend.controller'); + return $controller->getUriToCurrentPageForRedirect($request); + } + return null; + } + + /** + * Returns URI of target page, if the current page is an overlaid mountpoint. + * + * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page + * and is accessed directly, the user will be redirected to the mountpoint context. + */ + protected function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string + { + $pageInformation = $request->getAttribute('frontend.page.information'); + $originalMountPointPageRecord = $pageInformation->getOriginalMountPointPageRecord(); + if (!empty($originalMountPointPageRecord) + && (int)$originalMountPointPageRecord['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT + ) { + $controller = $request->getAttribute('frontend.controller'); + return $controller->getUriToCurrentPageForRedirect($request); + } + return null; } /** diff --git a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php index 98ed0694204b1ca7ea87846484fef671cf55dfe7..7b6660327638a9daf85ad64e0ec53b260b4897f7 100644 --- a/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php +++ b/typo3/sysext/frontend/Classes/Middleware/TypoScriptFrontendInitialization.php @@ -17,20 +17,41 @@ declare(strict_types=1); namespace TYPO3\CMS\Frontend\Middleware; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Backend\FrontendBackendUserAuthentication; use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Context\LanguageAspectFactory; +use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException; +use TYPO3\CMS\Core\Error\Http\PageNotFoundException; +use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException; +use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException; +use TYPO3\CMS\Core\Exception\Page\RootLineException; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Core\Routing\PageArguments; -use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\TimeTracker\TimeTracker; +use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Core\Utility\RootlineUtility; use TYPO3\CMS\Frontend\Aspect\PreviewAspect; use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\ErrorController; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent; +use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent; +use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent; use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; +use TYPO3\CMS\Frontend\Page\PageInformation; /** * Creates an instance of TypoScriptFrontendController and makes this globally available @@ -43,8 +64,26 @@ use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; */ final class TypoScriptFrontendInitialization implements MiddlewareInterface { + use LoggerAwareTrait; + + /** + * Is set to > 0 if the page could not be resolved. This will then result in early returns when resolving the page. + * + * @todo: Property needs to fall and class should no longer be marked "shared: false" + */ + private int $pageNotFound = 0; + + /** + * Array containing a history of why a requested page was not accessible. + * + * @todo: Property needs to fall and class should no longer be marked "shared: false" + */ + private array $pageAccessFailureHistory = []; + public function __construct( - private readonly Context $context + private readonly Context $context, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly TimeTracker $timeTracker, ) {} /** @@ -67,20 +106,35 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.'); } - $GLOBALS['TYPO3_REQUEST'] = $request; - /** @var Site $site */ $site = $request->getAttribute('site'); $pageArguments = $request->getAttribute('routing'); if (!$pageArguments instanceof PageArguments) { - // Page Arguments must be set in order to validate. This middleware only works if PageArguments - // is available, and is usually combined with the Page Resolver middleware - return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( - $request, - 'Page Arguments could not be resolved', - ['code' => PageAccessFailureReasons::INVALID_PAGE_ARGUMENTS] + throw new \RuntimeException( + 'PageArguments request attribute "routing" not found or valid. A previous middleware should have prepared this.', + 1703150865 ); } + // @todo: It would be better to move the initial creation of PageInformation into 'determineId()' + // and let the method return that object. This would make code flow more clean and allows + // extraction of 'determineId()' to a (stateless) service class. + $pageInformation = new PageInformation(); + $pageInformation->setId($pageArguments->getPageId()); + $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class)); + if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids'] ?? false) { + $mountPoint = (string)($pageArguments->getArguments()['MP'] ?? ''); + // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/") + $mountPoint = preg_replace('/[^0-9,-]/', '', $mountPoint); + $pageInformation->setMountPoint($mountPoint); + } + + $directResponse = $this->determineId($request, $pageInformation); + if ($directResponse) { + return $directResponse; + } + $request = $request->withAttribute('frontend.page.information', $pageInformation); + $GLOBALS['TYPO3_REQUEST'] = $request; + $controller = GeneralUtility::makeInstance( TypoScriptFrontendController::class, $this->context, @@ -88,10 +142,24 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface $request->getAttribute('language', $site->getDefaultLanguage()), $pageArguments ); - $directResponse = $controller->determineId($request); - if ($directResponse) { - return $directResponse; + // b/w compat layer + $controller->id = $pageInformation->getId(); + $controller->sys_page = $pageInformation->getPageRepository(); + $controller->page = $pageInformation->getPageRecord(); + $controller->MP = $pageInformation->getMountPoint(); + $controller->contentPid = $pageInformation->getContentFromPid(); + $controller->rootLine = $pageInformation->getRootLine(); + + // Update SYS_LASTCHANGED at the very last, when $this->page might be changed + // by settingLanguage() and the $this->page was finally resolved. + // Is also called when a translated page is in use, so the register reflects + // the state of the translated page, not the page in the default language. + $pageRecord = $pageInformation->getPageRecord(); + $controller->register['SYS_LASTCHANGED'] = (int)$pageRecord['tstamp']; + if ($controller->register['SYS_LASTCHANGED'] < (int)$pageRecord['SYS_LASTCHANGED']) { + $controller->register['SYS_LASTCHANGED'] = (int)$pageRecord['SYS_LASTCHANGED']; } + // Check if backend user has read access to this page. if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false) && $this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false) @@ -100,7 +168,7 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface return GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( $request, 'ID was not an accessible page', - $controller->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED) + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED) ); } @@ -111,4 +179,602 @@ final class TypoScriptFrontendInitialization implements MiddlewareInterface $GLOBALS['TSFE'] = $controller; return $handler->handle($request); } + + /** + * Set up proper PageInformation object later available as + * 'frontend.page.information' Request attribute. + * + * At this point, the Context object already contains relevant preview + * settings - e.g. if a backend user is logged in. + * + * @internal public since it is directly called as hack by RedirectService. Will change. + */ + public function determineId(ServerRequestInterface $request, PageInformation $pageInformation): ?ResponseInterface + { + $event = $this->eventDispatcher->dispatch(new BeforePageIsResolvedEvent($request, $pageInformation)); + $pageInformation = $event->getPageInformation(); + + $site = $request->getAttribute('site'); + + $this->timeTracker->push('determineId rootLine/'); + try { + // Sets ->page and ->rootline information based on ->id. ->id may change during this operation. + // If the found Page ID is not within the site, then pageNotFound is set. + $this->getPageAndRootline($request, $pageInformation); + // Checks if the rootPageId of the site is in the resolved rootLine. + // This is necessary so that references to page-id's via ?id=123 from other sites are not possible. + $siteRootWithinRootlineFound = false; + $rootLine = $pageInformation->getRootLine(); + foreach ($rootLine as $pageInRootLine) { + if ((int)$pageInRootLine['uid'] === $site->getRootPageId()) { + $siteRootWithinRootlineFound = true; + break; + } + } + // Page is 'not found' in case the id was outside the domain, code 3. + // This can only happen if there was a shortcut. So $pageInformation->pageRecord is now the shortcut target, + // but the original page is in $pageInformation->originalShortcutPageRecord. This only happens if people actually + // call TYPO3 with index.php?id=123 where 123 is in a different page tree, which is not allowed. + // Render the site root page instead. + $directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0); + if (!$siteRootWithinRootlineFound && $directlyRequestedId && (int)($pageInformation->getOriginalShortcutPageRecord()['uid'] ?? 0) !== $directlyRequestedId) { + $this->pageNotFound = 3; + $pageInformation->setId($site->getRootPageId()); + // re-get the page and rootline if the id was not found. + $this->getPageAndRootline($request, $pageInformation); + } + } catch (ShortcutTargetPageNotFoundException) { + $this->pageNotFound = 1; + } + $this->timeTracker->pull(); + + $event = $this->eventDispatcher->dispatch(new AfterPageWithRootLineIsResolvedEvent($request, $pageInformation)); + $pageInformation = $event->getPageInformation(); + if ($event->getResponse()) { + return $event->getResponse(); + } + + $response = null; + try { + $this->evaluatePageNotFound($this->pageNotFound, $request); + // Setting language and fetch translated page + $pageInformation = $this->settingLanguage($request, $pageInformation); + // Check the "content_from_pid" field of the resolved page + $pageInformation->setContentFromPid($this->resolveContentPid($request, $pageInformation)); + } catch (PropagateResponseException $e) { + $response = $e->getResponse(); + } + + $event = $this->eventDispatcher->dispatch(new AfterPageAndLanguageIsResolvedEvent($request, $pageInformation, $response)); + $pageInformation = $event->getPageInformation(); + // @todo: Change signature of method to always throw 'early' exceptions with response, if one is created. + // Catch in calling method. After that, return the 'final' pageInformation object here instead + // to follow 'normal' code flow. + return $event->getResponse(); + } + + /** + * Loads the page and root line records. + * + * A final page and the matching root line are determined and loaded by + * the algorithm defined by this method. + * + * First it loads the initial page from the page repository for given page uid. + * If that can't be loaded directly, it gets the root line for given page uid. + * It walks up the root line towards the root page until the page + * repository can deliver a page record. (The loading restrictions of + * the root line records are more liberal than that of the page record.) + * + * Now the page type is evaluated and handled if necessary. If the page is + * a shortcut, it is replaced by the target page. If the page is a mount + * point in overlay mode, the page is replaced by the mounted page. + * + * After this potential replacements are done, the root line is loaded + * (again) for this page record. It walks up the rootLine up to + * the first viewable record. + * + * While upon the first accessibility check of the root line it was done + * by loading page by page from the page repository, this time the method + * checkRootlineForIncludeSection() is used to find the most distant + * accessible page within the root line. + * + * Having found the final page id, the page record and the root line are + * loaded for last time by this method. + * + * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records + * or root lines. + * + * @throws ServiceUnavailableException + * @throws PageNotFoundException + * @throws ShortcutTargetPageNotFoundException + */ + protected function getPageAndRootline(ServerRequestInterface $request, PageInformation $pageInformation): void + { + $requestedPageRowWithoutGroupCheck = []; + $id = $pageInformation->getId(); + $pageRepository = $pageInformation->getPageRepository(); + $mountPoint = $pageInformation->getMountPoint(); + $pageRecord = $pageRepository->getPage($id); + $pageInformation->setPageRecord($pageRecord); + if (empty($pageRecord)) { + // If no page, we try to find the page above in the rootLine. + // Page is 'not found' in case the id itself was not an accessible page. code 1 + $this->pageNotFound = 1; + $requestedPageIsHidden = false; + try { + $hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? ''; + $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false); + if (!empty($hiddenField) && !$includeHiddenPages) { + // Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays) + $rawPageRecord = $pageRepository->getPage_noCheck($id); + // If page record could not be resolved throw exception + if ($rawPageRecord === []) { + $message = 'The requested page does not exist!'; + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + $message, + ['code' => PageAccessFailureReasons::PAGE_NOT_FOUND] + ); + throw new PropagateResponseException($response, 1674144383); + } catch (PageNotFoundException) { + throw new PageNotFoundException($message, 1674539331); + } + } + $requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField]; + } + $requestedPageRowWithoutGroupCheck = $pageRepository->getPage($id, true); + if (!empty($requestedPageRowWithoutGroupCheck)) { + $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck; + } + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get(); + $pageInformation->setRootLine($rootLine); + if (!empty($rootLine)) { + $c = count($rootLine) - 1; + while ($c > 0) { + // Add to page access failure history: + $this->pageAccessFailureHistory['direct_access'][] = $rootLine[$c]; + // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value. + $c--; + $id = (int)$rootLine[$c]['uid']; + $pageInformation->setId($id); + $pageRecord = $pageRepository->getPage($id); + $pageInformation->setPageRecord($pageRecord); + if (!empty($pageRecord)) { + break; + } + } + } + unset($rootLine); + } catch (RootLineException) { + // @todo: Empty for now, $pageInformation->rootLine will stay empty array. *Maybe* the try-catch could + // be relocated around the RootlineUtility->get() call above, but it is currently unclear if + // ErrorController->pageNotFoundAction() may eventually throw such exceptions as well? + } + if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($pageRecord))) { + // Error out if there is still no page record! + $message = 'The requested page does not exist!'; + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + $message, + $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) + ); + throw new PropagateResponseException($response, 1533931330); + } catch (PageNotFoundException) { + throw new PageNotFoundException($message, 1301648780); + } + } + } + + // Spacer and sysfolders are not accessible in frontend + $pageDoktype = (int)($pageRecord['doktype'] ?? 0); + $isSpacerOrSysfolder = $pageDoktype === PageRepository::DOKTYPE_SPACER || $pageDoktype === PageRepository::DOKTYPE_SYSFOLDER; + // Page itself is not accessible, but the parent page is a spacer/sysfolder + if ($isSpacerOrSysfolder && !empty($requestedPageRowWithoutGroupCheck)) { + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( + $request, + 'Subsection was found and not accessible', + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED) + ); + throw new PropagateResponseException($response, 1633171038); + } catch (PageNotFoundException) { + throw new PageNotFoundException('Subsection was found and not accessible', 1633171172); + } + } + if ($isSpacerOrSysfolder) { + $message = 'The requested page does not exist!'; + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + $message, + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE) + ); + throw new PropagateResponseException($response, 1533931343); + } catch (PageNotFoundException) { + throw new PageNotFoundException($message, 1301648781); + } + } + + // Is the ID a link to another page? + if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) { + // 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() + $pageRepository = $pageInformation->getPageRepository(); + $pageRecord = $pageInformation->getPageRecord(); + $pageInformation->setOriginalShortcutPageRecord($pageRecord); + $pageRecord = $pageRepository->resolveShortcutPage($pageRecord, true); + $pageInformation->setPageRecord($pageRecord); + $id = (int)$pageRecord['uid']; + $pageInformation->setId($id); + $pageDoktype = (int)($pageRecord['doktype'] ?? 0); + } + + // 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. + if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $pageRecord['mount_pid_ol']) { + $originalMountPointPageRecord = $pageRecord; + $pageInformation->setOriginalMountPointPageRecord($pageRecord); + $pageRecord = $pageRepository->getPage($originalMountPointPageRecord['mount_pid']); + if (empty($pageRecord)) { + $message = 'This page (ID ' . $originalMountPointPageRecord['uid'] . ') is of type "Mount point" and ' + . 'mounts a page which is not accessible (ID ' . $originalMountPointPageRecord['mount_pid'] . ').'; + throw new PageNotFoundException($message, 1402043263); + } + $pageInformation->setPageRecord($pageRecord); + // If the current page is a shortcut, the MP parameter will be replaced + if ($mountPoint === '' || !empty($pageInformation->getOriginalShortcutPageRecord())) { + $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. + try { + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get(); + } catch (RootLineException) { + $rootLine = []; + } + if (empty($rootLine)) { + $message = 'The requested page did not have a proper connection to the tree root!'; + $context = ['pageId' => $id]; + if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) { + $context['requestUrl'] = $normalizedParams->getRequestUrl(); + } + $this->logger->error($message, $context); + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( + $request, + $message, + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN) + ); + throw new PropagateResponseException($response, 1533931350); + } catch (AbstractServerErrorException $e) { + $this->logger->error($message, ['exception' => $e]); + $exceptionClass = get_class($e); + throw new $exceptionClass($message, 1301648167); + } + } + $pageInformation->setRootLine($rootLine); + + // Check for include section regarding hidden/starttime/endtime/fe_user - access control of a whole subbranch. + $updatedRootLine = $this->checkRootlineForIncludeSection($pageInformation); + if ($updatedRootLine !== null) { + if (empty($updatedRootLine)) { + $message = 'The requested page does not exist!'; + try { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + $message, + $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) + ); + throw new PropagateResponseException($response, 1533931351); + } catch (AbstractServerErrorException $e) { + $this->logger->warning($message); + $exceptionClass = get_class($e); + throw new $exceptionClass($message, 1301648234); + } + } + $el = reset($updatedRootLine); + $id = (int)$el['uid']; + $pageInformation->setId($id); + $pageRecord = $pageRepository->getPage($id); + $pageInformation->setPageRecord($pageRecord); + try { + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $id, $mountPoint)->get(); + } catch (RootLineException) { + $rootLine = []; + } + $pageInformation->setRootLine($rootLine); + } + } + + /** + * If $this->pageNotFound is set, then throw an exception to stop further page generation process + */ + protected function evaluatePageNotFound(int $pageNotFoundNumber, ServerRequestInterface $request): void + { + if (!$pageNotFoundNumber) { + return; + } + $response = match ($pageNotFoundNumber) { + 1 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( + $request, + 'ID was not an accessible page', + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED) + ), + 2 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( + $request, + 'Subsection was found and not accessible', + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED) + ), + 3 => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'ID was outside the domain', + $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH) + ), + default => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'Unspecified error', + $this->getPageAccessFailureReasons() + ), + }; + throw new PropagateResponseException($response, 1533931329); + } + + /** + * Setting the language key that will be used by the current page. + * In this function it should be checked, 1) that this language exists, 2) that a page_overlay_record + * exists, and if not the default language, 0 (zero), should be set. + * + * May reset: + * $pageInformation->pageRecord + * $pageInformation->pageRepository + * $pageInformation->rootLine + */ + protected function settingLanguage(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation + { + // Get values from site language + $site = $request->getAttribute('site'); + $language = $request->getAttribute('language', $site->getDefaultLanguage()); + $languageAspect = LanguageAspectFactory::createFromSiteLanguage($language); + $languageId = $languageAspect->getId(); + $languageContentId = $languageAspect->getContentId(); + + $pageRecord = $pageInformation->getPageRecord(); + $pageRepository = $pageInformation->getPageRepository(); + + $pageTranslationVisibility = new PageTranslationVisibility((int)($pageRecord['l18n_cfg'] ?? 0)); + // If the incoming language is set to another language than default + if ($languageAspect->getId() > 0) { + // Request the translation for the requested language + $olRec = $pageRepository->getPageOverlay($pageRecord, $languageAspect); + $overlaidLanguageId = (int)($olRec['sys_language_uid'] ?? 0); + if ($overlaidLanguageId !== $languageAspect->getId()) { + // If requested translation is not available + if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'Page is not available in the requested language.', + ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE] + ); + throw new PropagateResponseException($response, 1533931388); + } + switch ($languageAspect->getLegacyLanguageMode()) { + case 'strict': + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'Page is not available in the requested language (strict).', + ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE] + ); + throw new PropagateResponseException($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 = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'Page is not available in the requested language (fallbacks did not apply).', + ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE] + ); + throw new PropagateResponseException($response, 1533931402); + } + } + break; + default: + // Default is that everything defaults to the default language... + $languageId = ($languageContentId = 0); + } + } + + // Define the language aspect again now + $languageAspect = GeneralUtility::makeInstance( + LanguageAspect::class, + $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 language aspect + $this->context->setAspect('language', $languageAspect); + // Setting sys_language_uid inside sys-page by creating a new page repository + $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class)); + // If default language is not available + if ((!$languageAspect->getContentId() || !$languageAspect->getId()) + && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() + ) { + $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( + $request, + 'Page is not available in default language.', + ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE] + ); + throw new PropagateResponseException($response, 1533931423); + } + + if ($languageAspect->getId() > 0) { + // Updating rootLine with translations if the language key is set. + try { + $pageInformation->setRootLine(GeneralUtility::makeInstance( + RootlineUtility::class, + $pageInformation->getId(), + $pageInformation->getMountPoint() + )->get()); + } catch (RootLineException) { + $pageInformation->setRootLine([]); + } + } + + return $pageInformation; + } + + /** + * Check the value of "content_from_pid" of the current page record, and see if the current request + * should actually show content from another page. + * + * @return int the current page ID or another one if resolved properly + */ + protected function resolveContentPid(ServerRequestInterface $request, PageInformation $pageInformation): int + { + $pageRecord = $pageInformation->getPageRecord(); + if (empty($pageRecord['content_from_pid'])) { + return $pageInformation->getId(); + } + // @todo: This does *not* re-init $pageInformation->pageRepository. + // It is currently unclear if this has positive or negative side effects! + $pageInformation = clone $pageInformation; + // Set id to the content_from_pid value - we are going to evaluate this pid as if it was a given id for a page-display. + $pageInformation->setId($pageRecord['content_from_pid']); + $pageInformation->setMountPoint(''); + $this->getPageAndRootline($request, $pageInformation); + return $pageInformation->getId(); + } + + /** + * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. + * That data can be used inside a page-not-found handler + * + * @param string|null $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details + * @return array Summary of why page access was not allowed. + */ + protected function getPageAccessFailureReasons(string $failureReasonCode = null): array + { + $output = []; + if ($failureReasonCode) { + $output['code'] = $failureReasonCode; + } + $combinedRecords = array_merge( + is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]], + is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : [] + ); + if (!empty($combinedRecords)) { + $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class); + foreach ($combinedRecords as $k => $pagerec) { + // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course + // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the + // extendToSubpages flag set, hence checked only then! + if (!$k || $pagerec['extendToSubpages']) { + if ($pagerec['hidden'] ?? false) { + $output['hidden'][$pagerec['uid']] = true; + } + if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) { + $output['starttime'][$pagerec['uid']] = $pagerec['starttime']; + } + if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) { + $output['endtime'][$pagerec['uid']] = $pagerec['endtime']; + } + if (!$accessVoter->groupAccessGranted('pages', $pagerec, $this->context)) { + $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group']; + } + } + } + } + return $output; + } + + /** + * Checks if visibility of the page is blocked upwards in the root line. + * + * If any page in the root line is blocking visibility, true is returned. + * + * All pages from the blocking page downwards are removed from the root + * line, so that the remaining pages can be used to relocate the page up + * to the lowest visible page. + * + * The blocking feature of a page must be turned on by setting the page + * record field 'extendToSubpages' to 1 in case of hidden, starttime, + * endtime or fe_group restrictions. + * + * 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. + */ + protected function checkRootlineForIncludeSection(PageInformation $pageInformation): ?array + { + $rootLine = $pageInformation->getRootLine(); + $pageRecord = $pageInformation->getPageRecord(); + $c = count($rootLine); + $removeTheRestFlag = false; + $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class); + for ($a = 0; $a < $c; $a++) { + if (!$accessVoter->accessGrantedForPageInRootLine($rootLine[$a], $this->context)) { + // Add to page access failure history and mark the page as not found. + // Keep the rootLine however to trigger access denied error instead of a service unavailable error + $this->pageAccessFailureHistory['sub_section'][] = $rootLine[$a]; + $this->pageNotFound = 2; + } + if ((int)$rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) { + // If there is a backend user logged in, check if they have read access to the page + if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) { + // If there was no page selected, the user apparently did not have read access to the + // current page (not position in rootLine) and we set the remove-flag... + if (!$this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW)) { + $removeTheRestFlag = true; + } + } else { + // Don't go here, if there is no backend user logged in. + $removeTheRestFlag = true; + } + } + if ($removeTheRestFlag) { + // Page is 'not found' in case a subsection was found and not accessible, code 2 + $this->pageNotFound = 2; + unset($rootLine[$a]); + } + } + if ($removeTheRestFlag) { + return $rootLine; + } + return null; + } + + protected function getBackendUser(): ?FrontendBackendUserAuthentication + { + return $GLOBALS['BE_USER'] ?? null; + } } diff --git a/typo3/sysext/frontend/Classes/Page/PageInformation.php b/typo3/sysext/frontend/Classes/Page/PageInformation.php new file mode 100644 index 0000000000000000000000000000000000000000..6275be971ba8cf2584267549dcad9712d2acb785 --- /dev/null +++ b/typo3/sysext/frontend/Classes/Page/PageInformation.php @@ -0,0 +1,195 @@ +<?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 TYPO3\CMS\Core\Domain\Repository\PageRepository; + +/** + * This DTO carries various Frontend rendering related page information. It is + * set up by a Frontend middleware and attached to as 'frontend.page.information' + * Request attribute. + * + * @internal Still experimental + */ +final class PageInformation +{ + private int $id; + private PageRepository $pageRepository; + private array $pageRecord; + private string $mountPoint = ''; + private int $contentFromPid; + + /** + * Gets set when we are processing a page of type shortcut in the early stages + * of the request, used later in a middleware to resolve the shortcut and redirect again. + */ + private ?array $originalShortcutPageRecord = null; + + /** + * Gets set when we are processing a page of type mountpoint with enabled overlay in getPageAndRootline() + * Used later in a middleware to determine the final target URL where the user should be redirected to. + */ + private ?array $originalMountPointPageRecord = null; + + /** + * Rootline of page records all the way to the root. + * + * Both language and version overlays are applied to these page records: + * All "data" fields are set to language / version overlay values, *except* uid and + * pid, which are the default-language and live-version ids. + * + * First array row with the highest key is the deepest page (the requested page), + * then parent pages with descending keys until (but not including) the + * project root pseudo page 0. + * + * When page uid 5 is called in this example: + * [0] Project name + * |- [2] An organizational page, probably with is_siteroot=1 and a site config + * |- [3] Site root with a sys_template having "root" flag set + * |- [5] Here you are + * + * This $absoluteRootLine is: + * [3] => [uid = 5, pid = 3, title = Here you are, ...] + * [2] => [uid = 3, pid = 2, title = Site root with a sys_template having "root" flag set, ...] + * [1] => [uid = 2, pid = 0, title = An organizational page, probably with is_siteroot=1 and a site config, ...] + * + * Read-only! Extensions may read but never write this property! + * + * @var array<int, array<string, mixed>> + */ + private array $rootLine = []; + + /** + * @internal Only to be set by core + */ + public function setId(int $id): void + { + $this->id = $id; + } + + public function getId(): int + { + return $this->id; + } + + /** + * @internal Only to be set by core + */ + public function setPageRepository(PageRepository $pageRepository): void + { + $this->pageRepository = $pageRepository; + } + + /** + * @internal Only to be read by core + */ + public function getPageRepository(): PageRepository + { + return $this->pageRepository; + } + + /** + * @internal Only to be set by core + */ + public function setPageRecord(array $pageRecord): void + { + $this->pageRecord = $pageRecord; + } + + public function getPageRecord(): array + { + return $this->pageRecord; + } + + /** + * @internal Only to be set by core + */ + public function setMountPoint(string $mountPoint): void + { + $this->mountPoint = $mountPoint; + } + + /** + * @internal Only to be read by core + */ + public function getMountPoint(): string + { + return $this->mountPoint; + } + + /** + * @internal Only to be set by core + */ + public function setRootLine(array $rootLine): void + { + $this->rootLine = $rootLine; + } + + public function getRootLine(): array + { + return $this->rootLine; + } + + /** + * @internal Only to be set by core + */ + public function setOriginalShortcutPageRecord(array $originalShortcutPageRecord): void + { + $this->originalShortcutPageRecord = $originalShortcutPageRecord; + } + + /** + * @internal Only to be read by core + */ + public function getOriginalShortcutPageRecord(): ?array + { + return $this->originalShortcutPageRecord; + } + + /** + * @internal Only to be set by core + */ + public function setOriginalMountPointPageRecord(array $originalMountPointPageRecord): void + { + $this->originalMountPointPageRecord = $originalMountPointPageRecord; + } + + /** + * @internal Only to be read by core + */ + public function getOriginalMountPointPageRecord(): ?array + { + return $this->originalMountPointPageRecord; + } + + /** + * @internal Only to be set by core + */ + public function setContentFromPid(int $contentFromPid): void + { + $this->contentFromPid = $contentFromPid; + } + + /** + * @internal Only to be read by core + */ + public function getContentFromPid(): int + { + return $this->contentFromPid; + } +} diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml index 18d8e0c8ae6c27dd84312000a2239326096dc167..9b76fa7d995d0f9dc7f20a155b51e2756f6542e4 100644 --- a/typo3/sysext/frontend/Configuration/Services.yaml +++ b/typo3/sysext/frontend/Configuration/Services.yaml @@ -15,6 +15,10 @@ services: arguments: $cache: '@cache.assets' + # @todo: Remove "shared: false" again when this middleware is stateless + TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization: + shared: false + TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor: public: true diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php index d9a21846c45a6b28656d8a72ae00dcf1402bdba5..4b7d68a9ef4e0d2a19262d98d9290272e5050fb2 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php @@ -6255,4 +6255,25 @@ return [ 'Breaking-102621-MostTSFEMembersMarkedInternalOrRead-only.rst', ], ], + 'TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent->getController' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst', + ], + ], + 'TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent->getController' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst', + ], + ], + 'TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent->getController' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst', + ], + ], ]; diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php index 1bedbb7197c59587f07d50b9ed94d1da68509b8d..081e1f91c736a71ba4fd09fc94e116052815f22a 100644 --- a/typo3/sysext/redirects/Classes/Service/RedirectService.php +++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php @@ -39,6 +39,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Frontend\Cache\CacheInstruction; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\CMS\Frontend\Middleware\TypoScriptFrontendInitialization; +use TYPO3\CMS\Frontend\Page\PageInformation; use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder; use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent; @@ -389,14 +391,29 @@ class RedirectService implements LoggerAwareInterface { $cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction()); $originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction); + $pageArguments = new PageArguments($site->getRootPageId(), '0', []); + $pageInformation = new PageInformation(); + $pageInformation->setId($site->getRootPageId()); + $pageInformation->setPageRepository(GeneralUtility::makeInstance(PageRepository::class)); + $pageInformation->setMountPoint(''); + $tsfeInitMiddleware = GeneralUtility::makeInstance(TypoScriptFrontendInitialization::class); + // @todo: Evil hack. + $tsfeInitMiddleware->determineId($originalRequest, $pageInformation); + $originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation); $controller = GeneralUtility::makeInstance( TypoScriptFrontendController::class, GeneralUtility::makeInstance(Context::class), $site, $site->getDefaultLanguage(), - new PageArguments($site->getRootPageId(), '0', []) + $pageArguments ); - $controller->determineId($originalRequest); + // b/w compat layer + $controller->id = $pageInformation->getId(); + $controller->sys_page = $pageInformation->getPageRepository(); + $controller->page = $pageInformation->getPageRecord(); + $controller->MP = $pageInformation->getMountPoint(); + $controller->contentPid = $pageInformation->getContentFromPid(); + $controller->rootLine = $pageInformation->getRootLine(); $controller->calculateLinkVars($queryParams); $newRequest = $controller->getFromCache($originalRequest); $controller->releaseLocks(); @@ -404,9 +421,6 @@ class RedirectService implements LoggerAwareInterface if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) { $GLOBALS['TSFE'] = $controller; } - if (!$GLOBALS['TSFE']->sys_page instanceof PageRepository) { - $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class); - } return $controller; }