[TASK] Add Error Codes to Page Failure Reasons 01/57201/8
authorBenni Mack <benni@typo3.org>
Tue, 12 Jun 2018 12:23:01 +0000 (14:23 +0200)
committerGeorg Ringer <georg.ringer@gmail.com>
Wed, 27 Jun 2018 17:45:09 +0000 (19:45 +0200)
A new class with constants is added to make the TYPO3 internal errors
why a page cannot be displayed in the frontend more speakable.

Resolves: #17794
Releases: master
Change-Id: I8b9999e95f109666a524ee9157f89f498b21ca6d
Reviewed-on: https://review.typo3.org/57201
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Daniel Sattler <sattler@b13.de>
Tested-by: Daniel Sattler <sattler@b13.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Page/PageAccessFailureReasons.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Unit/Page/PageAccessFailureReasonsTest.php [new file with mode: 0644]

index ea41a84..1f338de 100644 (file)
@@ -63,6 +63,7 @@ use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Http\UrlHandlerInterface;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
+use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
 use TYPO3\CMS\Frontend\Page\PageRepository;
 
 /**
@@ -879,7 +880,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             );
             $this->logger->emergency($message, ['exception' => $exception]);
             try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message);
+                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                    $GLOBALS['TYPO3_REQUEST'],
+                    $message,
+                    ['code' => PageAccessFailureReasons::DATABASE_CONNECTION_FAILED]
+                );
                 $this->sendResponseAndExit($response);
             } catch (ServiceUnavailableException $e) {
                 throw new ServiceUnavailableException($message, 1301648782);
@@ -1337,7 +1342,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $message = 'No pages are found on the rootlevel!';
                     $this->logger->alert($message);
                     try {
-                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message);
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                            $GLOBALS['TYPO3_REQUEST'],
+                            $message,
+                            ['code' => PageAccessFailureReasons::NO_PAGES_FOUND]
+                        );
                         $this->sendResponseAndExit($response);
                     } catch (ServiceUnavailableException $e) {
                         throw new ServiceUnavailableException($message, 1301648975);
@@ -1356,17 +1365,41 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         }
         $timeTracker->pull();
         if ($this->pageNotFound) {
-            $pNotFoundMsg = [
-                1 => 'ID was not an accessible page',
-                2 => 'Subsection was found and not accessible',
-                3 => 'ID was outside the domain',
-                4 => 'The requested page alias does not exist'
-            ];
-            $message = $pNotFoundMsg[$this->pageNotFound];
-            if ($this->pageNotFound === 1 || $this->pageNotFound === 2) {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
-            } else {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
+            switch ($this->pageNotFound) {
+                case 1:
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'ID was not an accessible page',
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
+                    );
+                    break;
+                case 2:
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'Subsection was found and not accessible',
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
+                    );
+                    break;
+                case 3:
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'ID was outside the domain',
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
+                    );
+                    break;
+                case 4:
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'The requested page alias does not exist',
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_ALIAS_NOT_FOUND)
+                    );
+                    break;
+                default:
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'Unspecified error',
+                        $this->getPageAccessFailureReasons()
+                    );
             }
             $this->sendResponseAndExit($response);
         }
@@ -1456,7 +1489,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 $message = 'The requested page does not exist!';
                 $this->logger->error($message);
                 try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        $message,
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
+                    );
                     $this->sendResponseAndExit($response);
                 } catch (PageNotFoundException $e) {
                     throw new PageNotFoundException($message, 1301648780);
@@ -1468,7 +1505,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $message = 'The requested page does not exist!';
             $this->logger->error($message);
             try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                    $GLOBALS['TYPO3_REQUEST'],
+                    $message,
+                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
+                );
                 $this->sendResponseAndExit($response);
             } catch (PageNotFoundException $e) {
                 throw new PageNotFoundException($message, 1301648781);
@@ -1507,7 +1548,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $message = 'The requested page didn\'t have a proper connection to the tree-root!';
             $this->logger->error($message);
             try {
-                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
+                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                    $GLOBALS['TYPO3_REQUEST'],
+                    $message,
+                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
+                );
                 $this->sendResponseAndExit($response);
             } catch (ServiceUnavailableException $e) {
                 throw new ServiceUnavailableException($message, 1301648167);
@@ -1518,7 +1563,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             if (empty($this->rootLine)) {
                 $message = 'The requested page was not accessible!';
                 try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message, $this->getPageAccessFailureReasons());
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        $message,
+                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
+                    );
                     $this->sendResponseAndExit($response);
                 } catch (ServiceUnavailableException $e) {
                     $this->logger->warning($message);
@@ -1750,11 +1799,15 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     /**
      * 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 $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
      * @return array Summary of why page access was not allowed.
      */
-    public function getPageAccessFailureReasons()
+    public function getPageAccessFailureReasons(string $failureReasonCode = null)
     {
         $output = [];
+        if ($failureReasonCode) {
+            $output['code'] = $failureReasonCode;
+        }
         $combinedRecords = array_merge(is_array($this->pageAccessFailureHistory['direct_access']) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]], is_array($this->pageAccessFailureHistory['sub_section']) ? $this->pageAccessFailureHistory['sub_section'] : []);
         if (!empty($combinedRecords)) {
             foreach ($combinedRecords as $k => $pagerec) {
@@ -2128,7 +2181,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $cHash_calc = $this->cacheHash->calculateCacheHash($this->cHash_array);
             if (!hash_equals($cHash_calc, $this->cHash)) {
                 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Request parameters could not be validated (&cHash comparison failed)');
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        'Request parameters could not be validated (&cHash comparison failed)',
+                        ['code' => PageAccessFailureReasons::CACHEHASH_COMPARISON_FAILED]
+                    );
                     $this->sendResponseAndExit($response);
                 } else {
                     $this->disableCache();
@@ -2156,7 +2213,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 if ($this->tempContent) {
                     $this->clearPageCacheContent();
                 }
-                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Request parameters could not be validated (&cHash empty)');
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                    $GLOBALS['TYPO3_REQUEST'],
+                    'Request parameters could not be validated (&cHash empty)',
+                    ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
+                );
                 $this->sendResponseAndExit($response);
             } else {
                 $this->disableCache();
@@ -2428,7 +2489,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $message = 'The page is not configured! [type=' . $this->type . '][' . $this->sPre . '].';
                     $this->logger->alert($message);
                     try {
-                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message);
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                            $GLOBALS['TYPO3_REQUEST'],
+                            $message,
+                            ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
+                        );
                         $this->sendResponseAndExit($response);
                     } catch (ServiceUnavailableException $e) {
                         $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
@@ -2477,7 +2542,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 $message = 'No TypoScript template found!';
                 $this->logger->alert($message);
                 try {
-                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($GLOBALS['TYPO3_REQUEST'], $message);
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
+                        $GLOBALS['TYPO3_REQUEST'],
+                        $message,
+                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
+                    );
                     $this->sendResponseAndExit($response);
                 } catch (ServiceUnavailableException $e) {
                     throw new ServiceUnavailableException($message, 1294587218);
@@ -2581,12 +2650,20 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 if ($this->sys_language_uid) {
                     // If requested translation is not available:
                     if (GeneralUtility::hideIfNotTranslated($this->page['l18n_cfg'])) {
-                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Page is not available in the requested language.');
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                            $GLOBALS['TYPO3_REQUEST'],
+                            'Page is not available in the requested language.',
+                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
+                        );
                         $this->sendResponseAndExit($response);
                     } else {
                         switch ((string)$this->sys_language_mode) {
                             case 'strict':
-                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'Page is not available in the requested language (strict).');
+                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                                    $GLOBALS['TYPO3_REQUEST'],
+                                    'Page is not available in the requested language (strict).',
+                                    ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
+                                );
                                 $this->sendResponseAndExit($response);
                                 break;
                             case 'fallback':
@@ -2606,7 +2683,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                                         // 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($GLOBALS['TYPO3_REQUEST'], 'Page is not available in the requested language (fallbacks did not apply).');
+                                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                                            $GLOBALS['TYPO3_REQUEST'],
+                                            'Page is not available in the requested language (fallbacks did not apply).',
+                                            ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
+                                        );
                                         $this->sendResponseAndExit($response);
                                     }
                                 }
@@ -2631,7 +2712,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         if ((!$this->sys_language_uid || !$this->sys_language_content) && GeneralUtility::hideIfDefaultLanguage($this->page['l18n_cfg'])) {
             $message = 'Page is not available in default language.';
             $this->logger->error($message);
-            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], $message);
+            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                $GLOBALS['TYPO3_REQUEST'],
+                $message,
+                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
+            );
             $this->sendResponseAndExit($response);
         }
         $this->updateRootLinesWithTranslations();
diff --git a/typo3/sysext/frontend/Classes/Page/PageAccessFailureReasons.php b/typo3/sysext/frontend/Classes/Page/PageAccessFailureReasons.php
new file mode 100644 (file)
index 0000000..a7af263
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Page;
+
+/*
+ * 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!
+ */
+
+/**
+ * Contains a list of all reasons that TYPO3 internally uses when a page cannot be found/rendered etc.
+ * See TypoScriptFrontendController for more details and the usage.
+ */
+final class PageAccessFailureReasons
+{
+    // Page resolving issues
+    public const NO_PAGES_FOUND = 'page.database.empty';
+    public const PAGE_NOT_FOUND = 'page';
+    public const PAGE_ALIAS_NOT_FOUND = 'page.alias';
+    public const ROOTLINE_BROKEN = 'page.rootline';
+
+    // Page configuration issues
+    public const RENDERING_INSTRUCTIONS_NOT_FOUND = 'rendering_instructions';
+    public const RENDERING_INSTRUCTIONS_NOT_CONFIGURED = 'rendering_instructions.type';
+
+    // Validation errors
+    public const CACHEHASH_COMPARISON_FAILED = 'cache_hash.comparison';
+    public const CACHEHASH_EMPTY = 'cache_hash.empty';
+
+    // Language-related issues
+    public const LANGUAGE_NOT_AVAILABLE = 'language';
+    public const LANGUAGE_NOT_AVAILABLE_STRICT_MODE = 'language.strict';
+    public const LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE = 'language.fallbacks';
+    public const LANGUAGE_DEFAULT_NOT_AVAILABLE = 'language.default';
+
+    // Access restrictions
+    public const ACCESS_DENIED_GENERAL = 'access';
+    public const ACCESS_DENIED_PAGE_NOT_RESOLVED = 'access.page';
+    public const ACCESS_DENIED_SUBSECTION_NOT_RESOLVED = 'access.subsection';
+    public const ACCESS_DENIED_HOST_PAGE_MISMATCH = 'access.host_mismatch';
+    public const ACCESS_DENIED_INVALID_PAGETYPE = 'access.pagetype';
+
+    // System errors
+    public const DATABASE_CONNECTION_FAILED = 'system.database';
+
+    /**
+     * Labels for the status codes
+     *
+     * @var string[]
+     */
+    protected $messages = [
+        self::NO_PAGES_FOUND => 'No page on rootlevel found',
+        self::PAGE_NOT_FOUND => 'The requested page does not exist',
+        self::PAGE_ALIAS_NOT_FOUND => 'The requested page alias does not exist',
+
+        self::RENDERING_INSTRUCTIONS_NOT_FOUND => 'No TypoScript template found',
+        self::RENDERING_INSTRUCTIONS_NOT_CONFIGURED => 'The page is not configured',
+
+        self::CACHEHASH_COMPARISON_FAILED => 'Request parameters could not be validated (&cHash comparison failed)',
+        self::CACHEHASH_EMPTY => 'Request parameters could not be validated (&cHash empty)',
+
+        self::LANGUAGE_NOT_AVAILABLE => 'Page is not available in the requested language',
+        self::LANGUAGE_NOT_AVAILABLE_STRICT_MODE => 'Page is not available in the requested language (strict)',
+        self::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE => 'Page is not available in the requested language (fallbacks did not apply)',
+        self::LANGUAGE_DEFAULT_NOT_AVAILABLE => 'Page is not available in default language',
+
+        self::ACCESS_DENIED_GENERAL => 'The requested page was not accessible',
+        self::ACCESS_DENIED_PAGE_NOT_RESOLVED => 'ID was not an accessible page',
+        self::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED => 'Subsection was found and not accessible',
+        self::ACCESS_DENIED_HOST_PAGE_MISMATCH => 'ID was outside the domain',
+        self::ACCESS_DENIED_INVALID_PAGETYPE => 'The requested page type cannot be rendered',
+
+        self::DATABASE_CONNECTION_FAILED => 'Database Connection failed',
+        self::ROOTLINE_BROKEN => 'The requested page did not have a proper connection to the tree-root'
+    ];
+
+    /**
+     * @param string $reasonCode a valid reason code (see above)
+     * @return string
+     */
+    public function getMessageForReason(string $reasonCode): string
+    {
+        if (!isset($this->messages[$reasonCode])) {
+            throw new \InvalidArgumentException('No message for page access reason code "' . $reasonCode . '" found.', 1529299833);
+        }
+        return $this->messages[$reasonCode];
+    }
+}
diff --git a/typo3/sysext/frontend/Tests/Unit/Page/PageAccessFailureReasonsTest.php b/typo3/sysext/frontend/Tests/Unit/Page/PageAccessFailureReasonsTest.php
new file mode 100644 (file)
index 0000000..6147de8
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Tests\Unit\Page;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PageAccessFailureReasonsTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function getMessageForReasonReturnsExpectedMessageForCode()
+    {
+        $subject = new PageAccessFailureReasons();
+        $message = $subject->getMessageForReason(PageAccessFailureReasons::NO_PAGES_FOUND);
+        $this->assertEquals('No page on rootlevel found', $message);
+    }
+
+    /**
+     * @test
+     */
+    public function getMessageForReasonThrowsExceptionForWrongCode()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1529299833);
+        $subject = new PageAccessFailureReasons();
+        $subject->getMessageForReason('Unknown Reason');
+    }
+}