[TASK] Separate Error handling from TSFE 90/55690/20
authorBenni Mack <benni@typo3.org>
Mon, 12 Feb 2018 21:12:33 +0000 (22:12 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Thu, 15 Feb 2018 11:54:30 +0000 (12:54 +0100)
The goal is to decouple error handling from TSFE.
For that, a new ErrorController is added which can be
called and returns a Response object instead of
exits the current request.

In order to keep full backwards-compatibility,
TSFE currently does not return a response, but
instead exists with a response object.

Next step:
- Move the pageUnavailable_force functionality into a middleware
- See what can be further abstracted to return a response

Resolves: #83883
Releases: master
Change-Id: I41f5f1a32a3f7edd48eb5c0464a26762ad4a946f
Reviewed-on: https://review.typo3.org/55690
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/Controller/ErrorController.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php
typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php [new file with mode: 0644]
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst
new file mode 100644 (file)
index 0000000..b994d30
--- /dev/null
@@ -0,0 +1,46 @@
+.. include:: ../../Includes.txt
+
+===================================================================
+Deprecation: #83883 - Page Not Found And Error handling in Frontend
+===================================================================
+
+See :issue:`83883`
+
+Description
+===========
+
+The following methods have been marked as deprecated:
+
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageUnavailableAndExit()`
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageNotFoundAndExit()`
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->checkPageUnavailableHandler()`
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageUnavailableHandler()`
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageNotFoundHandler()`
+* php:`TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageErrorHandler()`
+
+These methods have been commonly used by third-party extensions to show that a page is not found, or
+a page is unavailable due to misconfiguration, or the access to a page was denied.
+
+
+Impact
+======
+
+Calling any of the methods above will trigger a deprecation error.
+
+
+Affected Installations
+======================
+
+Any installation with third-party PHP extension code calling these methods.
+
+
+Migration
+=========
+
+Use the new `ErrorController` with its custom actions `unavailableAction()`, `pageNotFoundAction()` and
+`accessDeniedAction()`.
+
+Instead of exiting the currently running script, a proposed PSR-7 compliant response is returned which can be
+handled by the third-party extension to enrich, return or customly exiting the script.
+
+.. index:: Frontend, PHP-API, FullyScanned
\ No newline at end of file
diff --git a/typo3/sysext/frontend/Classes/Controller/ErrorController.php b/typo3/sysext/frontend/Classes/Controller/ErrorController.php
new file mode 100644 (file)
index 0000000..6d0f1b5
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Controller;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use TYPO3\CMS\Core\Controller\ErrorPageController;
+use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
+use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
+use TYPO3\CMS\Core\Http\HtmlResponse;
+use TYPO3\CMS\Core\Http\RedirectResponse;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Handles "Page Not Found" or "Page Unavailable" requests,
+ * returns a response object.
+ */
+class ErrorController
+{
+    /**
+     * Used for creating a 500 response ("Page unavailable"), usually due some misconfiguration
+     * but if configured, a RedirectResponse could be returned as well.
+     *
+     * @param string $message
+     * @param array $reasons
+     * @return ResponseInterface
+     * @throws ServiceUnavailableException
+     */
+    public function unavailableAction(string $message, array $reasons = []): ResponseInterface
+    {
+        if (!$this->isPageUnavailableHandlerConfigured()) {
+            throw new ServiceUnavailableException($message, 1518472181);
+        }
+        return $this->handlePageError(
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'],
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'],
+            $message,
+            $reasons
+        );
+    }
+
+    /**
+     * Used for creating a 404 response ("Page Not Found"),
+     * but if configured, a RedirectResponse could be returned as well.
+     *
+     * @param string $message
+     * @param array $reasons
+     * @return ResponseInterface
+     * @throws PageNotFoundException
+     */
+    public function pageNotFoundAction(string $message, array $reasons = []): ResponseInterface
+    {
+        if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
+            throw new PageNotFoundException($message, 1518472189);
+        }
+        return $this->handlePageError(
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'],
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'],
+            $message,
+            $reasons
+        );
+    }
+
+    /**
+     * Used for creating a 403 response ("Access denied"),
+     * but if configured, a RedirectResponse could be returned as well.
+     *
+     * @param string $message
+     * @param array $reasons
+     * @return ResponseInterface
+     */
+    public function accessDeniedAction(string $message, array $reasons = []): ResponseInterface
+    {
+        return $this->handlePageError(
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'],
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'],
+            $message,
+            $reasons
+        );
+    }
+
+    /**
+     * Checks whether the pageUnavailableHandler should be used. To be used, pageUnavailable_handling must be set
+     * and devIPMask must not match the current visitor's IP address.
+     *
+     * @return bool TRUE/FALSE whether the pageUnavailable_handler should be used.
+     */
+    protected function isPageUnavailableHandlerConfigured(): bool
+    {
+        return
+            $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling']
+            && !GeneralUtility::cmpIP(
+                GeneralUtility::getIndpEnv('REMOTE_ADDR'),
+                $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
+            )
+        ;
+    }
+
+    /**
+     * Generic error page handler.
+     *
+     * @param mixed $errorHandler See docs of ['FE']['pageNotFound_handling'] and ['FE']['pageUnavailable_handling'] for all possible values
+     * @param string $header If set, this is passed directly to the PHP function, header()
+     * @param string $reason If set, error messages will also mention this as the reason for the page-not-found.
+     * @param array $pageAccessFailureReasons
+     * @return ResponseInterface
+     * @throws \RuntimeException
+     */
+    protected function handlePageError($errorHandler, string $header = '', string $reason = '', array $pageAccessFailureReasons = []): ResponseInterface
+    {
+        $response = null;
+        $content = '';
+        // Simply boolean; Just shows TYPO3 error page with reason:
+        if (gettype($errorHandler) === 'boolean' || strtolower($errorHandler) === 'true' || (string)$errorHandler === '1') {
+            $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
+                'Page Not Found',
+                'The page did not exist or was inaccessible.' . ($reason ? ' Reason: ' . $reason : '')
+            );
+        } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'USER_FUNCTION:')) {
+            $funcRef = trim(substr($errorHandler, 14));
+            $params = [
+                'currentUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
+                'reasonText' => $reason,
+                'pageAccessFailureReasons' => $pageAccessFailureReasons
+            ];
+            try {
+                $content = GeneralUtility::callUserFunction($funcRef, $params, $this);
+            } catch (\Exception $e) {
+                throw new \RuntimeException('Error: 404 page by USER_FUNCTION "' . $funcRef . '" failed.', 1518472235, $e);
+            }
+        } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'READFILE:')) {
+            $readFile = GeneralUtility::getFileAbsFileName(trim(substr($errorHandler, 9)));
+            if (@is_file($readFile)) {
+                $content = str_replace(
+                    [
+                        '###CURRENT_URL###',
+                        '###REASON###'
+                    ],
+                    [
+                        GeneralUtility::getIndpEnv('REQUEST_URI'),
+                        htmlspecialchars($reason)
+                    ],
+                    file_get_contents($readFile)
+                );
+            } else {
+                throw new \RuntimeException('Configuration Error: 404 page "' . $readFile . '" could not be found.', 1518472245);
+            }
+        } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'REDIRECT:')) {
+            $response = new RedirectResponse(substr($errorHandler, 9));
+        } elseif ($errorHandler !== '') {
+            // Check if URL is relative
+            $urlParts = parse_url($errorHandler);
+            // parse_url could return an array without the key "host", the empty check works better than strict check
+            if (empty($urlParts['host'])) {
+                $urlParts['host'] = GeneralUtility::getIndpEnv('HTTP_HOST');
+                if ($errorHandler[0] === '/') {
+                    $errorHandler = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . $errorHandler;
+                } else {
+                    $errorHandler = GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $errorHandler;
+                }
+                $checkBaseTag = false;
+            } else {
+                $checkBaseTag = true;
+            }
+            // Check recursion
+            if ($errorHandler === GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) {
+                $reason = $reason ?: 'Page cannot be found.';
+                $reason .= LF . LF . 'Additionally, ' . $errorHandler . ' was not found while trying to retrieve the error document.';
+                throw new \RuntimeException(nl2br(htmlspecialchars($reason)), 1518472252);
+            }
+            // Prepare headers
+            $requestHeaders = [
+                'User-agent: ' . GeneralUtility::getIndpEnv('HTTP_USER_AGENT'),
+                'Referer: ' . GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')
+            ];
+            $report = [];
+            $res = GeneralUtility::getUrl($errorHandler, 1, $requestHeaders, $report);
+            if ((int)$report['error'] !== 0 && (int)$report['error'] !== 200) {
+                throw new \RuntimeException('Failed to fetch error page "' . $errorHandler . '", reason: ' . $report['message'], 1518472257);
+            }
+            if ($res === false) {
+                // Last chance -- redirect
+                $response = new RedirectResponse($errorHandler);
+            } else {
+                // Header and content are separated by an empty line
+                list($returnedHeaders, $content) = explode(CRLF . CRLF, $res, 2);
+                $content .= CRLF;
+                // Forward these response headers to the client
+                $forwardHeaders = [
+                    'Content-Type:'
+                ];
+                $headerArr = preg_split('/\\r|\\n/', $returnedHeaders, -1, PREG_SPLIT_NO_EMPTY);
+                foreach ($headerArr as $headerLine) {
+                    foreach ($forwardHeaders as $h) {
+                        if (preg_match('/^' . $h . '/', $headerLine)) {
+                            $header .= CRLF . $headerLine;
+                        }
+                    }
+                }
+                // Put <base> if necessary
+                if ($checkBaseTag) {
+                    // If content already has <base> tag, we do not need to do anything
+                    if (false === stristr($content, '<base ')) {
+                        // Generate href for base tag
+                        $base = $urlParts['scheme'] . '://';
+                        if ($urlParts['user'] != '') {
+                            $base .= $urlParts['user'];
+                            if ($urlParts['pass'] != '') {
+                                $base .= ':' . $urlParts['pass'];
+                            }
+                            $base .= '@';
+                        }
+                        $base .= $urlParts['host'];
+                        // Add path portion skipping possible file name
+                        $base .= preg_replace('/(.*\\/)[^\\/]*/', '${1}', $urlParts['path']);
+                        // Put it into content (generate also <head> if necessary)
+                        $replacement = LF . '<base href="' . htmlentities($base) . '" />' . LF;
+                        if (stristr($content, '<head>')) {
+                            $content = preg_replace('/(<head>)/i', '\\1' . $replacement, $content);
+                        } else {
+                            $content = preg_replace('/(<html[^>]*>)/i', '\\1<head>' . $replacement . '</head>', $content);
+                        }
+                    }
+                }
+            }
+        } else {
+            $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
+                'Page Not Found',
+                $reason ? 'Reason: ' . $reason : 'Page cannot be found.'
+            );
+        }
+
+        if (!$response) {
+            $response = new HtmlResponse($content);
+        }
+        return $this->applySanitizedHeadersToResponse($response, $header);
+    }
+
+    /**
+     * Headers which have been requested, will be added to the response object.
+     * If a header is part of the HTTP Repsonse code, the response object will be annotated as well.
+     *
+     * @param ResponseInterface $response
+     * @param string $headers
+     * @return ResponseInterface
+     */
+    protected function applySanitizedHeadersToResponse(ResponseInterface $response, string $headers): ResponseInterface
+    {
+        if (!empty($headers)) {
+            $headerArr = preg_split('/\\r|\\n/', $headers, -1, PREG_SPLIT_NO_EMPTY);
+            foreach ($headerArr as $headerLine) {
+                if (strpos($headerLine, 'HTTP/') === 0 && strpos($headerLine, ':') === false) {
+                    list($protocolVersion, $statusCode, $reasonPhrase) = explode(' ', $headerLine, 3);
+                    list(, $protocolVersion) = explode('/', $protocolVersion, 2);
+                    $response = $response
+                        ->withProtocolVersion((int)$protocolVersion)
+                        ->withStatus($statusCode, $reasonPhrase);
+                } else {
+                    list($headerName, $value) = GeneralUtility::trimExplode(':', $headerLine, 2);
+                    $response = $response->withHeader($headerName, $value);
+                }
+            }
+        }
+        return $response;
+    }
+}
index 9a239ad..fa4b80c 100644 (file)
@@ -847,10 +847,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         } catch (ConnectionException $exception) {
             // Cannot connect to current database
             $message = 'Cannot connect to the configured database "' . $connection->getDatabase() . '"';
-            if ($this->checkPageUnavailableHandler()) {
-                $this->pageUnavailableAndExit($message);
-            } else {
-                $this->logger->emergency($message, ['exception' => $exception]);
+            $this->logger->emergency($message, ['exception' => $exception]);
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message);
+                $this->sendResponseAndExit($response);
+            } catch (ServiceUnavailableException $e) {
                 throw new ServiceUnavailableException($message, 1301648782);
             }
         }
@@ -1310,10 +1311,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                     $this->id = $theFirstPage['uid'];
                 } else {
                     $message = 'No pages are found on the rootlevel!';
-                    if ($this->checkPageUnavailableHandler()) {
-                        $this->pageUnavailableAndExit($message);
-                    } else {
-                        $this->logger->alert($message);
+                    $this->logger->alert($message);
+                    try {
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message);
+                        $this->sendResponseAndExit($response);
+                    } catch (ServiceUnavailableException $e) {
                         throw new ServiceUnavailableException($message, 1301648975);
                     }
                 }
@@ -1325,6 +1327,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->requestedId = $this->id;
         $this->getPageAndRootlineWithDomain($this->domainStartPage);
         $timeTracker->pull();
+        // @todo: in the future, the check if "pageNotFound_handling" is configured should go away, but this breaks
+        // Functional tests in workspaces currently
         if ($this->pageNotFound && $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
             $pNotFoundMsg = [
                 1 => 'ID was not an accessible page',
@@ -1332,11 +1336,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 3 => 'ID was outside the domain',
                 4 => 'The requested page alias does not exist'
             ];
-            $header = '';
+            $message = $pNotFoundMsg[$this->pageNotFound];
             if ($this->pageNotFound === 1 || $this->pageNotFound === 2) {
-                $header = $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'];
+                $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction($message, $this->getPageAccessFailureReasons());
+            } else {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons());
             }
-            $this->pageNotFoundAndExit($pNotFoundMsg[$this->pageNotFound], $header);
+            $this->sendResponseAndExit($response);
         }
         // Init SYS_LASTCHANGED
         $this->register['SYS_LASTCHANGED'] = (int)$this->page['tstamp'];
@@ -1423,10 +1429,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             // If still no page...
             if (empty($this->page)) {
                 $message = 'The requested page does not exist!';
-                if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
-                    $this->pageNotFoundAndExit($message);
-                } else {
-                    $this->logger->error($message);
+                $this->logger->error($message);
+                try {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons());
+                    $this->sendResponseAndExit($response);
+                } catch (PageNotFoundException $e) {
                     throw new PageNotFoundException($message, 1301648780);
                 }
             }
@@ -1434,10 +1441,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         // Spacer is not accessible in frontend
         if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER) {
             $message = 'The requested page does not exist!';
-            if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
-                $this->pageNotFoundAndExit($message);
-            } else {
-                $this->logger->error($message);
+            $this->logger->error($message);
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message, $this->getPageAccessFailureReasons());
+                $this->sendResponseAndExit($response);
+            } catch (PageNotFoundException $e) {
                 throw new PageNotFoundException($message, 1301648781);
             }
         }
@@ -1472,10 +1480,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         // If not rootline we're off...
         if (empty($this->rootLine)) {
             $message = 'The requested page didn\'t have a proper connection to the tree-root!';
-            if ($this->checkPageUnavailableHandler()) {
-                $this->pageUnavailableAndExit($message);
-            } else {
-                $this->logger->error($message);
+            $this->logger->error($message);
+            try {
+                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message, $this->getPageAccessFailureReasons());
+                $this->sendResponseAndExit($response);
+            } catch (ServiceUnavailableException $e) {
                 throw new ServiceUnavailableException($message, 1301648167);
             }
         }
@@ -1483,9 +1492,10 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         if ($this->checkRootlineForIncludeSection()) {
             if (empty($this->rootLine)) {
                 $message = 'The requested page was not accessible!';
-                if ($this->checkPageUnavailableHandler()) {
-                    $this->pageUnavailableAndExit($message);
-                } else {
+                try {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message, $this->getPageAccessFailureReasons());
+                    $this->sendResponseAndExit($response);
+                } catch (ServiceUnavailableException $e) {
                     $this->logger->warning($message);
                     throw new ServiceUnavailableException($message, 1301648234);
                 }
@@ -1871,9 +1881,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      *
      * @param string $reason Reason text
      * @param string $header HTTP header to send
+     * @deprecated
      */
     public function pageUnavailableAndExit($reason = '', $header = '')
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         $header = $header ?: $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'];
         $this->pageUnavailableHandler($GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'], $header, $reason);
         die;
@@ -1884,9 +1896,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      *
      * @param string $reason Reason text
      * @param string $header HTTP header to send
+     * @deprecated
      */
     public function pageNotFoundAndExit($reason = '', $header = '')
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         $header = $header ?: $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'];
         $this->pageNotFoundHandler($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'], $header, $reason);
         die;
@@ -1897,9 +1911,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * and devIPMask must not match the current visitor's IP address.
      *
      * @return bool TRUE/FALSE whether the pageUnavailable_handler should be used.
+     * @deprecated
      */
     public function checkPageUnavailableHandler()
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         if (
             $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling']
             && !GeneralUtility::cmpIP(
@@ -1920,9 +1936,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @param mixed $code See ['FE']['pageUnavailable_handling'] for possible values
      * @param string $header If set, this is passed directly to the PHP function, header()
      * @param string $reason If set, error messages will also mention this as the reason for the page-not-found.
+     * @deprecated
      */
     public function pageUnavailableHandler($code, $header, $reason)
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         $this->pageErrorHandler($code, $header, $reason);
     }
 
@@ -1932,9 +1950,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @param mixed $code See docs of ['FE']['pageNotFound_handling'] for possible values
      * @param string $header If set, this is passed directly to the PHP function, header()
      * @param string $reason If set, error messages will also mention this as the reason for the page-not-found.
+     * @deprecated
      */
     public function pageNotFoundHandler($code, $header = '', $reason = '')
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         $this->pageErrorHandler($code, $header, $reason);
     }
 
@@ -1946,9 +1966,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * @param string $header If set, this is passed directly to the PHP function, header()
      * @param string $reason If set, error messages will also mention this as the reason for the page-not-found.
      * @throws \RuntimeException
+     * @deprecated
      */
     public function pageErrorHandler($code, $header = '', $reason = '')
     {
+        trigger_error('This method will be removed in TYPO3 v10. Use TYPO3\'s ErrorController with Request/Response objects instead.', E_USER_DEPRECATED);
         // Issue header in any case:
         if ($header) {
             $headerArr = preg_split('/\\r|\\n/', $header, -1, PREG_SPLIT_NO_EMPTY);
@@ -2161,7 +2183,8 @@ 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']) {
-                    $this->pageNotFoundAndExit('Request parameters could not be validated (&cHash comparison failed)');
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Request parameters could not be validated (&cHash comparison failed)');
+                    $this->sendResponseAndExit($response);
                 } else {
                     $this->disableCache();
                     $this->getTimeTracker()->setTSlogMessage('The incoming cHash "' . $this->cHash . '" and calculated cHash "' . $cHash_calc . '" did not match, so caching was disabled. The fieldlist used was "' . implode(',', array_keys($this->cHash_array)) . '"', 2);
@@ -2188,7 +2211,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 if ($this->tempContent) {
                     $this->clearPageCacheContent();
                 }
-                $this->pageNotFoundAndExit('Request parameters could not be validated (&cHash empty)');
+                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Request parameters could not be validated (&cHash empty)');
+                $this->sendResponseAndExit($response);
             } else {
                 $this->disableCache();
                 $this->getTimeTracker()->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', 2);
@@ -2451,11 +2475,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 $this->pSetup = $this->tmpl->setup[$this->sPre . '.'];
                 if (!is_array($this->pSetup)) {
                     $message = 'The page is not configured! [type=' . $this->type . '][' . $this->sPre . '].';
-                    if ($this->checkPageUnavailableHandler()) {
-                        $this->pageUnavailableAndExit($message);
-                    } else {
+                    $this->logger->alert($message);
+                    try {
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message);
+                        $this->sendResponseAndExit($response);
+                    } catch (ServiceUnavailableException $e) {
                         $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
-                        $this->logger->alert($message);
                         throw new ServiceUnavailableException($message . ' ' . $explanation, 1294587217);
                     }
                 } else {
@@ -2498,11 +2523,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 }
                 $timeTracker->pull();
             } else {
-                if ($this->checkPageUnavailableHandler()) {
-                    $this->pageUnavailableAndExit('No TypoScript template found!');
-                } else {
-                    $message = 'No TypoScript template found!';
-                    $this->logger->alert($message);
+                $message = 'No TypoScript template found!';
+                $this->logger->alert($message);
+                try {
+                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction($message);
+                    $this->sendResponseAndExit($response);
+                } catch (ServiceUnavailableException $e) {
                     throw new ServiceUnavailableException($message, 1294587218);
                 }
             }
@@ -2574,11 +2600,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface
                 if ($this->sys_language_uid) {
                     // If requested translation is not available:
                     if (GeneralUtility::hideIfNotTranslated($this->page['l18n_cfg'])) {
-                        $this->pageNotFoundAndExit('Page is not available in the requested language.');
+                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Page is not available in the requested language.');
+                        $this->sendResponseAndExit($response);
                     } else {
                         switch ((string)$this->sys_language_mode) {
                             case 'strict':
-                                $this->pageNotFoundAndExit('Page is not available in the requested language (strict).');
+                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Page is not available in the requested language (strict).');
+                                $this->sendResponseAndExit($response);
                                 break;
                             case 'content_fallback':
                                 // Setting content uid (but leaving the sys_language_uid) when a content_fallback
@@ -2597,7 +2625,8 @@ 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.
-                                        $this->pageNotFoundAndExit('Page is not available in the requested language (fallbacks did not apply).');
+                                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction('Page is not available in the requested language (fallbacks did not apply).');
+                                        $this->sendResponseAndExit($response);
                                     }
                                 }
                                 break;
@@ -2621,7 +2650,8 @@ 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);
-            $this->pageNotFoundAndExit($message);
+            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($message);
+            $this->sendResponseAndExit($response);
         }
         $this->updateRootLinesWithTranslations();
 
@@ -4676,6 +4706,30 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
+     * Helper method to kill the request. Exits.
+     * Should not be used from the outside, rather return the response object
+     * Ideally, this method will be dropped by TYPO3 v9 LTS.
+     *
+     * @param ResponseInterface $response
+     */
+    protected function sendResponseAndExit(ResponseInterface $response)
+    {
+        // If the response code was not changed by legacy code (still is 200)
+        // then allow the PSR-7 response object to explicitly set it.
+        // Otherwise let legacy code take precedence.
+        // This code path can be deprecated once we expose the response object to third party code
+        if (http_response_code() === 200) {
+            header('HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
+        }
+
+        foreach ($response->getHeaders() as $name => $values) {
+            header($name . ': ' . implode(', ', $values));
+        }
+        echo $response->getBody()->__toString();
+        die;
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
index 521ba2b..8d8d7ef 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Http\RequestHandlerInterface;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Page\PageGenerator;
 use TYPO3\CMS\Frontend\Utility\CompressionUtility;
@@ -107,7 +108,7 @@ class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterf
                 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
             )
         ) {
-            $this->controller->pageUnavailableAndExit('This page is temporarily unavailable.');
+            return GeneralUtility::makeInstance(ErrorController::class)->unavailableAction('This page is temporarily unavailable.');
         }
 
         $this->controller->connectToDB();
diff --git a/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php b/typo3/sysext/frontend/Tests/Unit/Controller/ErrorControllerTest.php
new file mode 100644 (file)
index 0000000..71ee6fd
--- /dev/null
@@ -0,0 +1,406 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Frontend\Tests\Unit\Controller;
+
+/*
+ * 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\Core\Http\HtmlResponse;
+use TYPO3\CMS\Core\Http\RedirectResponse;
+use TYPO3\CMS\Frontend\Controller\ErrorController;
+
+/**
+ * Testcase for \TYPO3\CMS\Frontend\Controller\ErrorController
+ */
+class ErrorControllerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+
+    /**
+     * Tests concerning pageNotFound handling
+     */
+
+    /**
+     * @test
+     */
+    public function pageNotFoundHandlingThrowsExceptionIfNotConfigured()
+    {
+        $this->expectExceptionMessage('This test page was not found!');
+        $this->expectExceptionCode(1518472189);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = false;
+        $subject = new ErrorController();
+        $subject->pageNotFoundAction('This test page was not found!');
+    }
+
+    /**
+     * Data Provider for 404
+     *
+     * @return array
+     */
+    public function errorPageHandlingDataProvider()
+    {
+        return [
+            '404 with default errorpage' => [
+                'handler' => true,
+                'header' => 'HTTP/1.0 404 Not Found',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'Reason: Custom message',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '404 with default errorpage setting the handler to legacy value' => [
+                'handler' => '1',
+                'header' => 'HTTP/1.0 404 This is a dead end',
+                'message' => 'Come back tomorrow',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'This is a dead end',
+                    'content' => 'Reason: Come back tomorrow',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '404 with custom userfunction' => [
+                'handler' => 'USER_FUNCTION:' . ErrorControllerTest::class . '->mockedUserFunctionCall',
+                'header' => 'HTTP/1.0 404 Not Found',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'It\'s magic, Michael: Custom message',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '404 with a readfile functionality' => [
+                'handler' => 'READFILE:LICENSE.txt',
+                'header' => 'HTTP/1.0 404 Not Found',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'GNU GENERAL PUBLIC LICENSE',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '404 with a readfile functionality with an invalid file' => [
+                'handler' => 'READFILE:does_not_exist.php6',
+                'header' => 'HTTP/1.0 404 Not Found',
+                'message' => 'Custom message',
+                'response' => null,
+                'exceptionCode' => 1518472245,
+            ],
+            '404 with a redirect - never do that in production - it is bad for SEO. But with custom headers as well...' => [
+                'handler' => 'REDIRECT:www.typo3.org',
+                'header' => 'HTTP/1.0 404 Not Found
+X-TYPO3-Additional-Header: Banana Stand',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => RedirectResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'Not Found',
+                    'headers' => [
+                        'location' => ['www.typo3.org'],
+                        'X-TYPO3-Additional-Header' => ['Banana Stand'],
+                    ]
+                ]
+            ],
+            'Custom path, no prefix' => [
+                'handler' => '/404/',
+                'header' => 'HTTP/1.0 404 Not Found
+X-TYPO3-Additional-Header: Banana Stand',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => RedirectResponse::class,
+                    'statusCode' => 404,
+                    'reasonPhrase' => 'Not Found',
+                    'headers' => [
+                        'location' => ['https://localhost/404/'],
+                        'X-TYPO3-Additional-Header' => ['Banana Stand'],
+                    ]
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider errorPageHandlingDataProvider
+     */
+    public function pageNotFoundHandlingReturnsConfiguredResponseObject($handler, $header, $message, $expectedResponseDetails, $expectedExceptionCode = null)
+    {
+        if ($expectedExceptionCode !== null) {
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = $handler;
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'] = $header;
+        // faking getIndpEnv() variables
+        $_SERVER['REQUEST_URI'] = '/unit-test/';
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $_SERVER['SSL_SESSION_ID'] = true;
+        $subject = new ErrorController();
+        $response = $subject->pageNotFoundAction($message);
+        if (is_array($expectedResponseDetails)) {
+            $this->assertInstanceOf($expectedResponseDetails['type'], $response);
+            $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
+            $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
+            if (isset($expectedResponseDetails['content'])) {
+                $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
+            }
+            $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
+        }
+    }
+
+    /**
+     * Tests concerning accessDenied handling
+     */
+
+    /**
+     * Data Provider for 403
+     *
+     * @return array
+     */
+    public function accessDeniedDataProvider()
+    {
+        return [
+            '403 with default errorpage' => [
+                'handler' => true,
+                'header' => 'HTTP/1.0 403 Who are you',
+                'message' => 'Be nice, do good',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 403,
+                    'reasonPhrase' => 'Who are you',
+                    'content' => 'Reason: Be nice, do good',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider accessDeniedDataProvider
+     */
+    public function accessDeniedReturnsProperHeaders($handler, $header, $message, $expectedResponseDetails)
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'] = $handler;
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'] = $header;
+        // faking getIndpEnv() variables
+        $_SERVER['REQUEST_URI'] = '/unit-test/';
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $_SERVER['SSL_SESSION_ID'] = true;
+        $subject = new ErrorController();
+        $response = $subject->accessDeniedAction($message);
+        if (is_array($expectedResponseDetails)) {
+            $this->assertInstanceOf($expectedResponseDetails['type'], $response);
+            $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
+            $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
+            if (isset($expectedResponseDetails['content'])) {
+                $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
+            }
+            $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
+        }
+    }
+
+    /**
+     * Tests concerning unavailable handling
+     */
+
+    /**
+     * @test
+     */
+    public function unavailableHandlingThrowsExceptionIfNotConfigured()
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = true;
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+        $this->expectExceptionMessage('All your system are belong to us!');
+        $this->expectExceptionCode(1518472181);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = false;
+        $subject = new ErrorController();
+        $subject->unavailableAction('All your system are belong to us!');
+    }
+
+    /**
+     * @test
+     */
+    public function unavailableHandlingDoesNotTriggerDueToDevIpMask()
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = true;
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+
+        $this->expectExceptionMessage('All your system are belong to us!');
+        $this->expectExceptionCode(1518472181);
+        $subject = new ErrorController();
+        $subject->unavailableAction('All your system are belong to us!');
+    }
+    /**
+     * Data Provider for 503
+     *
+     * @return array
+     */
+    public function unavailableHandlingDataProvider()
+    {
+        return [
+            '503 with default errorpage' => [
+                'handler' => true,
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'Reason: Custom message',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '503 with default errorpage setting the handler to legacy value' => [
+                'handler' => '1',
+                'header' => 'HTTP/1.0 503 This is a dead end',
+                'message' => 'Come back tomorrow',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'This is a dead end',
+                    'content' => 'Reason: Come back tomorrow',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '503 with custom userfunction' => [
+                'handler' => 'USER_FUNCTION:' . ErrorControllerTest::class . '->mockedUserFunctionCall',
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'It\'s magic, Michael: Custom message',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '503 with a readfile functionality' => [
+                'handler' => 'READFILE:LICENSE.txt',
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => HtmlResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'Not Found',
+                    'content' => 'GNU GENERAL PUBLIC LICENSE',
+                    'headers' => [
+                        'Content-Type' => ['text/html; charset=utf-8']
+                    ]
+                ]
+            ],
+            '503 with a readfile functionality with an invalid file' => [
+                'handler' => 'READFILE:does_not_exist.php6',
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable',
+                'message' => 'Custom message',
+                'response' => null,
+                'exceptionCode' => 1518472245,
+            ],
+            '503 with a redirect - never do that in production - it is bad for SEO. But with custom headers as well...' => [
+                'handler' => 'REDIRECT:www.typo3.org',
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable
+X-TYPO3-Additional-Header: Banana Stand',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => RedirectResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'Not Found',
+                    'headers' => [
+                        'location' => ['www.typo3.org'],
+                        'X-TYPO3-Additional-Header' => ['Banana Stand'],
+                    ]
+                ]
+            ],
+            'Custom path, no prefix' => [
+                'handler' => '/fail/',
+                'header' => 'HTTP/1.0 503 Service Temporarily Unavailable
+X-TYPO3-Additional-Header: Banana Stand',
+                'message' => 'Custom message',
+                'response' => [
+                    'type' => RedirectResponse::class,
+                    'statusCode' => 503,
+                    'reasonPhrase' => 'Not Found',
+                    'headers' => [
+                        'location' => ['https://localhost/fail/'],
+                        'X-TYPO3-Additional-Header' => ['Banana Stand'],
+                    ]
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider errorPageHandlingDataProvider
+     */
+    public function pageUnavailableHandlingReturnsConfiguredResponseObject($handler, $header, $message, $expectedResponseDetails, $expectedExceptionCode = null)
+    {
+        if ($expectedExceptionCode !== null) {
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '-1';
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'] = $handler;
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'] = $header;
+        // faking getIndpEnv() variables
+        $_SERVER['REQUEST_URI'] = '/unit-test/';
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $_SERVER['SSL_SESSION_ID'] = true;
+        $subject = new ErrorController();
+        $response = $subject->unavailableAction($message);
+        if (is_array($expectedResponseDetails)) {
+            $this->assertInstanceOf($expectedResponseDetails['type'], $response);
+            $this->assertEquals($expectedResponseDetails['statusCode'], $response->getStatusCode());
+            $this->assertEquals($expectedResponseDetails['reasonPhrase'], $response->getReasonPhrase());
+            if (isset($expectedResponseDetails['content'])) {
+                $this->assertContains($expectedResponseDetails['content'], $response->getBody()->getContents());
+            }
+            $this->assertEquals($expectedResponseDetails['headers'], $response->getHeaders());
+        }
+    }
+
+    /**
+     * Callback function when testing "USER_FUNCTION:" prefix
+     */
+    public function mockedUserFunctionCall($params)
+    {
+        return '<p>It\'s magic, Michael: ' . $params['reasonText'] . '</p>';
+    }
+}
index 06eb3e7..d84c210 100644 (file)
@@ -1549,4 +1549,46 @@ return [
             'Deprecation-83252-Link-tagSyntaxProcesssing.rst',
         ],
     ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageUnavailableAndExit' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageNotFoundAndExit' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->checkPageUnavailableHandler' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageUnavailableHandler' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageNotFoundHandler' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->pageErrorHandler' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-83883-PageNotFoundAndErrorHandlingInFrontend.rst',
+        ],
+    ],
 ];