2017-09-14 22:21:00 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Exceptions;
|
|
|
|
|
2022-07-27 14:13:58 +03:00
|
|
|
use Akaunting\Money\Exceptions\UnexpectedAmountException;
|
2023-04-24 11:02:45 +03:00
|
|
|
use App\Events\Email\InvalidEmailDetected;
|
2022-06-01 10:15:55 +03:00
|
|
|
use App\Exceptions\Http\Resource as ResourceException;
|
2017-09-14 22:21:00 +03:00
|
|
|
use Illuminate\Auth\AuthenticationException;
|
2020-03-04 11:09:28 +03:00
|
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
2017-09-14 22:21:00 +03:00
|
|
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
2021-12-03 15:43:15 +03:00
|
|
|
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
2022-06-01 10:15:55 +03:00
|
|
|
use Illuminate\Http\Response;
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
use Illuminate\Validation\ValidationException;
|
2022-07-27 14:13:58 +03:00
|
|
|
use Illuminate\View\ViewException;
|
2019-01-31 17:09:59 +03:00
|
|
|
use Symfony\Component\Debug\Exception\FatalThrowableError;
|
2022-06-01 10:15:55 +03:00
|
|
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
2021-04-05 23:46:52 +03:00
|
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
2023-04-24 11:02:45 +03:00
|
|
|
use Symfony\Component\Mailer\Exception\HttpTransportException as MailerHttpTransportException;
|
2020-03-04 11:09:28 +03:00
|
|
|
use Throwable;
|
2017-09-14 22:21:00 +03:00
|
|
|
|
|
|
|
class Handler extends ExceptionHandler
|
|
|
|
{
|
2022-07-27 14:13:58 +03:00
|
|
|
/**
|
|
|
|
* A list of exception types with their corresponding custom log levels.
|
|
|
|
*
|
|
|
|
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
|
|
|
*/
|
|
|
|
protected $levels = [
|
|
|
|
//
|
|
|
|
];
|
|
|
|
|
2017-09-14 22:21:00 +03:00
|
|
|
/**
|
2020-10-14 17:07:59 +03:00
|
|
|
* A list of the exception types that are not reported.
|
2017-09-14 22:21:00 +03:00
|
|
|
*
|
2022-07-27 14:13:58 +03:00
|
|
|
* @var array<int, class-string<\Throwable>>
|
2017-09-14 22:21:00 +03:00
|
|
|
*/
|
|
|
|
protected $dontReport = [
|
2019-11-16 10:21:14 +03:00
|
|
|
//
|
|
|
|
];
|
|
|
|
|
|
|
|
/**
|
2022-07-27 14:13:58 +03:00
|
|
|
* A list of the inputs that are never flashed to the session on validation exceptions.
|
2019-11-16 10:21:14 +03:00
|
|
|
*
|
2022-07-27 14:13:58 +03:00
|
|
|
* @var array<int, string>
|
2019-11-16 10:21:14 +03:00
|
|
|
*/
|
|
|
|
protected $dontFlash = [
|
2022-07-27 14:13:58 +03:00
|
|
|
'current_password',
|
2019-11-16 10:21:14 +03:00
|
|
|
'password',
|
|
|
|
'password_confirmation',
|
2017-09-14 22:21:00 +03:00
|
|
|
];
|
|
|
|
|
2022-10-17 14:19:42 +03:00
|
|
|
/**
|
|
|
|
* Register the exception handling callbacks for the application.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function register()
|
|
|
|
{
|
|
|
|
$this->reportable(function (Throwable $e) {
|
|
|
|
if (config('logging.default') == 'bugsnag') {
|
2022-10-19 01:29:40 +03:00
|
|
|
call_user_func(config('bugsnag.before_send'), $e);
|
2022-10-17 14:19:42 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-14 22:21:00 +03:00
|
|
|
/**
|
|
|
|
* Report or log an exception.
|
|
|
|
*
|
2020-03-04 11:09:28 +03:00
|
|
|
* @param \Throwable $exception
|
2017-09-14 22:21:00 +03:00
|
|
|
* @return void
|
2020-03-13 15:38:41 +03:00
|
|
|
*
|
|
|
|
* @throws \Exception
|
2017-09-14 22:21:00 +03:00
|
|
|
*/
|
2020-03-04 11:09:28 +03:00
|
|
|
public function report(Throwable $exception)
|
2017-09-14 22:21:00 +03:00
|
|
|
{
|
2023-05-11 09:37:53 +03:00
|
|
|
if ($exception instanceof MailerHttpTransportException) {
|
|
|
|
$email = $this->handleMailerExceptions($exception);
|
|
|
|
|
|
|
|
if (! empty($email)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-14 22:21:00 +03:00
|
|
|
parent::report($exception);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render an exception into an HTTP response.
|
|
|
|
*
|
|
|
|
* @param \Illuminate\Http\Request $request
|
2020-03-04 11:09:28 +03:00
|
|
|
* @param \Throwable $exception
|
2020-03-13 15:38:41 +03:00
|
|
|
* @return \Symfony\Component\HttpFoundation\Response
|
|
|
|
*
|
|
|
|
* @throws \Throwable
|
2017-09-14 22:21:00 +03:00
|
|
|
*/
|
2020-03-04 11:09:28 +03:00
|
|
|
public function render($request, Throwable $exception)
|
2017-09-14 22:21:00 +03:00
|
|
|
{
|
2022-06-01 10:15:55 +03:00
|
|
|
if ($request->isApi()) {
|
|
|
|
return $this->handleApiExceptions($request, $exception);
|
|
|
|
}
|
|
|
|
|
2020-04-26 06:07:27 -04:00
|
|
|
if (config('app.debug') === false) {
|
2022-06-01 10:15:55 +03:00
|
|
|
return $this->handleWebExceptions($request, $exception);
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
|
|
|
|
2017-09-14 22:21:00 +03:00
|
|
|
return parent::render($request, $exception);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-11-16 10:21:14 +03:00
|
|
|
* Convert an authentication exception into a response.
|
2017-09-14 22:21:00 +03:00
|
|
|
*
|
|
|
|
* @param \Illuminate\Http\Request $request
|
|
|
|
* @param \Illuminate\Auth\AuthenticationException $exception
|
2019-11-16 10:21:14 +03:00
|
|
|
* @return \Symfony\Component\HttpFoundation\Response
|
2017-09-14 22:21:00 +03:00
|
|
|
*/
|
|
|
|
protected function unauthenticated($request, AuthenticationException $exception)
|
|
|
|
{
|
2019-11-16 10:21:14 +03:00
|
|
|
// Store the current url in the session
|
2021-04-05 23:46:52 +03:00
|
|
|
if ($request->url() !== config('app.url')) {
|
|
|
|
session(['url.intended' => $request->url()]);
|
|
|
|
}
|
2018-12-03 13:45:33 +03:00
|
|
|
|
2019-11-16 10:21:14 +03:00
|
|
|
return $request->expectsJson()
|
2021-04-05 23:46:52 +03:00
|
|
|
? response()->json(['message' => $exception->getMessage()], 401)
|
|
|
|
: redirect()->to($exception->redirectTo() ?? route('login'));
|
2017-09-14 22:21:00 +03:00
|
|
|
}
|
2019-01-31 17:09:59 +03:00
|
|
|
|
2022-06-01 10:15:55 +03:00
|
|
|
protected function handleWebExceptions($request, $exception)
|
2019-01-31 17:09:59 +03:00
|
|
|
{
|
|
|
|
if ($exception instanceof NotFoundHttpException) {
|
|
|
|
// ajax 404 json feedback
|
|
|
|
if ($request->ajax()) {
|
2022-07-27 14:13:58 +03:00
|
|
|
return response()->json([
|
|
|
|
'error' => trans('errors.header.404'),
|
|
|
|
], 404);
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
|
|
|
|
2021-02-12 19:26:38 +03:00
|
|
|
flash(trans('errors.body.page_not_found'))->error()->important();
|
2019-01-31 17:09:59 +03:00
|
|
|
|
|
|
|
// normal 404 view page feedback
|
|
|
|
return redirect()
|
|
|
|
->back()
|
|
|
|
->withErrors(['msg', trans('errors.body.page_not_found')]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($exception instanceof ModelNotFoundException) {
|
|
|
|
// ajax 404 json feedback
|
|
|
|
if ($request->ajax()) {
|
2022-07-27 14:13:58 +03:00
|
|
|
return response()->json([
|
|
|
|
'error' => trans('errors.header.404'),
|
|
|
|
], 404);
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
|
|
|
|
2021-03-17 12:22:05 +03:00
|
|
|
try {
|
|
|
|
$names = explode('.', $request->route()->getName());
|
|
|
|
$names[count($names) - 1] = 'index';
|
|
|
|
|
|
|
|
$route = route(implode('.', $names));
|
|
|
|
|
|
|
|
flash(trans('errors.message.record'))->warning()->important();
|
|
|
|
|
|
|
|
return redirect($route);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
// normal 404 view page feedback
|
|
|
|
return response()->view('errors.404', [], 404);
|
|
|
|
}
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($exception instanceof FatalThrowableError) {
|
|
|
|
// ajax 500 json feedback
|
|
|
|
if ($request->ajax()) {
|
2022-07-27 14:13:58 +03:00
|
|
|
return response()->json([
|
|
|
|
'error' => trans('errors.header.500'),
|
|
|
|
], 500);
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// normal 500 view page feedback
|
|
|
|
return response()->view('errors.500', [], 500);
|
|
|
|
}
|
|
|
|
|
2021-12-03 15:43:15 +03:00
|
|
|
if ($exception instanceof ThrottleRequestsException) {
|
|
|
|
// ajax 500 json feedback
|
|
|
|
if ($request->ajax()) {
|
2022-07-27 14:13:58 +03:00
|
|
|
return response()->json([
|
|
|
|
'error' => $exception->getMessage(),
|
|
|
|
], 429);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($exception instanceof ViewException) {
|
|
|
|
$real_exception = $this->getRealException($exception, ViewException::class);
|
|
|
|
|
|
|
|
if ($real_exception instanceof UnexpectedAmountException) {
|
|
|
|
if ($request->ajax()) {
|
|
|
|
return response()->json([
|
|
|
|
'error' => trans('errors.message.amount'),
|
|
|
|
], 500);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response()->view('errors.500', [
|
|
|
|
'message' => trans('errors.message.amount'),
|
|
|
|
], 500);
|
2021-12-03 15:43:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-24 11:02:45 +03:00
|
|
|
if ($exception instanceof MailerHttpTransportException) {
|
2023-05-11 09:37:53 +03:00
|
|
|
$email = $this->handleMailerExceptions($exception);
|
|
|
|
|
|
|
|
if (! empty($email)) {
|
|
|
|
$message = trans('notifications.menu.invalid_email.description', ['email' => $email]);
|
2023-04-24 13:33:55 +03:00
|
|
|
|
|
|
|
if ($request->ajax()) {
|
|
|
|
return response()->json([
|
2023-05-11 09:37:53 +03:00
|
|
|
'error' => $message,
|
2023-04-24 13:33:55 +03:00
|
|
|
], $exception->getCode());
|
|
|
|
}
|
2023-05-11 09:37:53 +03:00
|
|
|
|
|
|
|
return response()->view('errors.403', [
|
|
|
|
'message' => $message,
|
|
|
|
], $exception->getCode());
|
2023-04-24 11:02:45 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-04 11:02:25 +03:00
|
|
|
return parent::render($request, $exception);
|
2019-01-31 17:09:59 +03:00
|
|
|
}
|
2022-06-01 10:15:55 +03:00
|
|
|
|
|
|
|
protected function handleApiExceptions($request, $exception): Response
|
|
|
|
{
|
|
|
|
$replacements = $this->prepareApiReplacements($exception);
|
|
|
|
|
|
|
|
$response = config('api.error_format');
|
|
|
|
|
|
|
|
array_walk_recursive($response, function (&$value, $key) use ($replacements) {
|
|
|
|
if (Str::startsWith($value, ':') && isset($replacements[$value])) {
|
|
|
|
$value = $replacements[$value];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$response = $this->recursivelyRemoveEmptyApiReplacements($response);
|
|
|
|
|
|
|
|
return new Response($response, $this->getStatusCode($exception), $this->getHeaders($exception));
|
|
|
|
}
|
|
|
|
|
2023-05-11 09:37:53 +03:00
|
|
|
protected function handleMailerExceptions(MailerHttpTransportException $exception): string
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Couldn't access the SentMessage object to get the email address
|
|
|
|
* https://symfony.com/doc/current/mailer.html#debugging-emails
|
|
|
|
*
|
|
|
|
* https://codespeedy.com/extract-email-addresses-from-a-string-in-php
|
|
|
|
* https://phpliveregex.com/p/IMG
|
|
|
|
*/
|
|
|
|
preg_match("/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/", $exception->getMessage(), $matches);
|
|
|
|
|
|
|
|
if (empty($matches[0])) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
$email = $matches[0];
|
|
|
|
|
|
|
|
event(new InvalidEmailDetected($email, $exception->getMessage()));
|
|
|
|
|
|
|
|
return $email;
|
|
|
|
}
|
|
|
|
|
2022-06-01 10:15:55 +03:00
|
|
|
/**
|
|
|
|
* Prepare the replacements array by gathering the keys and values.
|
|
|
|
*
|
|
|
|
* @param Throwable $exception
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function prepareApiReplacements(Throwable $exception): array
|
|
|
|
{
|
|
|
|
$code = $this->getStatusCode($exception);
|
|
|
|
|
|
|
|
if (! $message = $exception->getMessage()) {
|
|
|
|
$message = sprintf('%d %s', $code, Response::$statusTexts[$code]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$replacements = [
|
|
|
|
':message' => $message,
|
|
|
|
':status_code' => $code,
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($exception instanceof ResourceException && $exception->hasErrors()) {
|
|
|
|
$replacements[':errors'] = $exception->getErrors();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($exception instanceof ValidationException) {
|
|
|
|
$replacements[':errors'] = $exception->errors();
|
|
|
|
$replacements[':status_code'] = $exception->status;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($code = $exception->getCode()) {
|
|
|
|
$replacements[':code'] = $code;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config('api.debug')) {
|
|
|
|
$replacements[':debug'] = [
|
|
|
|
'line' => $exception->getLine(),
|
|
|
|
'file' => $exception->getFile(),
|
|
|
|
'class' => get_class($exception),
|
|
|
|
'trace' => explode("\n", $exception->getTraceAsString()),
|
|
|
|
];
|
|
|
|
|
|
|
|
// Attach trace of previous exception, if exists
|
|
|
|
if (! is_null($exception->getPrevious())) {
|
|
|
|
$currentTrace = $replacements[':debug']['trace'];
|
|
|
|
|
|
|
|
$replacements[':debug']['trace'] = [
|
|
|
|
'previous' => explode("\n", $exception->getPrevious()->getTraceAsString()),
|
|
|
|
'current' => $currentTrace,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $replacements;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recursively remove any empty replacement values in the response array.
|
|
|
|
*
|
|
|
|
* @param array $input
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function recursivelyRemoveEmptyApiReplacements(array $input)
|
|
|
|
{
|
|
|
|
foreach ($input as &$value) {
|
|
|
|
if (is_array($value)) {
|
|
|
|
$value = $this->recursivelyRemoveEmptyApiReplacements($value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return array_filter($input, function ($value) {
|
|
|
|
if (is_string($value)) {
|
|
|
|
return ! Str::startsWith($value, ':');
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the status code from the exception.
|
|
|
|
*
|
|
|
|
* @param Throwable $exception
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
protected function getStatusCode(Throwable $exception): int
|
|
|
|
{
|
|
|
|
$code = null;
|
|
|
|
|
|
|
|
if ($exception instanceof ValidationException) {
|
|
|
|
$code = $exception->status;
|
|
|
|
} elseif ($exception instanceof HttpExceptionInterface) {
|
|
|
|
$code = $exception->getStatusCode();
|
|
|
|
} else {
|
|
|
|
// By default throw 500
|
|
|
|
$code = 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Be extra defensive
|
|
|
|
if ($code < 100 || $code > 599) {
|
|
|
|
$code = 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $code;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the headers from the exception.
|
|
|
|
*/
|
|
|
|
protected function getHeaders(Throwable $exception): array
|
|
|
|
{
|
|
|
|
return ($exception instanceof HttpExceptionInterface)
|
|
|
|
? $exception->getHeaders()
|
|
|
|
: [];
|
|
|
|
}
|
2022-07-27 14:13:58 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the real exception.
|
|
|
|
*/
|
|
|
|
protected function getRealException(Throwable $exception, string $current): Throwable
|
|
|
|
{
|
|
|
|
$previous = $exception->getPrevious() ?? $exception;
|
|
|
|
|
|
|
|
while (($previous instanceof $current) && $previous->getPrevious()) {
|
|
|
|
$previous = $previous->getPrevious();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $previous;
|
|
|
|
}
|
2017-09-14 22:21:00 +03:00
|
|
|
}
|