From a40a4d210faa84c97324a4fc8a4a36ce0b8ba4a1 Mon Sep 17 00:00:00 2001
From: Susanne Moog <look@susi.dev>
Date: Thu, 6 Jul 2023 10:28:26 +0200
Subject: [PATCH] [BUGFIX] Check rootline for extendToSubpages when previewing

The rootline may contain extendToSubpages settings for hidden
or timed records which prevented previewing a page because the
preview checks only considered the current page.

The rootline is now taken into account.

Resolves: #17599
Releases: main, 12.4
Change-Id: I0271caf8decd46fd9dedf31158d62a86e1e0028c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79856
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
---
 .../Classes/Middleware/PreviewSimulator.php   | 47 ++++++++++++++++++-
 1 file changed, 45 insertions(+), 2 deletions(-)

diff --git a/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php b/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php
index 60300bb69bf4..88242448dbb2 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PreviewSimulator.php
@@ -21,6 +21,7 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\DateTimeAspect;
 use TYPO3\CMS\Core\Context\LanguageAspectFactory;
@@ -28,6 +29,7 @@ use TYPO3\CMS\Core\Context\VisibilityAspect;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
 use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\RootlineUtility;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
@@ -66,11 +68,12 @@ class PreviewSimulator implements MiddlewareInterface
             }
             // The preview flag is set if the current page turns out to be hidden
             $showHiddenPages = $this->checkIfPageIsHidden($pageArguments->getPageId(), $request);
+            $rootlineRequiresPreviewFlag = $this->checkIfRootlineRequiresPreview($pageArguments->getPageId());
             $simulatingDate = $this->simulateDate($request);
             $simulatingGroup = $this->simulateUserGroup($request);
             $showHiddenRecords = $visibilityAspect->includeHidden();
             $isOfflineWorkspace = $this->context->getPropertyFromAspect('workspace', 'id', 0) > 0;
-            $isPreview = $simulatingDate || $simulatingGroup || $showHiddenRecords || $showHiddenPages || $isOfflineWorkspace;
+            $isPreview = $simulatingDate || $simulatingGroup || $showHiddenRecords || $showHiddenPages || $isOfflineWorkspace || $rootlineRequiresPreviewFlag;
             if ($this->context->hasAspect('frontend.preview')) {
                 $previewAspect = $this->context->getAspect('frontend.preview');
                 $isPreview = $previewAspect->isPreview() || $isPreview;
@@ -78,7 +81,7 @@ class PreviewSimulator implements MiddlewareInterface
             $previewAspect = GeneralUtility::makeInstance(PreviewAspect::class, $isPreview);
             $this->context->setAspect('frontend.preview', $previewAspect);
 
-            if ($showHiddenPages) {
+            if ($showHiddenPages || $rootlineRequiresPreviewFlag) {
                 $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $visibilityAspect->includeHiddenContent(), $visibilityAspect->includeDeletedRecords());
                 $this->context->setAspect('visibility', $newAspect);
             }
@@ -87,6 +90,46 @@ class PreviewSimulator implements MiddlewareInterface
         return $handler->handle($request);
     }
 
+    protected function checkIfRootlineRequiresPreview(int $pageId): bool
+    {
+        $rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, '', $this->context);
+        $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $this->context);
+        $groupRestricted = false;
+        $timeRestricted = false;
+        $hidden = false;
+        try {
+            $rootLine = $rootlineUtility->get();
+            $pageInfo = $pageRepository->getPage_noCheck($pageId);
+            // Only check rootline if the current page has not set extendToSubpages itself
+            // @see \TYPO3\CMS\Backend\Routing\PreviewUriBuilder::class
+            if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
+                // remove the current page from the rootline
+                array_shift($rootLine);
+                foreach ($rootLine as $page) {
+                    // Skip root node and pages which do not define extendToSubpages
+                    if ((int)($page['uid'] ?? 0) === 0 || !(bool)($page['extendToSubpages'] ?? false)) {
+                        continue;
+                    }
+                    $groupRestricted = (bool)(string)($page['fe_group'] ?? '');
+                    $timeRestricted = (int)($page['starttime'] ?? 0) || (int)($page['endtime'] ?? 0);
+                    $hidden = (int)($page['hidden'] ?? 0);
+                    // Stop as soon as a page in the rootline has extendToSubpages set
+                    break;
+                }
+            }
+
+        } catch (\Exception) {
+            // if the rootline cannot be resolved (404 because of delete placeholder in workspaces for example)
+            // we do not want to fail here but rather continue handling the request to trigger the TSFE 404 handling
+        } finally {
+            // clear the rootline cache to ensure it's cleanly built with the full context later on.
+            $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
+            $cacheManager->getCache('runtime')->flushByTag(RootlineUtility::RUNTIME_CACHE_TAG);
+            $cacheManager->getCache('rootline')->flush();
+        }
+        return $groupRestricted || $timeRestricted || $hidden;
+    }
+
     /**
      * Checks if the page is hidden in the active workspace + language setup.
      */
-- 
GitLab