--- /dev/null
+{if $title !== ''}
+ {capture assign='pageTitle'}{$title}{/capture}
+ {capture assign='contentTitle'}{$title}{/capture}
+{else}
+ {capture assign='pageTitle'}{lang}wcf.global.error.title{/lang}{/capture}
+ {capture assign='contentTitle'}{lang}wcf.global.error.title{/lang}{/capture}
+{/if}
+
+{include file='header' __disableAds=true}
+
+{if ENABLE_DEBUG_MODE}
+{if $exception !== null}
+<!--
+{* A comment may not contain double dashes. *}
+{@'--'|str_replace:'- -':$exception}
+-->
+{/if}
+{/if}
+
+<div class="section">
+ <div class="box64 userException">
+ {icon size=64 name='circle-exclamation'}
+ <p>
+ {@$message}
+ </p>
+ </div>
+</div>
+
+{if $showLogin}
+<section class="section">
+ <h2 class="sectionTitle">{lang}wcf.user.login{/lang}</h2>
+
+ <p>{lang}wcf.page.error.loginAvailable{/lang}</p>
+ <p style="margin-top: 20px">
+ <a
+ href="{link controller='Login' url=$__wcf->getRequestURI()}{/link}"
+ class="button"
+ rel="nofollow"
+ >{icon name='key'} {lang}wcf.user.loginOrRegister{/lang}</a>
+ </p>
+</section>
+{/if}
+
+{include file='footer' __disableAds=true}
--- /dev/null
+{include file='header' pageTitle=$title templateName='error' templateNameApplication='wcf'}
+
+{if ENABLE_DEBUG_MODE}
+{if $exception !== null}
+<!--
+{* A comment may not contain double dashes. *}
+{@'--'|str_replace:'- -':$exception}
+-->
+{/if}
+{/if}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{$title}</h1>
+ </div>
+</header>
+
+<div class="section">
+ <div class="box64 userException">
+ {icon size=64 name='circle-exclamation'}
+ <p>
+ {@$message}
+ </p>
+ </div>
+</div>
+
+{include file='footer'}
--- /dev/null
+<?php
+
+namespace wcf\http\error;
+
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Stores additional metadata for response generation for erroneous requests.
+ *
+ * @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 ErrorDetail
+{
+ private const ATTRIBUTE = self::class;
+
+ private function __construct(
+ private ?string $message = null,
+ private ?\Throwable $throwable = null,
+ ) {
+ }
+
+ public function getMessage(): ?string
+ {
+ return $this->message;
+ }
+
+ public function getThrowable(): ?\Throwable
+ {
+ return $this->throwable;
+ }
+
+ /**
+ * Creates a new ErrorDetail object with the Throwable's message
+ * as the message.
+ */
+ public static function fromThrowable(\Throwable $e): self
+ {
+ return self::fromMessageWithThrowable($e->getMessage(), $e);
+ }
+
+ /**
+ * Creates a new ErrorDetail object with the given message.
+ */
+ public static function fromMessage(string $message): self
+ {
+ return new self(
+ $message,
+ );
+ }
+
+ /**
+ * Creates a new ErrorDetail object with the given message and the
+ * Throwable as context.
+ */
+ public static function fromMessageWithThrowable(string $message, \Throwable $e): self
+ {
+ return new self(
+ $message,
+ $e
+ );
+ }
+
+ public function attachToRequest(ServerRequestInterface $request): ServerRequestInterface
+ {
+ return $request->withAttribute(self::ATTRIBUTE, $this);
+ }
+
+ public static function fromRequest(ServerRequestInterface $request): ?self
+ {
+ return $request->getAttribute(self::ATTRIBUTE);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\http\error;
+
+use Psr\Http\Message\StreamInterface;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Renders an nice HTML error page.
+ *
+ * @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 HtmlErrorRenderer
+{
+ public function render(
+ string $title,
+ string $message,
+ ?\Throwable $exception = null,
+ bool $showLogin = false
+ ): StreamInterface {
+ return $this->renderHtmlMessage(
+ $title,
+ StringUtil::encodeHTML($message),
+ $exception,
+ $showLogin
+ );
+ }
+
+ public function renderHtmlMessage(
+ string $title,
+ string $message,
+ ?\Throwable $exception = null,
+ bool $showLogin = false
+ ): StreamInterface {
+ return HeaderUtil::parseOutputStream(WCF::getTPL()->fetchStream(
+ 'error',
+ 'wcf',
+ [
+ 'title' => $title,
+ 'message' => $message,
+ 'exception' => $exception,
+ 'showLogin' => $showLogin,
+ 'templateName' => 'error',
+ 'templateNameApplication' => 'wcf',
+ ]
+ ));
+ }
+}
--- /dev/null
+<?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\request\RequestHandler;
+use wcf\system\session\SessionHandler;
+use wcf\system\WCF;
+
+/**
+ * Returns a "Not Found" 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 NotFoundHandler implements RequestHandlerInterface
+{
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $errorDetail = ErrorDetail::fromRequest($request);
+ $message = $errorDetail?->getMessage() ?? WCF::getLanguage()->getDynamicVariable('wcf.page.error.illegalLink');
+
+ if (!RequestHandler::getInstance()->isACPRequest()) {
+ BoxHandler::disablePageLayout();
+ }
+ SessionHandler::getInstance()->disableTracking();
+
+ $preferredType = Helper::getPreferredContentType($request, [
+ 'text/html',
+ 'application/json',
+ ]);
+
+ return match ($preferredType) {
+ 'application/json' => new JsonResponse(
+ [
+ 'message' => $message,
+ ],
+ 404,
+ [],
+ \JSON_PRETTY_PRINT
+ ),
+ 'text/html' => new HtmlResponse(
+ (new HtmlErrorRenderer())->render(
+ WCF::getLanguage()->getDynamicVariable('wcf.page.error.illegalLink.title'),
+ $message,
+ $errorDetail?->getThrowable()
+ ),
+ 404
+ ),
+ };
+ }
+}
--- /dev/null
+<?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\request\RequestHandler;
+use wcf\system\session\SessionHandler;
+use wcf\system\WCF;
+
+/**
+ * Returns a "Permission Denied" 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 PermissionDeniedHandler implements RequestHandlerInterface
+{
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $errorDetail = ErrorDetail::fromRequest($request);
+ $message = $errorDetail?->getMessage() ?? WCF::getLanguage()->getDynamicVariable('wcf.page.error.permissionDenied');
+
+ if (!RequestHandler::getInstance()->isACPRequest()) {
+ BoxHandler::disablePageLayout();
+ NoticeHandler::disableNotices();
+ }
+ SessionHandler::getInstance()->disableTracking();
+
+ $preferredType = Helper::getPreferredContentType($request, [
+ 'text/html',
+ 'application/json',
+ ]);
+
+ return match ($preferredType) {
+ 'application/json' => new JsonResponse(
+ [
+ 'message' => $message,
+ ],
+ 403,
+ [],
+ \JSON_PRETTY_PRINT
+ ),
+ 'text/html' => new HtmlResponse(
+ (new HtmlErrorRenderer())->render(
+ WCF::getLanguage()->getDynamicVariable('wcf.page.error.permissionDenied.title'),
+ $message,
+ $errorDetail?->getThrowable(),
+ !WCF::getUser()->userID,
+ ),
+ 403
+ ),
+ };
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\http\middleware;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\http\error\ErrorDetail;
+use wcf\http\error\NotFoundHandler;
+use wcf\http\error\PermissionDeniedHandler;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * Catches PermissionDeniedException and IllegalLinkException and delegates
+ * to appropriate handlers.
+ *
+ * @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 HandleExceptions implements MiddlewareInterface
+{
+ /**
+ * @inheritDoc
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ try {
+ return $handler->handle($request);
+ } catch (PermissionDeniedException|IllegalLinkException $e) {
+ if ($e instanceof PermissionDeniedException) {
+ $handler = new PermissionDeniedHandler();
+ } elseif ($e instanceof IllegalLinkException) {
+ $handler = new NotFoundHandler();
+ } else {
+ throw new \LogicException('Unreachable');
+ }
+
+ return $handler->handle(ErrorDetail::fromThrowable($e)->attachToRequest($request));
+ }
+ }
+}
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
+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.
\JSON_PRETTY_PRINT
),
'text/html' => new HtmlResponse(
- // TODO: Create a more generically reusable template for this type of error message.
- WCF::getTPL()->fetchStream(
- 'userException',
- 'wcf',
- [
- 'name' => $e::class,
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'message' => $message,
- 'stacktrace' => $e->getTraceAsString(),
- 'templateName' => 'userException',
- 'templateNameApplication' => 'wcf',
- 'exceptionClassName' => $e::class,
- ]
+ (new HtmlErrorRenderer())->render(
+ WCF::getLanguage()->getDynamicVariable('wcf.global.error.title'),
+ $message,
+ $e
),
400
),
use wcf\http\middleware\EnforceCacheControlPrivate;
use wcf\http\middleware\EnforceFrameOptions;
use wcf\http\middleware\EnforceNoCacheForTemporaryRedirects;
+use wcf\http\middleware\HandleExceptions;
use wcf\http\middleware\HandleStartupErrors;
use wcf\http\middleware\HandleValinorMappingErrors;
use wcf\http\middleware\JsonBody;
new CheckForOfflineMode(),
new JsonBody(),
new TriggerBackgroundQueue(),
+ new HandleExceptions(),
new HandleValinorMappingErrors(),
]);