From a4cef9b31e628e12f5d0fd6ca0499956d2bc5b3b Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Thu, 21 Dec 2023 17:35:53 +0100
Subject: [PATCH] [!!!][FEATURE] Establish FE frontend.page.information
 attribute
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The patch extracts TSFE->determineId() and its sub
methods from TSFE into TypoScriptFrontendInitialization
middleware.

The information created by these methods is modeled into
the new DTO PageInformation, which is added as
'frontend.page.information' request attribute before
handling other middlewares below. The non-internal old
properties within TSFE are set and kept as b/w compat
layer for now.

This is a powerful change: It allows us to reduce
dependencies to TSFE significantly, which we will
start to leverage with upcoming patches.

The patch is an intermediate change as such: Looking
at determineId() and its related methods makes clear
the code can benefit heavily from further refactoring.
The method could be ultimately extracted into
a service class that only returns the 'final'
PageInformation object, or throws exceptions for
'early' responses. To keep the patch reviewable at
this point, these refactorings will continue with
additional patches.

Detail notes:

* TSFE->sys_page PageRepository instance is for now
  modeled as (@internal) PageInformation->pageRepository.
  This can be refactored away later: Consumers should
  create instances of that class on demand.

* "original mount and shortcut page record" are also
  modeled as (@internal) PageInformation properties
  for now. They are internal handling since the redirects
  can only be created after TypoScript has been calculated
  later on, so the information has to be carried around
  for now. We *may* be able to change this later.

* The two middleware properties $pageNotFound and
  $pageAccessFailureHistory are a tribute to the current
  code flow in determineId(). They should be refactored
  away when the code is further shuffled around. The
  properties currently require a hack that needs to make
  the middleware "shared: false" to circumvent side
  effects in combination with error handling sub requests.

* The code flow around determineId() updates local state
  and state of PageInformation multiple times and is in
  general very hard to follow. The patch crafted this
  carefully and did not refactor the code flow heavily
  for now. Changing the early response creation strategy
  will make the code flow much more straightforward
  later.

* determineId() and with it the PageInformation object
  is now created *before* TSFE is instantiated, shifting
  the instantiation to a slightly later point.

* getPageAccessFailureReasons() is for now transferred
  to the middleware as well. It can be later dissolved
  when determineId() receives further refactoring. Two
  consumers (indirectly) by ShortcutAndMountPointRedirect
  middleware could be dissolved already, so the method could
  be removed from TSFE.

* TSFE->linkVars needs to be dissolved soon: The strategy
  should be to create "linkVars" on demand within the
  link handling code and eventually encapsulate it with
  a runtime cache entry.

* getRedirectUriForMountPoint() and getRedirectUriForShortcut()
  are relocated to ShortcutAndMountPointRedirect
  middleware. TSFE->getUriToCurrentPageForRedirect() had
  to be made (@internal) public to do that, but that method
  can be refactored away when TSFE->linkVars is dissolved.

* The hacks within RedirectService are extended to deal
  with the new situation for now. The entire thing needs
  to fall at a later point completely.

* Further patches should start avoiding the TSFE properties
  that are now set as b/w compat layer, and start using
  the PageInformation request attribute instead. This will
  reduce overall dependencies to TSFE significantly.

Resolves: #102715
Releases: main
Change-Id: I6470899cf65cbaaeb2177a8b20c0800f045a070c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/82267
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .phpstorm.meta.php                            |   2 +
 ...stTSFEMembersMarkedInternalOrRead-only.rst |  14 +-
 ...rontendDetermineIdRelatedEventsChanged.rst |  60 ++
 ...rontendpageinformationRequestAttribute.rst |  43 +
 .../TypoScriptFrontendController.php          | 744 +-----------------
 .../AfterPageAndLanguageIsResolvedEvent.php   |  19 +-
 .../AfterPageWithRootLineIsResolvedEvent.php  |  21 +-
 .../Event/BeforePageIsResolvedEvent.php       |  19 +-
 .../ShortcutAndMountPointRedirect.php         |  66 +-
 .../TypoScriptFrontendInitialization.php      | 694 +++++++++++++++-
 .../frontend/Classes/Page/PageInformation.php | 195 +++++
 .../frontend/Configuration/Services.yaml      |   4 +
 .../Php/MethodCallMatcher.php                 |  21 +
 .../Classes/Service/RedirectService.php       |  24 +-
 14 files changed, 1140 insertions(+), 786 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Breaking-102715-FrontendDetermineIdRelatedEventsChanged.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.0/Feature-102715-NewFrontendpageinformationRequestAttribute.rst
 create mode 100644 typo3/sysext/frontend/Classes/Page/PageInformation.php

diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
index 320f276bd25f..1be0c876421e 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 cfc78ed5eb03..cc84c05a3104 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 000000000000..615286ebe602
--- /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 000000000000..209d358e32bc
--- /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 bbbd9aa6b9c1..10c0ef276bc8 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 4b48bb928f43..8019199f0256 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 9cfbae08d909..69e4e9d31c40 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 e93e7e4dac56..b216d436e56f 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 8d6bc78729ad..24b7a139def2 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 98ed0694204b..7b6660327638 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 000000000000..6275be971ba8
--- /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 18d8e0c8ae6c..9b76fa7d995d 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 d9a21846c45a..4b7d68a9ef4e 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 1bedbb7197c5..081e1f91c736 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;
     }
 
-- 
GitLab