Replace the use of Exceptions in middlewares
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 13 Apr 2023 09:29:09 +0000 (11:29 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 13 Apr 2023 12:20:16 +0000 (14:20 +0200)
This is problematic, because Exceptions will skip the response direction of the
middleware stack. Instead appropriate responses are either generated directly
or forwarded to an appropriate request handler.

wcfsetup/install/files/lib/http/error/OfflineHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/http/middleware/CheckForEnterpriseNonOwnerAccess.class.php
wcfsetup/install/files/lib/http/middleware/CheckForExpiredAppEvaluation.class.php
wcfsetup/install/files/lib/http/middleware/CheckForOfflineMode.class.php
wcfsetup/install/files/lib/http/middleware/CheckSystemEnvironment.class.php
wcfsetup/install/files/lib/http/middleware/CheckUserBan.class.php
wcfsetup/install/files/lib/http/middleware/EnforceAcpAuthentication.class.php
wcfsetup/install/files/lib/http/middleware/HandleValinorMappingErrors.class.php

diff --git a/wcfsetup/install/files/lib/http/error/OfflineHandler.class.php b/wcfsetup/install/files/lib/http/error/OfflineHandler.class.php
new file mode 100644 (file)
index 0000000..f397ee7
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+namespace wcf\http\error;
+
+use Laminas\Diactoros\Response\HtmlResponse;
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\http\Helper;
+use wcf\system\box\BoxHandler;
+use wcf\system\notice\NoticeHandler;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+
+/**
+ * Returns an "Offline" response.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2023 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+final class OfflineHandler implements RequestHandlerInterface
+{
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        BoxHandler::disablePageLayout();
+        NoticeHandler::disableNotices();
+
+        $preferredType = Helper::getPreferredContentType($request, [
+            'text/html',
+            'application/json',
+        ]);
+
+        return HeaderUtil::withNoCacheHeaders(match ($preferredType) {
+            'application/json' => new JsonResponse(
+                [
+                    'message' => WCF::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'),
+                ],
+                503,
+                [],
+                \JSON_PRETTY_PRINT
+            ),
+            'text/html' => new HtmlResponse(
+                HeaderUtil::parseOutputStream(WCF::getTPL()->fetchStream(
+                    'offline',
+                    'wcf',
+                    [
+                        'templateName' => 'offline',
+                        'templateNameApplication' => 'wcf',
+                    ]
+                )),
+                503
+            ),
+        });
+    }
+}
index 241d7bbed40f3ec44619cb2b2d38f1f70bbce257..64b1318f051bfce218ea3a8cb080a5f1b0050d7a 100644 (file)
@@ -6,7 +6,7 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use wcf\system\exception\IllegalLinkException;
+use wcf\http\error\NotFoundHandler;
 use wcf\system\request\RequestHandler;
 use wcf\system\WCF;
 
@@ -34,7 +34,7 @@ final class CheckForEnterpriseNonOwnerAccess implements MiddlewareInterface
             && \constant($requestHandler->getActiveRequest()->getClassName() . '::BLACKLISTED_IN_ENTERPRISE_MODE')
             && !WCF::getUser()->hasOwnerAccess()
         ) {
-            throw new IllegalLinkException();
+            return (new NotFoundHandler())->handle($request);
         }
 
         return $handler->handle($request);
index 5e4576fe467fc4225ca233af50cfafc6d650c76a..3fe9847d7b481965572e1bb68a23baae48f05b15 100644 (file)
@@ -2,12 +2,13 @@
 
 namespace wcf\http\middleware;
 
+use Laminas\Diactoros\Response\HtmlResponse;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use wcf\http\error\HtmlErrorRenderer;
 use wcf\system\application\ApplicationHandler;
-use wcf\system\exception\NamedUserException;
 use wcf\system\request\RequestHandler;
 use wcf\system\WCF;
 
@@ -42,14 +43,20 @@ final class CheckForExpiredAppEvaluation implements MiddlewareInterface
                     $isWoltLab = true;
                 }
 
-                throw new NamedUserException(WCF::getLanguage()->getDynamicVariable(
-                    'wcf.acp.package.evaluation.expired',
-                    [
-                        'packageName' => $package->getName(),
-                        'pluginStoreFileID' => $pluginStoreFileID,
-                        'isWoltLab' => $isWoltLab,
-                    ]
-                ));
+                return new HtmlResponse(
+                    (new HtmlErrorRenderer())->renderHtmlMessage(
+                        WCF::getLanguage()->getDynamicVariable('wcf.global.error.title'),
+                        WCF::getLanguage()->getDynamicVariable(
+                            'wcf.acp.package.evaluation.expired',
+                            [
+                                'packageName' => $package->getName(),
+                                'pluginStoreFileID' => $pluginStoreFileID,
+                                'isWoltLab' => $isWoltLab,
+                            ]
+                        ),
+                    ),
+                    503
+                );
             }
         }
 
index 9da4359351545fcac86e46a97608217df9524e83..305ed5dc85169c6f6e690117a119426f8f957984 100644 (file)
@@ -2,18 +2,13 @@
 
 namespace wcf\http\middleware;
 
-use Laminas\Diactoros\Response\HtmlResponse;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use wcf\http\Helper;
-use wcf\system\box\BoxHandler;
-use wcf\system\exception\AJAXException;
-use wcf\system\notice\NoticeHandler;
+use wcf\http\error\OfflineHandler;
 use wcf\system\request\RequestHandler;
 use wcf\system\WCF;
-use wcf\util\HeaderUtil;
 
 /**
  * Checks whether the offline mode is enabled and the request must be intercepted.
@@ -42,32 +37,7 @@ final class CheckForOfflineMode implements MiddlewareInterface
             return $handler->handle($request);
         }
 
-        return HeaderUtil::withNoCacheHeaders($this->getOfflineResponse($request));
-    }
-
-    private function getOfflineResponse(ServerRequestInterface $request): ResponseInterface
-    {
-        if (Helper::isAjaxRequest($request)) {
-            throw new AJAXException(
-                WCF::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'),
-                AJAXException::INSUFFICIENT_PERMISSIONS
-            );
-        } else {
-            BoxHandler::disablePageLayout();
-            NoticeHandler::disableNotices();
-
-            return new HtmlResponse(
-                HeaderUtil::parseOutputStream(WCF::getTPL()->fetchStream(
-                    'offline',
-                    'wcf',
-                    [
-                        'templateName' => 'offline',
-                        'templateNameApplication' => 'wcf',
-                    ]
-                )),
-                503
-            );
-        }
+        return (new OfflineHandler())->handle($request);
     }
 
     private function offlineModeEnabled(): bool
index 2fd847389a0bd16cf2f0d18f4e502b2852e1793e..525b810791a797c2bb2266243b4deba6929db209 100644 (file)
@@ -2,11 +2,12 @@
 
 namespace wcf\http\middleware;
 
+use Laminas\Diactoros\Response\HtmlResponse;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use wcf\system\exception\NamedUserException;
+use wcf\http\error\HtmlErrorRenderer;
 use wcf\system\request\RequestHandler;
 use wcf\system\WCF;
 
@@ -27,9 +28,13 @@ final class CheckSystemEnvironment implements MiddlewareInterface
     {
         if (!RequestHandler::getInstance()->isACPRequest()) {
             if (!(80100 <= \PHP_VERSION_ID && \PHP_VERSION_ID <= 80299)) {
-                \header('HTTP/1.1 500 Internal Server Error');
-
-                throw new NamedUserException(WCF::getLanguage()->get('wcf.global.incompatiblePhpVersion'));
+                return new HtmlResponse(
+                    (new HtmlErrorRenderer())->render(
+                        WCF::getLanguage()->getDynamicVariable('wcf.global.error.title'),
+                        WCF::getLanguage()->get('wcf.global.incompatiblePhpVersion'),
+                    ),
+                    500
+                );
             }
         }
 
index 5a5c3fbdf6928906ccc5515c5dd26365d1a46f31..b5c18ec0382bc50fbd6f4ee42ac2ebb4d419ec0c 100644 (file)
@@ -7,9 +7,9 @@ use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
 use wcf\data\user\User;
+use wcf\http\error\ErrorDetail;
+use wcf\http\error\PermissionDeniedHandler;
 use wcf\http\Helper;
-use wcf\system\exception\AJAXException;
-use wcf\system\exception\NamedUserException;
 use wcf\system\WCF;
 
 /**
@@ -30,18 +30,16 @@ final class CheckUserBan implements MiddlewareInterface
         $user = WCF::getUser();
 
         if ($this->isBanned($user)) {
-            if (Helper::isAjaxRequest($request)) {
-                throw new AJAXException(
-                    WCF::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'),
-                    AJAXException::INSUFFICIENT_PERMISSIONS
-                );
-            } else {
+            if (!Helper::isAjaxRequest($request)) {
                 // Delete sessions only for non-AJAX requests to ensure
                 // that the user was able to see the message properly
                 WCF::getSession()->deleteUserSessionsExcept($user);
-
-                throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'));
             }
+
+            return (new PermissionDeniedHandler())->handle(
+                ErrorDetail::fromMessage(WCF::getLanguage()->getDynamicVariable('wcf.user.error.isBanned'))
+                    ->attachToRequest($request)
+            );
         }
 
         return $handler->handle($request);
index d31435b37b89800dcfc93314c4ea103d7b3589b1..5867f8d4cde6ac59e66679721ec4fea5796c56c5 100644 (file)
@@ -16,8 +16,9 @@ use wcf\action\AJAXInvokeAction;
 use wcf\data\acp\session\access\log\ACPSessionAccessLogEditor;
 use wcf\data\acp\session\log\ACPSessionLog;
 use wcf\data\acp\session\log\ACPSessionLogEditor;
+use wcf\http\error\ErrorDetail;
+use wcf\http\error\PermissionDeniedHandler;
 use wcf\http\Helper;
-use wcf\system\exception\AJAXException;
 use wcf\system\request\LinkHandler;
 use wcf\system\request\RequestHandler;
 use wcf\system\user\multifactor\TMultifactorRequirementEnforcer;
@@ -90,11 +91,7 @@ final class EnforceAcpAuthentication implements MiddlewareInterface
     private function handleGuest(ServerRequestInterface $request): ResponseInterface
     {
         if (Helper::isAjaxRequest($request)) {
-            throw new AJAXException(
-                WCF::getLanguage()->getDynamicVariable('wcf.ajax.error.sessionExpired'),
-                AJAXException::SESSION_EXPIRED,
-                ''
-            );
+            return (new PermissionDeniedHandler())->handle($request);
         }
 
         return new RedirectResponse(
@@ -114,10 +111,7 @@ final class EnforceAcpAuthentication implements MiddlewareInterface
         ]);
 
         if (Helper::isAjaxRequest($request)) {
-            throw new AJAXException(
-                WCF::getLanguage()->getDynamicVariable('wcf.ajax.error.permissionDenied'),
-                AJAXException::INSUFFICIENT_PERMISSIONS
-            );
+            return (new PermissionDeniedHandler())->handle($request);
         }
 
         return new HtmlResponse(
@@ -132,9 +126,9 @@ final class EnforceAcpAuthentication implements MiddlewareInterface
     private function handleReauthentication(ServerRequestInterface $request): ResponseInterface
     {
         if (Helper::isAjaxRequest($request)) {
-            throw new AJAXException(
-                WCF::getLanguage()->getDynamicVariable('wcf.user.reauthentication.explanation'),
-                AJAXException::SESSION_EXPIRED
+            return (new PermissionDeniedHandler())->handle(
+                ErrorDetail::fromMessage(WCF::getLanguage()->getDynamicVariable('wcf.user.reauthentication.explanation'))
+                    ->attachToRequest($request)
             );
         }
 
index 15a63f6e1b958fed41b48e4140c0ce1af4fbaeda..7b6aa61e154b3799d55faf5ac895396fb1475cfe 100644 (file)
@@ -15,7 +15,6 @@ use wcf\http\error\HtmlErrorRenderer;
 use wcf\http\Helper;
 use wcf\system\valinor\formatter\PrependPath;
 use wcf\system\WCF;
-use wcf\util\StringUtil;
 
 /**
  * Catches Valinor's MappingErrors and returns a HTTP 400 Bad Request.