Merge pull request #1843 from flarum/fl/1641-exception-handling

Implement new error handling stack
This commit is contained in:
Franz Liedke 2019-08-13 22:45:46 +02:00 committed by GitHub
commit fcb3921a42
61 changed files with 656 additions and 1271 deletions

View File

@ -16,6 +16,10 @@ use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\ViewRenderer;
use Flarum\Foundation\ErrorHandling\WhoopsRenderer;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
@ -51,11 +55,11 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe = new MiddlewarePipe;
// All requests should first be piped through our global error handler
if ($app->inDebugMode()) {
$pipe->pipe($app->make(HttpMiddleware\HandleErrorsWithWhoops::class));
} else {
$pipe->pipe($app->make(HttpMiddleware\HandleErrorsWithView::class));
}
$pipe->pipe(new HttpMiddleware\HandleErrors(
$app->make(Registry::class),
$app->inDebugMode() ? $app->make(WhoopsRenderer::class) : $app->make(ViewRenderer::class),
$app->tagged(Reporter::class)
));
$pipe->pipe($app->make(HttpMiddleware\ParseJsonBody::class));
$pipe->pipe($app->make(HttpMiddleware\StartSession::class));

View File

@ -20,12 +20,13 @@ use Flarum\Event\ConfigureMiddleware;
use Flarum\Event\ConfigureNotificationTypes;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Flarum\Foundation\ErrorHandling\JsonApiRenderer;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Tobscure\JsonApi\ErrorHandler;
use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler;
use Zend\Stratigility\MiddlewarePipe;
class ApiServiceProvider extends AbstractServiceProvider
@ -49,7 +50,11 @@ class ApiServiceProvider extends AbstractServiceProvider
$this->app->singleton('flarum.api.middleware', function (Application $app) {
$pipe = new MiddlewarePipe;
$pipe->pipe($app->make(Middleware\HandleErrors::class));
$pipe->pipe(new HttpMiddleware\HandleErrors(
$app->make(Registry::class),
$app->make(JsonApiRenderer::class),
$app->tagged(Reporter::class)
));
$pipe->pipe($app->make(HttpMiddleware\ParseJsonBody::class));
$pipe->pipe($app->make(Middleware\FakeHttpMethods::class));
@ -68,25 +73,6 @@ class ApiServiceProvider extends AbstractServiceProvider
$this->app->afterResolving('flarum.api.middleware', function (MiddlewarePipe $pipe) {
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
});
$this->app->singleton(ErrorHandler::class, function () {
$handler = new ErrorHandler;
$handler->registerHandler(new ExceptionHandler\FloodingExceptionHandler);
$handler->registerHandler(new ExceptionHandler\IlluminateValidationExceptionHandler);
$handler->registerHandler(new ExceptionHandler\InvalidAccessTokenExceptionHandler);
$handler->registerHandler(new ExceptionHandler\InvalidConfirmationTokenExceptionHandler);
$handler->registerHandler(new ExceptionHandler\MethodNotAllowedExceptionHandler);
$handler->registerHandler(new ExceptionHandler\ModelNotFoundExceptionHandler);
$handler->registerHandler(new ExceptionHandler\PermissionDeniedExceptionHandler);
$handler->registerHandler(new ExceptionHandler\RouteNotFoundExceptionHandler);
$handler->registerHandler(new ExceptionHandler\TokenMismatchExceptionHandler);
$handler->registerHandler(new ExceptionHandler\ValidationExceptionHandler);
$handler->registerHandler(new InvalidParameterExceptionHandler);
$handler->registerHandler(new ExceptionHandler\FallbackExceptionHandler($this->app->inDebugMode(), $this->app->make('log')));
return $handler;
});
}
/**

View File

@ -12,11 +12,14 @@
namespace Flarum\Api;
use Exception;
use Flarum\Foundation\ErrorHandling\JsonApiRenderer;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Zend\Diactoros\ServerRequestFactory;
class Client
@ -27,18 +30,18 @@ class Client
protected $container;
/**
* @var ErrorHandler
* @var Registry
*/
protected $errorHandler;
protected $registry;
/**
* @param Container $container
* @param ErrorHandler $errorHandler
* @param Registry $registry
*/
public function __construct(Container $container, ErrorHandler $errorHandler = null)
public function __construct(Container $container, Registry $registry)
{
$this->container = $container;
$this->errorHandler = $errorHandler;
$this->registry = $registry;
}
/**
@ -69,23 +72,14 @@ class Client
try {
return $controller->handle($request);
} catch (Exception $e) {
if (! $this->errorHandler) {
} catch (Throwable $e) {
$error = $this->registry->handle($e);
if ($error->shouldBeReported()) {
throw $e;
}
return $this->errorHandler->handle($e);
return (new JsonApiRenderer)->format($error, $request);
}
}
/**
* @param ErrorHandler $errorHandler
* @return Client
*/
public function setErrorHandler(?ErrorHandler $errorHandler): self
{
$this->errorHandler = $errorHandler;
return $this;
}
}

View File

@ -1,51 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api;
use Exception;
use Throwable;
use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\ErrorHandler as JsonApiErrorHandler;
class ErrorHandler
{
/**
* @var JsonApiErrorHandler
*/
protected $errorHandler;
/**
* @param JsonApiErrorHandler $errorHandler
*/
public function __construct(JsonApiErrorHandler $errorHandler)
{
$this->errorHandler = $errorHandler;
}
/**
* @param Exception $e
* @return JsonApiResponse
*/
public function handle(Throwable $e)
{
if (! $e instanceof Exception) {
$e = new Exception($e->getMessage(), $e->getCode(), $e);
}
$response = $this->errorHandler->handle($e);
$document = new Document;
$document->setErrors($response->getErrors());
return new JsonApiResponse($document, $response->getStatus());
}
}

View File

@ -12,7 +12,12 @@
namespace Flarum\Api\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class InvalidAccessTokenException extends Exception
class InvalidAccessTokenException extends Exception implements KnownError
{
public function getType(): string
{
return 'invalid_access_token';
}
}

View File

@ -1,77 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Psr\Log\LoggerInterface;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class FallbackExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var bool
*/
protected $debug;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param bool $debug
* @param LoggerInterface $logger
*/
public function __construct($debug, LoggerInterface $logger)
{
$this->debug = $debug;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return true;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 500;
$error = $this->constructError($e, $status);
$this->logger->error($e);
return new ResponseBag($status, [$error]);
}
/**
* @param Exception $e
* @param $status
* @return array
*/
private function constructError(Exception $e, $status)
{
$error = ['code' => $status, 'title' => 'Internal server error'];
if ($this->debug) {
$error['detail'] = (string) $e;
}
return $error;
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Post\Exception\FloodingException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class FloodingExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof FloodingException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 429;
$error = [
'status' => (string) $status,
'code' => 'too_many_requests'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,58 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Illuminate\Validation\ValidationException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof ValidationException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 422;
$errors = $this->formatErrors($e->errors());
return new ResponseBag($status, $errors);
}
/**
* @param array $errors
* @return array
*/
protected function formatErrors(array $errors)
{
$errors = array_map(function ($field, $messages) {
return [
'status' => '422',
'code' => 'validation_error',
'detail' => implode("\n", $messages),
'source' => ['pointer' => "/data/attributes/$field"]
];
}, array_keys($errors), $errors);
return $errors;
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Api\Exception\InvalidAccessTokenException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class InvalidAccessTokenExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof InvalidAccessTokenException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 401;
$error = [
'status' => (string) $status,
'code' => 'invalid_access_token'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\User\Exception\InvalidConfirmationTokenException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class InvalidConfirmationTokenExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof InvalidConfirmationTokenException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 403;
$error = [
'status' => (string) $status,
'code' => 'invalid_confirmation_token'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Http\Exception\MethodNotAllowedException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class MethodNotAllowedExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof MethodNotAllowedException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 405;
$error = [
'status' => (string) $status,
'code' => 'method_not_allowed'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof ModelNotFoundException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 404;
$error = [
'status' => (string) $status,
'code' => 'resource_not_found'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\User\Exception\PermissionDeniedException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof PermissionDeniedException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 401;
$error = [
'status' => (string) $status,
'code' => 'permission_denied'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Http\Exception\RouteNotFoundException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class RouteNotFoundExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof RouteNotFoundException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 404;
$error = [
'status' => (string) $status,
'code' => 'route_not_found'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,42 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Http\Exception\TokenMismatchException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class TokenMismatchExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof TokenMismatchException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$status = 400;
$error = [
'status' => (string) $status,
'code' => 'csrf_token_mismatch'
];
return new ResponseBag($status, [$error]);
}
}

View File

@ -1,53 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\ExceptionHandler;
use Exception;
use Flarum\Foundation\ValidationException;
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
use Tobscure\JsonApi\Exception\Handler\ResponseBag;
class ValidationExceptionHandler implements ExceptionHandlerInterface
{
/**
* {@inheritdoc}
*/
public function manages(Exception $e)
{
return $e instanceof ValidationException;
}
/**
* {@inheritdoc}
*/
public function handle(Exception $e)
{
$errors = array_merge(
$this->buildErrors($e->getAttributes(), '/data/attributes'),
$this->buildErrors($e->getRelationships(), '/data/relationships')
);
return new ResponseBag(422, $errors);
}
private function buildErrors(array $messages, $pointer)
{
return array_map(function ($path, $detail) use ($pointer) {
return [
'status' => '422',
'code' => 'validation_error',
'detail' => $detail,
'source' => ['pointer' => $pointer.'/'.$path]
];
}, array_keys($messages), $messages);
}
}

View File

@ -1,47 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Api\Middleware;
use Flarum\Api\ErrorHandler;
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 ErrorHandler
*/
protected $errorHandler;
/**
* @param ErrorHandler $errorHandler
*/
public function __construct(ErrorHandler $errorHandler)
{
$this->errorHandler = $errorHandler;
}
/**
* 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) {
return $this->errorHandler->handle($e);
}
}
}

View File

@ -18,6 +18,10 @@ use Flarum\Extension\Event\Enabled;
use Flarum\Formatter\Formatter;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Application;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\ViewRenderer;
use Flarum\Foundation\ErrorHandling\WhoopsRenderer;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\AddLocaleAssets;
use Flarum\Frontend\AddTranslations;
@ -61,11 +65,11 @@ class ForumServiceProvider extends AbstractServiceProvider
$pipe = new MiddlewarePipe;
// All requests should first be piped through our global error handler
if ($app->inDebugMode()) {
$pipe->pipe($app->make(HttpMiddleware\HandleErrorsWithWhoops::class));
} else {
$pipe->pipe($app->make(HttpMiddleware\HandleErrorsWithView::class));
}
$pipe->pipe(new HttpMiddleware\HandleErrors(
$app->make(Registry::class),
$app->inDebugMode() ? $app->make(WhoopsRenderer::class) : $app->make(ViewRenderer::class),
$app->tagged(Reporter::class)
));
$pipe->pipe($app->make(HttpMiddleware\ParseJsonBody::class));
$pipe->pipe($app->make(HttpMiddleware\CollectGarbage::class));

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\HandledError;
use Illuminate\Validation\ValidationException;
class IlluminateValidationExceptionHandler
{
public function handle(ValidationException $e): HandledError
{
return (new HandledError(
$e, 'validation_error', 422
))->withDetails($this->errorDetails($e));
}
protected function errorDetails(ValidationException $e): array
{
$errors = $e->errors();
return array_map(function ($field, $messages) {
return [
'detail' => implode("\n", $messages),
'source' => ['pointer' => "/data/attributes/$field"]
];
}, array_keys($errors), $errors);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\HandledError;
use Flarum\Foundation\ValidationException;
class ValidationExceptionHandler
{
public function handle(ValidationException $e)
{
return (new HandledError(
$e, 'validation_error', 422
))->withDetails(array_merge(
$this->buildDetails($e->getAttributes(), '/data/attributes'),
$this->buildDetails($e->getRelationships(), '/data/relationships')
));
}
private function buildDetails(array $messages, $pointer): array
{
return array_map(function ($path, $detail) use ($pointer) {
return [
'detail' => $detail,
'source' => ['pointer' => $pointer.'/'.$path]
];
}, array_keys($messages), $messages);
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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());
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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());
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}
}

View File

@ -0,0 +1,17 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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);
}

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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());
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Foundation;
use Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\LogReporter;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException as IlluminateValidationException;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class ErrorServiceProvider extends AbstractServiceProvider
{
public function register()
{
$this->app->singleton('flarum.error.statuses', function () {
return [
// 400 Bad Request
'csrf_token_mismatch' => 400,
'invalid_parameter' => 400,
// 401 Unauthorized
'invalid_access_token' => 401,
// 403 Forbidden
'forbidden' => 403,
'invalid_confirmation_token' => 403,
'permission_denied' => 403,
// 404 Not Found
'model_not_found' => 404,
'route_not_found' => 404,
// 405 Method Not Allowed
'method_not_allowed' => 405,
// 429 Too Many Requests
'too_many_requests' => 429,
];
});
$this->app->singleton('flarum.error.classes', function () {
return [
InvalidParameterException::class => 'invalid_parameter',
ModelNotFoundException::class => 'model_not_found',
];
});
$this->app->singleton('flarum.error.handlers', function () {
return [
IlluminateValidationException::class => ExceptionHandler\IlluminateValidationExceptionHandler::class,
ValidationException::class => ExceptionHandler\ValidationExceptionHandler::class,
];
});
$this->app->singleton(Registry::class, function () {
return new Registry(
$this->app->make('flarum.error.statuses'),
$this->app->make('flarum.error.classes'),
$this->app->make('flarum.error.handlers')
);
});
$this->app->tag(LogReporter::class, Reporter::class);
}
}

View File

@ -117,6 +117,7 @@ class InstalledSite implements SiteInterface
$laravel->register(DatabaseServiceProvider::class);
$laravel->register(DiscussionServiceProvider::class);
$laravel->register(ExtensionServiceProvider::class);
$laravel->register(ErrorServiceProvider::class);
$laravel->register(FilesystemServiceProvider::class);
$laravel->register(FormatterServiceProvider::class);
$laravel->register(ForumServiceProvider::class);

View File

@ -0,0 +1,17 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}

View File

@ -69,6 +69,7 @@ class UninstalledSite implements SiteInterface
$this->registerLogger($laravel);
$laravel->register(ErrorServiceProvider::class);
$laravel->register(LocaleServiceProvider::class);
$laravel->register(FilesystemServiceProvider::class);
$laravel->register(SessionServiceProvider::class);

View File

@ -70,8 +70,6 @@ class Frontend
{
$actor = $request->getAttribute('actor');
$this->api->setErrorHandler(null);
return $this->getResponseBody(
$this->api->send(ShowForumController::class, $actor)
);

View File

@ -12,11 +12,12 @@
namespace Flarum\Http\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class ForbiddenException extends Exception
class ForbiddenException extends Exception implements KnownError
{
public function __construct($message = null, $code = 403, Exception $previous = null)
public function getType(): string
{
parent::__construct($message, $code, $previous);
return 'forbidden';
}
}

View File

@ -12,11 +12,12 @@
namespace Flarum\Http\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class MethodNotAllowedException extends Exception
class MethodNotAllowedException extends Exception implements KnownError
{
public function __construct($message = null, $code = 405, Exception $previous = null)
public function getType(): string
{
parent::__construct($message, $code, $previous);
return 'method_not_allowed';
}
}

View File

@ -12,11 +12,12 @@
namespace Flarum\Http\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class RouteNotFoundException extends Exception
class RouteNotFoundException extends Exception implements KnownError
{
public function __construct($message = null, $code = 404, Exception $previous = null)
public function getType(): string
{
parent::__construct($message, $code, $previous);
return 'route_not_found';
}
}

View File

@ -12,11 +12,12 @@
namespace Flarum\Http\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class TokenMismatchException extends Exception
class TokenMismatchException extends Exception implements KnownError
{
public function __construct($message = null, $code = 419, Exception $previous = null)
public function getType(): string
{
parent::__construct($message, $code, $previous);
return 'csrf_token_mismatch';
}
}

View File

@ -11,27 +11,36 @@
namespace Flarum\Http\Middleware;
use Franzl\Middleware\Whoops\WhoopsRunner;
use Flarum\Foundation\ErrorHandling\Formatter;
use Flarum\Foundation\ErrorHandling\Registry;
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 Psr\Log\LoggerInterface;
use Throwable;
class HandleErrorsWithWhoops implements Middleware
class HandleErrors implements Middleware
{
/**
* @var LoggerInterface
* @var Registry
*/
protected $logger;
protected $registry;
/**
* @param LoggerInterface $logger
* @var Formatter
*/
public function __construct(LoggerInterface $logger)
protected $formatter;
/**
* @var \Flarum\Foundation\ErrorHandling\Reporter[]
*/
protected $reporters;
public function __construct(Registry $registry, Formatter $formatter, array $reporters)
{
$this->logger = $logger;
$this->registry = $registry;
$this->formatter = $formatter;
$this->reporters = $reporters;
}
/**
@ -42,19 +51,15 @@ class HandleErrorsWithWhoops implements Middleware
try {
return $handler->handle($request);
} catch (Throwable $e) {
$status = 500;
$errorCode = $e->getCode();
$error = $this->registry->handle($e);
if ($errorCode !== 404) {
$this->logger->error($e);
if ($error->shouldBeReported()) {
foreach ($this->reporters as $reporter) {
$reporter->report($error);
}
}
if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) {
$status = $errorCode;
}
return WhoopsRunner::handle($e, $request)
->withStatus($status);
return $this->formatter->format($error, $request);
}
}
}

View File

@ -1,111 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\View\Factory as ViewFactory;
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 Psr\Log\LoggerInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Throwable;
use Zend\Diactoros\Response\HtmlResponse;
class HandleErrorsWithView implements Middleware
{
/**
* @var ViewFactory
*/
protected $view;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @param ViewFactory $view
* @param LoggerInterface $logger
* @param TranslatorInterface $translator
* @param SettingsRepositoryInterface $settings
*/
public function __construct(ViewFactory $view, LoggerInterface $logger, TranslatorInterface $translator, SettingsRepositoryInterface $settings)
{
$this->view = $view;
$this->logger = $logger;
$this->translator = $translator;
$this->settings = $settings;
}
/**
* 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) {
return $this->formatException($e);
}
}
protected function formatException(Throwable $error)
{
$status = 500;
$errorCode = $error->getCode();
// If it seems to be a valid HTTP status code, we pass on the
// exception's status.
if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) {
$status = $errorCode;
}
if (! $this->view->exists($name = "flarum.forum::error.$status")) {
$name = 'flarum.forum::error.default';
$this->logger->error($error);
}
$view = $this->view->make($name)
->with('error', $error)
->with('message', $this->getMessage($status));
return new HtmlResponse($view->render(), $status);
}
private function getMessage($status)
{
return $this->getTranslationIfExists($status)
?? $this->getTranslationIfExists(500)
?? 'An error occurred while trying to load this page.';
}
private function getTranslationIfExists($status)
{
$key = "core.views.error.${status}_message";
$translation = $this->translator->trans($key, ['{forum}' => $this->settings->get('forum_title')]);
return $translation === $key ? null : $translation;
}
}

View File

@ -12,8 +12,11 @@
namespace Flarum\Install;
use Flarum\Foundation\AppInterface;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\WhoopsRenderer;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrorsWithWhoops;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\StartSession;
use Flarum\Install\Console\InstallCommand;
use Illuminate\Contracts\Container\Container;
@ -37,7 +40,11 @@ class Installer implements AppInterface
public function getRequestHandler()
{
$pipe = new MiddlewarePipe;
$pipe->pipe($this->container->make(HandleErrorsWithWhoops::class));
$pipe->pipe(new HandleErrors(
$this->container->make(Registry::class),
$this->container->make(WhoopsRenderer::class),
$this->container->tagged(Reporter::class)
));
$pipe->pipe($this->container->make(StartSession::class));
$pipe->pipe(
new DispatchRoute($this->container->make('flarum.install.routes'))

View File

@ -12,7 +12,12 @@
namespace Flarum\Post\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class FloodingException extends Exception
class FloodingException extends Exception implements KnownError
{
public function getType(): string
{
return 'too_many_requests';
}
}

View File

@ -12,7 +12,12 @@
namespace Flarum\User\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class InvalidConfirmationTokenException extends Exception
class InvalidConfirmationTokenException extends Exception implements KnownError
{
public function getType(): string
{
return 'invalid_confirmation_token';
}
}

View File

@ -12,11 +12,12 @@
namespace Flarum\User\Exception;
use Exception;
use Flarum\Foundation\KnownError;
class PermissionDeniedException extends Exception
class PermissionDeniedException extends Exception implements KnownError
{
public function __construct($message = null, $code = 403, Exception $previous = null)
public function getType(): string
{
parent::__construct($message, $code, $previous);
return 'permission_denied';
}
}

View File

@ -17,7 +17,6 @@ use Flarum\Api\Client;
use Flarum\Api\Controller\CreateGroupController;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\Guest;
use Flarum\User\User;
use Illuminate\Support\Str;
@ -63,11 +62,10 @@ class AuthenticateWithApiKeyTest extends TestCase
{
/** @var Client $api */
$api = $this->app()->getContainer()->make(Client::class);
$api->setErrorHandler(null);
$this->expectException(PermissionDeniedException::class);
$response = $api->send(CreateGroupController::class, new Guest);
$api->send(CreateGroupController::class, new Guest);
$this->assertEquals(403, $response->getStatusCode());
}
/**

View File

@ -53,8 +53,6 @@ abstract class ApiControllerTestCase extends TestCase
/** @var Client $api */
$api = $this->app()->getContainer()->make(Client::class);
$api->setErrorHandler(null);
return $api->send($controller, $actor ?? new Guest, $queryParams, $body);
}
}

View File

@ -15,7 +15,6 @@ use Flarum\Api\Controller\CreateDiscussionController;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
class CreateDiscussionControllerTest extends ApiControllerTestCase
{
@ -72,11 +71,9 @@ class CreateDiscussionControllerTest extends ApiControllerTestCase
$this->actor = User::find(1);
$data = Arr::except($this->data, 'content');
$response = $this->callWith($data);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('The given data was invalid.');
$this->callWith($data);
$this->assertEquals(422, $response->getStatusCode());
}
/**
@ -87,10 +84,8 @@ class CreateDiscussionControllerTest extends ApiControllerTestCase
$this->actor = User::find(1);
$data = Arr::except($this->data, 'title');
$response = $this->callWith($data);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('The given data was invalid.');
$this->callWith($data);
$this->assertEquals(422, $response->getStatusCode());
}
}

View File

@ -13,11 +13,9 @@ namespace Flarum\Tests\integration\api\Controller;
use Flarum\Api\Controller\CreateGroupController;
use Flarum\Group\Group;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class CreateGroupControllerTest extends ApiControllerTestCase
{
@ -55,10 +53,7 @@ class CreateGroupControllerTest extends ApiControllerTestCase
{
$this->actor = User::find(1);
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('The given data was invalid.');
$this->callWith();
$this->assertEquals(422, $this->callWith()->getStatusCode());
}
/**
@ -89,8 +84,6 @@ class CreateGroupControllerTest extends ApiControllerTestCase
{
$this->actor = User::find(2);
$this->expectException(PermissionDeniedException::class);
$this->callWith($this->data);
$this->assertEquals(403, $this->callWith($this->data)->getStatusCode());
}
}

View File

@ -13,10 +13,8 @@ namespace Flarum\Tests\integration\api\Controller;
use Flarum\Api\Controller\CreateUserController;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\User;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
class CreateUserControllerTest extends ApiControllerTestCase
{
@ -53,10 +51,9 @@ class CreateUserControllerTest extends ApiControllerTestCase
*/
public function cannot_create_user_without_data()
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('The given data was invalid.');
$response = $this->callWith();
$this->callWith();
$this->assertEquals(422, $response->getStatusCode());
}
/**
@ -106,12 +103,9 @@ class CreateUserControllerTest extends ApiControllerTestCase
$settings = app(SettingsRepositoryInterface::class);
$settings->set('allow_sign_up', false);
$this->expectException(PermissionDeniedException::class);
$response = $this->callWith($this->data);
$this->assertEquals(403, $response->getStatusCode());
try {
$this->callWith($this->data);
} finally {
$settings->set('allow_sign_up', true);
}
}
}

View File

@ -12,7 +12,6 @@
namespace Flarum\Tests\integration\api\Controller;
use Flarum\Api\Controller\ListNotificationsController;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\User;
class ListNotificationsControllerTest extends ApiControllerTestCase
@ -35,9 +34,9 @@ class ListNotificationsControllerTest extends ApiControllerTestCase
*/
public function disallows_index_for_guest()
{
$this->expectException(PermissionDeniedException::class);
$response = $this->callWith();
$this->callWith();
$this->assertEquals(403, $response->getStatusCode());
}
/**

View File

@ -12,7 +12,6 @@
namespace Flarum\Tests\integration\api\Controller;
use Flarum\Api\Controller\ListUsersController;
use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\User;
class ListUsersControllerTest extends ApiControllerTestCase
@ -41,9 +40,9 @@ class ListUsersControllerTest extends ApiControllerTestCase
*/
public function disallows_index_for_guest()
{
$this->expectException(PermissionDeniedException::class);
$response = $this->callWith();
$this->callWith();
$this->assertEquals(403, $response->getStatusCode());
}
/**

View File

@ -15,7 +15,6 @@ use Carbon\Carbon;
use Flarum\Api\Controller\ShowDiscussionController;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ShowDiscussionControllerTest extends ApiControllerTestCase
{
@ -73,11 +72,9 @@ class ShowDiscussionControllerTest extends ApiControllerTestCase
*/
public function guest_cannot_see_empty_discussion()
{
$this->expectException(ModelNotFoundException::class);
$response = $this->callWith([], ['id' => 1]);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(404, $response->getStatusCode());
}
/**
@ -95,8 +92,8 @@ class ShowDiscussionControllerTest extends ApiControllerTestCase
*/
public function guests_cannot_see_private_discussion()
{
$this->expectException(ModelNotFoundException::class);
$response = $this->callWith([], ['id' => 3]);
$this->callWith([], ['id' => 3]);
$this->assertEquals(404, $response->getStatusCode());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\FloodingExceptionHandler;
use Flarum\Post\Exception\FloodingException;
use PHPUnit\Framework\TestCase;
class FloodingExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new FloodingExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new FloodingException));
}
public function test_it_provides_expected_output()
{
$result = $this->handler->handle(new FloodingException);
$this->assertEquals(429, $result->getStatus());
$this->assertEquals([
[
'status' => '429',
'code' => 'too_many_requests'
]
], $result->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\Exception\InvalidAccessTokenException;
use Flarum\Api\ExceptionHandler\InvalidAccessTokenExceptionHandler;
use PHPUnit\Framework\TestCase;
class InvalidAccessTokenExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new InvalidAccessTokenExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new InvalidAccessTokenException));
}
public function test_output()
{
$response = $this->handler->handle(new InvalidAccessTokenException);
$this->assertEquals(401, $response->getStatus());
$this->assertEquals([
[
'status' => '401',
'code' => 'invalid_access_token'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\InvalidConfirmationTokenExceptionHandler;
use Flarum\User\Exception\InvalidConfirmationTokenException;
use PHPUnit\Framework\TestCase;
class InvalidConfirmationTokenExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new InvalidConfirmationTokenExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new InvalidConfirmationTokenException));
}
public function test_output()
{
$response = $this->handler->handle(new InvalidConfirmationTokenException);
$this->assertEquals(403, $response->getStatus());
$this->assertEquals([
[
'status' => '403',
'code' => 'invalid_confirmation_token'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\MethodNotAllowedExceptionHandler;
use Flarum\Http\Exception\MethodNotAllowedException;
use PHPUnit\Framework\TestCase;
class MethodNotAllowedExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new MethodNotAllowedExceptionHandler();
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new MethodNotAllowedException()));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new MethodNotAllowedException);
$this->assertEquals(405, $response->getStatus());
$this->assertEquals([
[
'status' => '405',
'code' => 'method_not_allowed'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\ModelNotFoundExceptionHandler;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PHPUnit\Framework\TestCase;
class ModelNotFoundExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new ModelNotFoundExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new ModelNotFoundException));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new ModelNotFoundException);
$this->assertEquals(404, $response->getStatus());
$this->assertEquals([
[
'status' => '404',
'code' => 'resource_not_found'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\PermissionDeniedExceptionHandler;
use Flarum\User\Exception\PermissionDeniedException;
use PHPUnit\Framework\TestCase;
class PermissionDeniedExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new PermissionDeniedExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new PermissionDeniedException));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new PermissionDeniedException);
$this->assertEquals(401, $response->getStatus());
$this->assertEquals([
[
'status' => '401',
'code' => 'permission_denied'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\RouteNotFoundExceptionHandler;
use Flarum\Http\Exception\RouteNotFoundException;
use PHPUnit\Framework\TestCase;
class RouteNotFoundExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new RouteNotFoundExceptionHandler();
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new RouteNotFoundException()));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new RouteNotFoundException);
$this->assertEquals(404, $response->getStatus());
$this->assertEquals([
[
'status' => '404',
'code' => 'route_not_found'
]
], $response->getErrors());
}
}

View File

@ -1,46 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\TokenMismatchExceptionHandler;
use Flarum\Http\Exception\TokenMismatchException;
use PHPUnit\Framework\TestCase;
class TokenMismatchExceptionHandlerTest extends TestCase
{
private $handler;
public function setUp()
{
$this->handler = new TokenMismatchExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new TokenMismatchException()));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new TokenMismatchException);
$this->assertEquals(400, $response->getStatus());
$this->assertEquals([
[
'status' => '400',
'code' => 'csrf_token_mismatch'
]
], $response->getErrors());
}
}

View File

@ -9,10 +9,9 @@
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
namespace Flarum\Tests\unit\Foundation\ErrorHandling\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\IlluminateValidationExceptionHandler;
use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;
use Illuminate\Validation\Factory;
@ -28,29 +27,20 @@ class IlluminateValidationExceptionHandlerTest extends TestCase
$this->handler = new IlluminateValidationExceptionHandler;
}
public function test_it_handles_familiar_exceptions()
{
$validException = new ValidationException($this->makeValidator());
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages($validException));
}
public function test_it_creates_the_desired_output()
{
$exception = new ValidationException($this->makeValidator(['foo' => ''], ['foo' => 'required']));
$response = $this->handler->handle($exception);
$error = $this->handler->handle($exception);
$this->assertEquals(422, $response->getStatus());
$this->assertEquals(422, $error->getStatusCode());
$this->assertEquals('validation_error', $error->getType());
$this->assertEquals([
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'validation.required',
'source' => ['pointer' => '/data/attributes/foo']
]
], $response->getErrors());
], $error->getDetails());
}
private function makeValidator($data = [], $rules = [])

View File

@ -9,10 +9,9 @@
* file that was distributed with this source code.
*/
namespace Flarum\Tests\unit\Api\ExceptionHandler;
namespace Flarum\Tests\unit\Foundation\ErrorHandling\ExceptionHandler;
use Exception;
use Flarum\Api\ExceptionHandler\ValidationExceptionHandler;
use Flarum\Foundation\ErrorHandling\ExceptionHandler\ValidationExceptionHandler;
use Flarum\Foundation\ValidationException;
use PHPUnit\Framework\TestCase;
@ -25,33 +24,24 @@ class ValidationExceptionHandlerTest extends TestCase
$this->handler = new ValidationExceptionHandler;
}
public function test_it_handles_recognisable_exceptions()
{
$this->assertFalse($this->handler->manages(new Exception));
$this->assertTrue($this->handler->manages(new ValidationException([])));
}
public function test_managing_exceptions()
{
$response = $this->handler->handle(new ValidationException(
$error = $this->handler->handle(new ValidationException(
['foo' => 'Attribute error'],
['bar' => 'Relationship error']
));
$this->assertEquals(422, $response->getStatus());
$this->assertEquals(422, $error->getStatusCode());
$this->assertEquals('validation_error', $error->getType());
$this->assertEquals([
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'Attribute error',
'source' => ['pointer' => '/data/attributes/foo']
],
[
'status' => '422',
'code' => 'validation_error',
'detail' => 'Relationship error',
'source' => ['pointer' => '/data/relationships/bar']
]
], $response->getErrors());
], $error->getDetails());
}
}