From a848ba4f2798105d86537ae7b1185f661d29bd3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christian=20E=C3=9Fl?= <indy.essl@gmail.com>
Date: Sun, 23 Feb 2020 10:40:32 +0100
Subject: [PATCH] [FEATURE] Allow PageContentErrorHandler to resolve pages with
 sub requests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The PageContentErrorHandler provided by the core can take in either a
URL or a page uid for resolving an error page in the frontend. In both
cases, the class would then start a Guzzle/cURL request to fetch the
error page content.
This has now been changed for internal pages, where a page uid has been
given. In this case, the PageContentErrorHandler will now dispatch an
internal SubRequest instead, to avoid an unnecessary cURL call.

In staging environments, the website would often be access protected
with basic auth options (for example a .htpasswd auth file on Apache
Webservers). In such a case, error pages with the default
PageContentErrorHandler would have failed before, as the internal cURL
call for fetching the error page was lacking these required basic auth
options.
For internal pages, a sub request is now used, bypassing the need for
an external cURL call.

This solution is mostly based on Benni Mack's
LocalPageContentErrorHandler with his approval.

Resolves: #90505
Releases: master
Change-Id: I9da835cd42503d7a52f9050ae5658eae53336a56
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63389
Reviewed-by: Björn Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../PageContentErrorHandler.php               | 105 +++++++++++-------
 ...lerResolveInternalPagesWithSubRequests.rst |  23 ++++
 2 files changed, 88 insertions(+), 40 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-90505-MakePageContentErrorHandlerResolveInternalPagesWithSubRequests.rst

diff --git a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php
index 7935b5905472..c58510c83ffd 100644
--- a/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php
+++ b/typo3/sysext/core/Classes/Error/PageErrorHandler/PageContentErrorHandler.php
@@ -18,14 +18,19 @@ namespace TYPO3\CMS\Core\Error\PageErrorHandler;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\DependencyInjection\FailsafeContainer;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\HtmlResponse;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Http\MiddlewareStackResolver;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Site\Entity\Site;
-use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Http\RequestHandler;
 
 /**
  * Renders the content of a page to be displayed (also in relation to language etc)
@@ -68,8 +73,20 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
      */
     public function handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
     {
+        $linkService = GeneralUtility::makeInstance(LinkService::class);
+        $urlParams = $linkService->resolve((string)$this->errorHandlerConfiguration['errorContentSource']);
+
+        if ($urlParams['type'] !== 'page' && $urlParams['type'] !== 'url') {
+            throw new \InvalidArgumentException('PageContentErrorHandler can only handle TYPO3 urls of types "page" or "url"', 1522826609);
+        }
+
+        if ($urlParams['type'] === 'page') {
+            $response = $this->buildSubRequest($request, (int)$urlParams['pageuid']);
+            return $response->withStatus($this->statusCode);
+        }
+
+        $resolvedUrl = $urlParams['url'];
         try {
-            $resolvedUrl = $this->resolveUrl($request, $this->errorHandlerConfiguration['errorContentSource']);
             $content = null;
             $report = [];
 
@@ -82,59 +99,67 @@ class PageContentErrorHandler implements PageErrorHandlerInterface
         } catch (InvalidRouteArgumentsException | SiteNotFoundException $e) {
             $content = 'Invalid error handler configuration: ' . $this->errorHandlerConfiguration['errorContentSource'];
         }
+
         return new HtmlResponse($content, $this->statusCode);
     }
 
     /**
-     * Resolve the URL (currently only page and external URL are supported)
-     *
      * @param ServerRequestInterface $request
-     * @param string $typoLinkUrl
-     * @return string
+     * @param int $pageId
+     * @return ResponseInterface
      * @throws SiteNotFoundException
-     * @throws InvalidRouteArgumentsException
+     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
+     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
+     * @throws \TYPO3\CMS\Core\Exception
+     * @throws \RuntimeException
      */
-    protected function resolveUrl(ServerRequestInterface $request, string $typoLinkUrl): string
+    protected function buildSubRequest(ServerRequestInterface $request, int $pageId): ResponseInterface
     {
-        $linkService = GeneralUtility::makeInstance(LinkService::class);
-        $urlParams = $linkService->resolve($typoLinkUrl);
-        if ($urlParams['type'] !== 'page' && $urlParams['type'] !== 'url') {
-            throw new \InvalidArgumentException('PageContentErrorHandler can only handle TYPO3 urls of types "page" or "url"', 1522826609);
-        }
-        if ($urlParams['type'] === 'url') {
-            return $urlParams['url'];
-        }
-
         $site = $request->getAttribute('site', null);
         if (!$site instanceof Site) {
-            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$urlParams['pageuid']);
+            $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId);
+            $request = $request->withAttribute('site', $site);
         }
-        $language = $request->getAttribute('language', null);
-        if (!$language instanceof SiteLanguage || !$language->isEnabled()) {
-            $language = $site->getDefaultLanguage();
+
+        if (!$this->pageExistsAndInRootline($pageId, $site->getRootPageId())) {
+            throw new \RuntimeException('Page does not exist or is not in rootline.', 1582448967);
         }
 
-        // Build Url
-        $uri = $site->getRouter()->generateUri(
-            (int)$urlParams['pageuid'],
-            ['_language' => $language]
+        $request = $request->withQueryParams(['id' => $pageId]);
+        $dispatcher = $this->buildDispatcher();
+        return $dispatcher->handle($request);
+    }
+
+    /**
+     * @return MiddlewareDispatcher
+     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
+     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
+     * @throws \TYPO3\CMS\Core\Exception
+     */
+    protected function buildDispatcher()
+    {
+        $requestHandler = GeneralUtility::makeInstance(RequestHandler::class);
+        $resolver = new MiddlewareStackResolver(
+            GeneralUtility::makeInstance(FailsafeContainer::class),
+            GeneralUtility::makeInstance(DependencyOrderingService::class),
+            GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_core')
         );
 
-        // Fallback to the current URL if the site is not having a proper scheme and host
-        $currentUri = $request->getUri();
-        if (empty($uri->getScheme())) {
-            $uri = $uri->withScheme($currentUri->getScheme());
-        }
-        if (empty($uri->getUserInfo())) {
-            $uri = $uri->withUserInfo($currentUri->getUserInfo());
-        }
-        if (empty($uri->getHost())) {
-            $uri = $uri->withHost($currentUri->getHost());
-        }
-        if ($uri->getPort() === null) {
-            $uri = $uri->withPort($currentUri->getPort());
-        }
+        $middlewares = $resolver->resolve('frontend');
+        return new MiddlewareDispatcher($requestHandler, $middlewares);
+    }
 
-        return (string)$uri;
+    /**
+     * @param int $pageId
+     * @param int $rootPageId
+     * @return bool
+     */
+    protected function pageExistsAndInRootline(int $pageId, int $rootPageId): bool
+    {
+        try {
+            return GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId)->getRootPageId() === $rootPageId;
+        } catch (SiteNotFoundException $e) {
+            return false;
+        }
     }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90505-MakePageContentErrorHandlerResolveInternalPagesWithSubRequests.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90505-MakePageContentErrorHandlerResolveInternalPagesWithSubRequests.rst
new file mode 100644
index 000000000000..abd83d06d198
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90505-MakePageContentErrorHandlerResolveInternalPagesWithSubRequests.rst
@@ -0,0 +1,23 @@
+.. include:: ../../Includes.txt
+
+===========================================================================================
+Feature: #90505 - Allow PageContentErrorHandler to resolve internal pages with sub requests
+===========================================================================================
+
+See :issue:`90505`
+
+Description
+===========
+
+The PageContentErrorHandler provided by the core can take in either a URL or a page uid for resolving an error page in the frontend. In both cases, the class would then start a Guzzle/cURL request to fetch the error page content.
+This has now been changed for internal pages, where a page uid has been given. In this case, the PageContentErrorHandler will now dispatch an internal SubRequest instead, to avoid an unnecessary cURL call.
+
+
+Impact
+======
+
+In staging environments, the website would often be access protected with basic auth options (for example a .htpasswd auth file on Apache Webservers).
+In such a case, error pages with the default PageContentErrorHandler would have failed before, as the internal cURL call for fetching the error page was lacking these required basic auth options.
+For internal pages, a sub request is now used, bypassing the need for an external cURL call.
+
+.. index:: Frontend, ext:core
-- 
GitLab