OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
592
app/Core/Exceptions/ExceptionHandler.php
Normal file
592
app/Core/Exceptions/ExceptionHandler.php
Normal file
@@ -0,0 +1,592 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Reflector;
|
||||
use Illuminate\Support\Traits\ReflectsClosures;
|
||||
use InvalidArgumentException;
|
||||
use Leantime\Core\Application;
|
||||
use Leantime\Core\Exceptions\Contracts\LeantimeExceptionInterface;
|
||||
use Leantime\Core\Http\ApiRequest;
|
||||
use Leantime\Core\UI\Template;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Symfony\Component\Console\Application as ConsoleApplication;
|
||||
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
|
||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Throwable;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
use Whoops\Run as Whoops;
|
||||
|
||||
class ExceptionHandler implements ExceptionHandlerContract
|
||||
{
|
||||
use ReflectsClosures;
|
||||
|
||||
/**
|
||||
* The container implementation.
|
||||
*/
|
||||
protected Application $container;
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $dontReport = [];
|
||||
|
||||
/**
|
||||
* The callbacks that should be used during reporting.
|
||||
*
|
||||
* @var ReportableHandler[]
|
||||
*/
|
||||
protected $reportCallbacks = [];
|
||||
|
||||
/**
|
||||
* The callbacks that should be used during rendering.
|
||||
*
|
||||
* @var \Closure[]
|
||||
*/
|
||||
protected $renderCallbacks = [];
|
||||
|
||||
/**
|
||||
* The registered exception mappings.
|
||||
*
|
||||
* @var array<string, \Closure>
|
||||
*/
|
||||
protected $exceptionMap = [];
|
||||
|
||||
/**
|
||||
* A list of the internal exception types that should not be reported.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $internalDontReport = [
|
||||
HttpException::class,
|
||||
HttpResponseException::class,
|
||||
SuspiciousOperationException::class,
|
||||
TokenMismatchException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new exception handler instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Application $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
$this->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a reportable callback.
|
||||
*
|
||||
* @return \Leantime\Core\Exceptions\ReportableHandler
|
||||
*/
|
||||
public function reportable(callable $reportUsing)
|
||||
{
|
||||
if (! $reportUsing instanceof Closure) {
|
||||
$reportUsing = Closure::fromCallable($reportUsing);
|
||||
}
|
||||
|
||||
return tap(new ReportableHandler($reportUsing), function ($callback) {
|
||||
$this->reportCallbacks[] = $callback;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a renderable callback.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function renderable(callable $renderUsing)
|
||||
{
|
||||
if (! $renderUsing instanceof Closure) {
|
||||
$renderUsing = Closure::fromCallable($renderUsing);
|
||||
}
|
||||
|
||||
$this->renderCallbacks[] = $renderUsing;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new exception mapping.
|
||||
*
|
||||
* @param \Closure|string $from
|
||||
* @param \Closure|string|null $to
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function map($from, $to = null)
|
||||
{
|
||||
if (is_string($to)) {
|
||||
$to = function ($exception) use ($to) {
|
||||
return new $to('', 0, $exception);
|
||||
};
|
||||
}
|
||||
|
||||
if (is_callable($from) && is_null($to)) {
|
||||
$from = $this->firstClosureParameterType($to = $from);
|
||||
}
|
||||
|
||||
if (! is_string($from) || ! $to instanceof Closure) {
|
||||
throw new InvalidArgumentException('Invalid exception mapping.');
|
||||
}
|
||||
|
||||
$this->exceptionMap[$from] = $to;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the given exception type should not be reported.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function ignore(string $class)
|
||||
{
|
||||
$this->dontReport[] = $class;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function report(Throwable $e)
|
||||
{
|
||||
$e = $this->mapException($e);
|
||||
|
||||
if ($this->shouldntReport($e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Reflector::isCallable($reportCallable = [$e, 'report'])) {
|
||||
if ($this->container->call($reportCallable) !== false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->reportCallbacks as $reportCallback) {
|
||||
if ($reportCallback->handles($e)) {
|
||||
if ($reportCallback($e) === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$logger = app(LoggerInterface::class);
|
||||
} catch (Exception $ex) {
|
||||
throw $e; // throw the original exception
|
||||
}
|
||||
|
||||
$logger->error($e->getMessage(), ['exception' => $e]);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the exception should be reported.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldReport(Throwable $e)
|
||||
{
|
||||
return ! $this->shouldntReport($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the exception is in the "do not report" list.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldntReport(Throwable $e)
|
||||
{
|
||||
$dontReport = array_merge($this->dontReport, $this->internalDontReport);
|
||||
|
||||
return ! is_null(Arr::first($dontReport, function ($type) use ($e) {
|
||||
return $e instanceof $type;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default exception context variables for logging.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function exceptionContext(Throwable $e)
|
||||
{
|
||||
if (method_exists($e, 'context')) {
|
||||
return $e->context();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default context variables for logging.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function context()
|
||||
{
|
||||
try {
|
||||
return array_filter([
|
||||
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
if (method_exists($e, 'render') && $response = $e->render($request)) {
|
||||
return $response;
|
||||
} elseif ($e instanceof Responsable) {
|
||||
return $e->toResponse($request);
|
||||
}
|
||||
|
||||
$e = $this->prepareException($this->mapException($e));
|
||||
|
||||
foreach ($this->renderCallbacks as $renderCallback) {
|
||||
foreach ($this->firstClosureParameterTypes($renderCallback) as $type) {
|
||||
if (is_a($e, $type)) {
|
||||
$response = $renderCallback($e, $request);
|
||||
|
||||
if (! is_null($response)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($e instanceof HttpResponseException) {
|
||||
return $e->getResponse();
|
||||
}
|
||||
|
||||
return $this->shouldReturnJson($request, $e)
|
||||
? $this->prepareJsonResponse($request, $e)
|
||||
: $this->prepareResponse($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the exception using a registered mapper if possible.
|
||||
*
|
||||
* @return \Throwable
|
||||
*/
|
||||
protected function mapException(Throwable $e)
|
||||
{
|
||||
foreach ($this->exceptionMap as $class => $mapper) {
|
||||
if (is_a($e, $class)) {
|
||||
return $mapper($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare exception for rendering.
|
||||
*
|
||||
* @return \Throwable
|
||||
*/
|
||||
protected function prepareException(Throwable $e)
|
||||
{
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the exception handler response should be JSON.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldReturnJson($request, Throwable $e)
|
||||
{
|
||||
// API requests (x-api-key / bearer) are JSON by contract even when the client omits an
|
||||
// Accept header, so they get a JSON error body instead of an HTML error page.
|
||||
return $request instanceof ApiRequest || $request->expectsJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a response for the given exception.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function prepareResponse($request, Throwable $e)
|
||||
{
|
||||
if (! $this->isHttpException($e) && config('debug')) {
|
||||
return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
|
||||
}
|
||||
|
||||
if (! $this->isHttpException($e)) {
|
||||
$e = new HttpException(500, $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->toIlluminateResponse(
|
||||
$this->renderHttpException($e),
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Symfony response for the given exception.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function convertExceptionToResponse(Throwable $e)
|
||||
{
|
||||
return new SymfonyResponse(
|
||||
$this->renderExceptionContent($e),
|
||||
$this->isHttpException($e) ? $e->getStatusCode() : 500,
|
||||
$this->isHttpException($e) ? $e->getHeaders() : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response content for the given exception.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderExceptionContent(Throwable $e)
|
||||
{
|
||||
try {
|
||||
return config('debug') && class_exists(Whoops::class)
|
||||
? $this->renderExceptionWithWhoops($e)
|
||||
: $this->renderExceptionWithSymfony($e, config('debug'));
|
||||
} catch (Exception $e) {
|
||||
return $this->renderExceptionWithSymfony($e, config('debug'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception to a string using "Whoops".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderExceptionWithWhoops(Throwable $e)
|
||||
{
|
||||
return tap(new Whoops, function ($whoops) {
|
||||
$whoops->appendHandler($this->whoopsHandler());
|
||||
|
||||
$whoops->writeToOutput(false);
|
||||
|
||||
$whoops->allowQuit(false);
|
||||
})->handleException($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Whoops handler for the application.
|
||||
*
|
||||
* @return \Whoops\Handler\HandlerInterface
|
||||
*/
|
||||
protected function whoopsHandler()
|
||||
{
|
||||
try {
|
||||
return app(HandlerInterface::class);
|
||||
} catch (BindingResolutionException $e) {
|
||||
return (new WhoopsHandler)->forDebug();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception to a string using Symfony.
|
||||
*
|
||||
* @param bool $debug
|
||||
* @return string
|
||||
*/
|
||||
protected function renderExceptionWithSymfony(Throwable $e, $debug)
|
||||
{
|
||||
$renderer = new HtmlErrorRenderer($debug);
|
||||
|
||||
return $renderer->render($e)->getAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given HttpException.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderHttpException(HttpExceptionInterface $e)
|
||||
{
|
||||
|
||||
try {
|
||||
$view = $this->getHttpExceptionView($e);
|
||||
|
||||
return app()->make(Template::class)->display($view, 'error', $e->getStatusCode());
|
||||
} catch (Throwable $e) {
|
||||
return $this->convertExceptionToResponse($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the error template hint paths.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerErrorViewPaths() {}
|
||||
|
||||
/**
|
||||
* Get the view used to render HTTP exceptions.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getHttpExceptionView(HttpExceptionInterface $e)
|
||||
{
|
||||
$status = $e->getStatusCode();
|
||||
|
||||
// Dedicated error pages exist only for these statuses. Anything else (e.g. a 422 from a
|
||||
// ValidationException or a 409 from EntityExistsException — now that typed exceptions
|
||||
// carry real HTTP statuses) falls back to the generic 500 page instead of throwing a
|
||||
// view-not-found that degrades to a raw Symfony error page.
|
||||
return in_array($status, [403, 404, 500, 501], true)
|
||||
? "errors.error{$status}"
|
||||
: 'errors.error500';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the given exception into an Illuminate response.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Response $response
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function toIlluminateResponse($response, Throwable $e)
|
||||
{
|
||||
if ($response instanceof SymfonyRedirectResponse) {
|
||||
$response = new RedirectResponse(
|
||||
$response->getTargetUrl(),
|
||||
$response->getStatusCode(),
|
||||
$response->headers->all()
|
||||
);
|
||||
} else {
|
||||
$response = new Response(
|
||||
$response->getContent(),
|
||||
$response->getStatusCode(),
|
||||
$response->headers->all()
|
||||
);
|
||||
}
|
||||
|
||||
return $response->withException($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a JSON response for the given exception.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function prepareJsonResponse($request, Throwable $e)
|
||||
{
|
||||
return new JsonResponse(
|
||||
$this->convertExceptionToArray($e),
|
||||
$this->isHttpException($e) ? $e->getStatusCode() : 500,
|
||||
$this->isHttpException($e) ? $e->getHeaders() : [],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given exception to an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertExceptionToArray(Throwable $e)
|
||||
{
|
||||
return config('debug') ? [
|
||||
'message' => $e->getMessage(),
|
||||
'exception' => get_class($e),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => collect($e->getTrace())->map(function ($trace) {
|
||||
return Arr::except($trace, ['args']);
|
||||
})->all(),
|
||||
] : [
|
||||
// Leantime exceptions expose a curated, client-safe message; fall back to the raw
|
||||
// HttpException message (or a generic string) for everything else. Mirrors the
|
||||
// JSON-RPC surface (JsonRpcErrorResponse::fromException), which also uses getClientMessage().
|
||||
'message' => $e instanceof LeantimeExceptionInterface
|
||||
? $e->getClientMessage()
|
||||
: ($this->isHttpException($e) ? $e->getMessage() : 'Server Error'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception to the console.
|
||||
*
|
||||
* @param \Symfony\Component\Console\Output\OutputInterface $output
|
||||
* @return void
|
||||
*/
|
||||
public function renderForConsole($output, Throwable $e)
|
||||
{
|
||||
(new ConsoleApplication)->renderThrowable($e, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given exception is an HTTP exception.
|
||||
*
|
||||
* @phpstan-assert-if-true \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isHttpException(Throwable $e)
|
||||
{
|
||||
return $e instanceof HttpExceptionInterface;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user