diff --git a/src/Foundation/ErrorHandling/Formatter.php b/src/Foundation/ErrorHandling/Formatter.php new file mode 100644 index 000000000..ec7eb4756 --- /dev/null +++ b/src/Foundation/ErrorHandling/Formatter.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +interface Formatter +{ + public function format(HandledError $error, Request $request): Response; +} diff --git a/src/Foundation/ErrorHandling/HandledError.php b/src/Foundation/ErrorHandling/HandledError.php new file mode 100644 index 000000000..738ba6da5 --- /dev/null +++ b/src/Foundation/ErrorHandling/HandledError.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Throwable; + +class HandledError +{ + private $error; + private $type; + private $statusCode; + + private $details = []; + + public static function unknown(Throwable $error) + { + return new static($error, 'unknown', 500); + } + + public function __construct(Throwable $error, $type, $statusCode) + { + $this->error = $error; + $this->type = $type; + $this->statusCode = $statusCode; + } + + public function withDetails(array $details): self + { + $this->details = $details; + + return $this; + } + + public function getError(): Throwable + { + return $this->error; + } + + public function getType(): string + { + return $this->type; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function shouldBeReported(): bool + { + return $this->type === 'unknown'; + } + + public function getDetails(): array + { + return $this->details; + } +} diff --git a/src/Foundation/ErrorHandling/JsonApiRenderer.php b/src/Foundation/ErrorHandling/JsonApiRenderer.php new file mode 100644 index 000000000..948f8ae8f --- /dev/null +++ b/src/Foundation/ErrorHandling/JsonApiRenderer.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Flarum\Api\JsonApiResponse; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Tobscure\JsonApi\Document; + +class JsonApiRenderer implements Formatter +{ + public function format(HandledError $error, Request $request): Response + { + $document = new Document; + $document->setErrors([ + [ + 'status' => (string) $error->getStatusCode(), + 'code' => $error->getType(), + ], + ]); + + return new JsonApiResponse($document, $error->getStatusCode()); + } +} diff --git a/src/Foundation/ErrorHandling/LogReporter.php b/src/Foundation/ErrorHandling/LogReporter.php new file mode 100644 index 000000000..edff300d1 --- /dev/null +++ b/src/Foundation/ErrorHandling/LogReporter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Psr\Log\LoggerInterface; + +class LogReporter implements Reporter +{ + /** + * @var LoggerInterface + */ + protected $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function report(HandledError $error) + { + $this->logger->error($error->getError()); + } +} diff --git a/src/Foundation/ErrorHandling/Registry.php b/src/Foundation/ErrorHandling/Registry.php new file mode 100644 index 000000000..45ed5c4ec --- /dev/null +++ b/src/Foundation/ErrorHandling/Registry.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Flarum\Foundation\KnownError; +use Throwable; + +class Registry +{ + private $statusMap; + private $classMap; + private $handlerMap; + + public function __construct(array $statusMap, array $classMap, array $handlerMap) + { + $this->statusMap = $statusMap; + $this->classMap = $classMap; + $this->handlerMap = $handlerMap; + } + + public function handle(Throwable $error): HandledError + { + return $this->handleKnownTypes($error) + ?? $this->handleCustomTypes($error) + ?? HandledError::unknown($error); + } + + private function handleKnownTypes(Throwable $error): ?HandledError + { + $errorType = null; + + if ($error instanceof KnownError) { + $errorType = $error->getType(); + } else { + $errorClass = get_class($error); + if (isset($this->classMap[$errorClass])) { + $errorType = $this->classMap[$errorClass]; + } + } + + if ($errorType) { + return new HandledError( + $error, + $errorType, + $this->statusMap[$errorType] ?? 500 + ); + } + + return null; + } + + private function handleCustomTypes(Throwable $error): ?HandledError + { + $errorClass = get_class($error); + + if (isset($this->handlerMap[$errorClass])) { + $handler = new $this->handlerMap[$errorClass]; + + return $handler->handle($error); + } + + return null; + } +} diff --git a/src/Foundation/ErrorHandling/Reporter.php b/src/Foundation/ErrorHandling/Reporter.php new file mode 100644 index 000000000..2e52d289b --- /dev/null +++ b/src/Foundation/ErrorHandling/Reporter.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +interface Reporter +{ + public function report(HandledError $error); +} diff --git a/src/Foundation/ErrorHandling/ViewRenderer.php b/src/Foundation/ErrorHandling/ViewRenderer.php new file mode 100644 index 000000000..4ac50ffd7 --- /dev/null +++ b/src/Foundation/ErrorHandling/ViewRenderer.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Contracts\View\Factory as ViewFactory; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Symfony\Component\Translation\TranslatorInterface; +use Zend\Diactoros\Response\HtmlResponse; + +class ViewRenderer implements Formatter +{ + /** + * @var ViewFactory + */ + protected $view; + + /** + * @var TranslatorInterface + */ + protected $translator; + + /** + * @var SettingsRepositoryInterface + */ + protected $settings; + + public function __construct(ViewFactory $view, TranslatorInterface $translator, SettingsRepositoryInterface $settings) + { + $this->view = $view; + $this->translator = $translator; + $this->settings = $settings; + } + + public function format(HandledError $error, Request $request): Response + { + $view = $this->view->make($this->determineView($error)) + ->with('error', $error->getError()) + ->with('message', $this->getMessage($error)); + + return new HtmlResponse($view->render(), $error->getStatusCode()); + } + + private function determineView(HandledError $error): string + { + $view = [ + 'route_not_found' => '404', + 'csrf_token_mismatch' => '419', + ][$error->getType()] ?? 'default'; + + return "flarum.forum::error.$view"; + } + + private function getMessage(HandledError $error) + { + return $this->getTranslationIfExists($error->getStatusCode()) + ?? $this->getTranslationIfExists('unknown') + ?? 'An error occurred while trying to load this page.'; + } + + private function getTranslationIfExists(string $errorType) + { + $key = "core.views.error.${errorType}_message"; + $translation = $this->translator->trans($key, ['{forum}' => $this->settings->get('forum_title')]); + + return $translation === $key ? null : $translation; + } +} diff --git a/src/Foundation/ErrorHandling/WhoopsRenderer.php b/src/Foundation/ErrorHandling/WhoopsRenderer.php new file mode 100644 index 000000000..7be647558 --- /dev/null +++ b/src/Foundation/ErrorHandling/WhoopsRenderer.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation\ErrorHandling; + +use Franzl\Middleware\Whoops\WhoopsRunner; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +class WhoopsRenderer implements Formatter +{ + public function format(HandledError $error, Request $request): Response + { + return WhoopsRunner::handle($error->getError(), $request) + ->withStatus($error->getStatusCode()); + } +} diff --git a/src/Foundation/KnownError.php b/src/Foundation/KnownError.php new file mode 100644 index 000000000..bb7e4591a --- /dev/null +++ b/src/Foundation/KnownError.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Foundation; + +interface KnownError +{ + public function getType(): string; +} diff --git a/src/Http/Middleware/HandleErrors.php b/src/Http/Middleware/HandleErrors.php new file mode 100644 index 000000000..ff077f105 --- /dev/null +++ b/src/Http/Middleware/HandleErrors.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Middleware; + +use Flarum\Foundation\ErrorHandling\Formatter; +use Flarum\Foundation\ErrorHandling\Registry; +use Flarum\Foundation\ErrorHandling\Reporter; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\MiddlewareInterface as Middleware; +use Psr\Http\Server\RequestHandlerInterface as Handler; +use Throwable; + +class HandleErrors implements Middleware +{ + /** + * @var Registry + */ + protected $registry; + + /** + * @var Formatter + */ + protected $formatter; + + /** + * @var Reporter + */ + protected $reporter; + + public function __construct(Registry $registry, Formatter $formatter, Reporter $reporter) + { + $this->registry = $registry; + $this->formatter = $formatter; + $this->reporter = $reporter; + } + + /** + * Catch all errors that happen during further middleware execution. + */ + public function process(Request $request, Handler $handler): Response + { + try { + return $handler->handle($request); + } catch (Throwable $e) { + $error = $this->registry->handle($e); + + if ($error->shouldBeReported()) { + $this->reporter->report($error); + } + + return $this->formatter->format($error, $request); + } + } +}