OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
29
app/Core/Exceptions/AuthException.php
Normal file
29
app/Core/Exceptions/AuthException.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see AuthorizationException} instead.
|
||||
*
|
||||
* Thin, deprecated alias of {@see AuthorizationException} (HTTP 403 / JSON-RPC -32001), kept
|
||||
* only because the AdvancedAuth plugin — and potentially external installs — still throw this
|
||||
* class name (e.g. AdvancedAuth\Listeners\CheckDomain). It carries no behaviour of its own
|
||||
* beyond preserving the legacy ($message, $code) constructor signature, so it is NOT a second
|
||||
* authorization exception — it IS an AuthorizationException.
|
||||
*/
|
||||
class AuthException extends AuthorizationException
|
||||
{
|
||||
/**
|
||||
* @param string $message The exception message.
|
||||
* @param int $code HTTP status, also exposed via getStatusCode() and getCode() (default 403).
|
||||
* @param Throwable|null $previous Previous throwable for chaining.
|
||||
*/
|
||||
public function __construct(string $message = '', int $code = 403, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $previous);
|
||||
$this->statusCode = $code;
|
||||
$this->code = $code;
|
||||
}
|
||||
}
|
||||
27
app/Core/Exceptions/AuthorizationException.php
Normal file
27
app/Core/Exceptions/AuthorizationException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Thrown when an authenticated user is not allowed to perform an action or access a resource.
|
||||
*
|
||||
* Renders as HTTP 403 on the web/REST surfaces and JSON-RPC error -32001 (an
|
||||
* implementation-defined code in the reserved -32000..-32099 range) on /api/jsonrpc.
|
||||
*
|
||||
* Services should throw this instead of returning `false`/`[]` on an authorization failure,
|
||||
* so the denial is unambiguous (today `return []`/`return false` collide with "no results"
|
||||
* and "not found").
|
||||
*/
|
||||
class AuthorizationException extends LeantimeException
|
||||
{
|
||||
protected int $statusCode = 403;
|
||||
|
||||
protected int $rpcCode = -32001;
|
||||
|
||||
public function __construct(string $message = 'You are not allowed to perform this action.', ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
48
app/Core/Exceptions/Contracts/LeantimeExceptionInterface.php
Normal file
48
app/Core/Exceptions/Contracts/LeantimeExceptionInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions\Contracts;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/**
|
||||
* Contract for Leantime's first-class domain exceptions.
|
||||
*
|
||||
* The design principle: exceptions carry *semantics*; each entry point owns *format*.
|
||||
* A single thrown exception must render correctly across all of Leantime's surfaces —
|
||||
* the JSON-RPC endpoint, the web/HTMX controllers (via the global ExceptionHandler), and
|
||||
* any future REST surface — so the exception declares what it *means* and lets each
|
||||
* surface decide how to present it:
|
||||
*
|
||||
* - getStatusCode() : the HTTP status for the web/REST surfaces. By extending Symfony's
|
||||
* HttpExceptionInterface, the global ExceptionHandler honors this with
|
||||
* no special-casing (its isHttpException() check already keys off it).
|
||||
* - getRpcCode() : the JSON-RPC 2.0 error code for the /api/jsonrpc surface
|
||||
* (JsonRpcErrorResponse::fromException reads it).
|
||||
* - getClientMessage(): a message that is safe to expose to a client. getMessage() stays
|
||||
* internal/loggable; this is the curated, user-facing sentence.
|
||||
* - getErrorData() : optional structured detail (e.g. a field => [messages] map for
|
||||
* validation), serialized into the JSON-RPC error `data` member.
|
||||
*
|
||||
* @see \Leantime\Core\Exceptions\LeantimeException The abstract base implementing this.
|
||||
* @see \Leantime\Core\Http\Responses\JsonRpcErrorResponse::fromException()
|
||||
*/
|
||||
interface LeantimeExceptionInterface extends HttpExceptionInterface
|
||||
{
|
||||
/**
|
||||
* JSON-RPC 2.0 error code for this failure (e.g. -32602 invalid params).
|
||||
*/
|
||||
public function getRpcCode(): int;
|
||||
|
||||
/**
|
||||
* A client-safe description of the failure. Distinct from getMessage(), which may
|
||||
* contain internal detail that should only be logged.
|
||||
*/
|
||||
public function getClientMessage(): string;
|
||||
|
||||
/**
|
||||
* Optional structured detail (e.g. ['field' => ['message', ...]]); empty when none.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getErrorData(): array;
|
||||
}
|
||||
26
app/Core/Exceptions/EntityExistsException.php
Normal file
26
app/Core/Exceptions/EntityExistsException.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* An entity being created already exists — a conflict (HTTP 409 / JSON-RPC -32005).
|
||||
*
|
||||
* A {@see LeantimeException} so the 409 status is honored across all surfaces.
|
||||
*/
|
||||
class EntityExistsException extends LeantimeException
|
||||
{
|
||||
protected int $rpcCode = -32005;
|
||||
|
||||
/**
|
||||
* @param string $message The exception message.
|
||||
* @param int $code HTTP status, also exposed via getStatusCode() and getCode().
|
||||
* @param Throwable|null $previous Previous throwable for chaining.
|
||||
*/
|
||||
public function __construct(string $message = '', int $code = 409, ?Throwable $previous = null)
|
||||
{
|
||||
$this->statusCode = $code;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
361
app/Core/Exceptions/HandleExceptions.php
Normal file
361
app/Core/Exceptions/HandleExceptions.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
use Illuminate\Log\LogManager;
|
||||
use Illuminate\Support\Env;
|
||||
use Monolog\Handler\NullHandler;
|
||||
use PHPUnit\Runner\ErrorHandler;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\ErrorHandler\Error\FatalError;
|
||||
use Throwable;
|
||||
|
||||
class HandleExceptions
|
||||
{
|
||||
/**
|
||||
* Reserved memory so that errors can be displayed properly on memory exhaustion.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public static $reservedMemory;
|
||||
|
||||
/**
|
||||
* The application instance.
|
||||
*
|
||||
* @var \Leantime\Core\Application|null
|
||||
*/
|
||||
protected static $app;
|
||||
|
||||
/**
|
||||
* Bootstrap the given application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function bootstrap(\Leantime\Core\Application $app)
|
||||
{
|
||||
static::$reservedMemory = str_repeat('x', 32768);
|
||||
|
||||
static::$app = $app;
|
||||
|
||||
error_reporting(-1);
|
||||
|
||||
set_error_handler($this->forwardsTo('handleError'));
|
||||
|
||||
set_exception_handler($this->forwardsTo('handleException'));
|
||||
|
||||
register_shutdown_function($this->forwardsTo('handleShutdown'));
|
||||
|
||||
if (! $app->environment('testing')) {
|
||||
ini_set('display_errors', 'Off');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report PHP deprecations, or convert PHP errors to ErrorException instances.
|
||||
*
|
||||
* @param int $level
|
||||
* @param string $message
|
||||
* @param string $file
|
||||
* @param int $line
|
||||
* @return void
|
||||
*
|
||||
* @throws \ErrorException
|
||||
*/
|
||||
public function handleError($level, $message, $file = '', $line = 0)
|
||||
{
|
||||
if ($this->isDeprecation($level)) {
|
||||
$this->handleDeprecationError($message, $file, $line, $level);
|
||||
} elseif (error_reporting() & $level) {
|
||||
throw new ErrorException($message, 0, $level, $file, $line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a deprecation to the "deprecations" logger.
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $file
|
||||
* @param int $line
|
||||
* @param int $level
|
||||
* @return void
|
||||
*/
|
||||
public function handleDeprecationError($message, $file, $line, $level = E_DEPRECATED)
|
||||
{
|
||||
if ($this->shouldIgnoreDeprecationErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$logger = static::$app->make(LogManager::class);
|
||||
} catch (Exception) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureDeprecationLoggerIsConfigured();
|
||||
|
||||
$options = static::$app['config']->get('logging.deprecations') ?? [];
|
||||
|
||||
with($logger->channel('deprecations'), function ($log) use ($message, $file, $line, $level, $options) {
|
||||
if ($options['trace'] ?? false) {
|
||||
$log->warning((string) new ErrorException($message, 0, $level, $file, $line));
|
||||
} else {
|
||||
$log->warning(sprintf('%s in %s on line %s',
|
||||
$message, $file, $line
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if deprecation errors should be ignored.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldIgnoreDeprecationErrors()
|
||||
{
|
||||
return ! class_exists(LogManager::class)
|
||||
|| ! static::$app->hasBeenBootstrapped()
|
||||
|| (static::$app->runningUnitTests() && ! Env::get('LOG_DEPRECATIONS_WHILE_TESTING'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the "deprecations" logger is configured.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureDeprecationLoggerIsConfigured()
|
||||
{
|
||||
with(static::$app['config'], function ($config) {
|
||||
if ($config->get('logging.channels.deprecations')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureNullLogDriverIsConfigured();
|
||||
|
||||
if (is_array($options = $config->get('logging.deprecations'))) {
|
||||
$driver = $options['channel'] ?? 'null';
|
||||
} else {
|
||||
$driver = $options ?? 'null';
|
||||
}
|
||||
|
||||
$config->set('logging.channels.deprecations', $config->get("logging.channels.{$driver}"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the "null" log driver is configured.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureNullLogDriverIsConfigured()
|
||||
{
|
||||
with(static::$app['config'], function ($config) {
|
||||
if ($config->get('logging.channels.null')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config->set('logging.channels.null', [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an uncaught exception from the application.
|
||||
*
|
||||
* Note: Most exceptions can be handled via the try / catch block in
|
||||
* the HTTP and Console kernels. But, fatal error exceptions must
|
||||
* be handled differently since they are not normal exceptions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleException(Throwable $e)
|
||||
{
|
||||
static::$reservedMemory = null;
|
||||
|
||||
try {
|
||||
$this->getExceptionHandler()->report($e);
|
||||
} catch (Exception) {
|
||||
$exceptionHandlerFailed = true;
|
||||
}
|
||||
|
||||
if (static::$app->runningInConsole()) {
|
||||
$this->renderForConsole($e);
|
||||
|
||||
if ($exceptionHandlerFailed ?? false) {
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
$this->renderHttpResponse($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception to the console.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function renderForConsole(Throwable $e)
|
||||
{
|
||||
$this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception as an HTTP response and send it.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function renderHttpResponse(Throwable $e)
|
||||
{
|
||||
$this->getExceptionHandler()->render(request(), $e)->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the PHP shutdown event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleShutdown()
|
||||
{
|
||||
static::$reservedMemory = null;
|
||||
|
||||
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
|
||||
$this->handleException($this->fatalErrorFromPhpError($error, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new fatal error instance from an error array.
|
||||
*
|
||||
* @param int|null $traceOffset
|
||||
* @return \Symfony\Component\ErrorHandler\Error\FatalError
|
||||
*/
|
||||
protected function fatalErrorFromPhpError(array $error, $traceOffset = null)
|
||||
{
|
||||
return new FatalError($error['message'], 0, $error, $traceOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a method call to the given method if an application instance exists.
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
protected function forwardsTo($method)
|
||||
{
|
||||
return fn (...$arguments) => static::$app
|
||||
? $this->{$method}(...$arguments)
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the error level is a deprecation.
|
||||
*
|
||||
* @param int $level
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDeprecation($level)
|
||||
{
|
||||
return in_array($level, [E_DEPRECATED, E_USER_DEPRECATED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the error type is fatal.
|
||||
*
|
||||
* @param int $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFatal($type)
|
||||
{
|
||||
return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the exception handler.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Debug\ExceptionHandler
|
||||
*/
|
||||
protected function getExceptionHandler()
|
||||
{
|
||||
return static::$app->make(ExceptionHandler::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the local application instance from memory.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated This method will be removed in a future Laravel version.
|
||||
*/
|
||||
public static function forgetApp()
|
||||
{
|
||||
static::$app = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the bootstrapper's global state.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function flushState()
|
||||
{
|
||||
if (is_null(static::$app)) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::flushHandlersState();
|
||||
|
||||
static::$app = null;
|
||||
|
||||
static::$reservedMemory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the bootstrapper's global handlers state.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function flushHandlersState()
|
||||
{
|
||||
while (true) {
|
||||
$previousHandler = set_exception_handler(static fn () => null);
|
||||
|
||||
restore_exception_handler();
|
||||
|
||||
if ($previousHandler === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
restore_exception_handler();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// @phpstan-ignore-next-line argument.type
|
||||
$previousHandler = set_error_handler(static fn () => null);
|
||||
|
||||
restore_error_handler();
|
||||
|
||||
if ($previousHandler === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if (class_exists(ErrorHandler::class)) {
|
||||
$instance = ErrorHandler::instance();
|
||||
|
||||
// The closure is rebound to $instance (PHPUnit's ErrorHandler, which has $enabled) via
|
||||
// ->call(); PHPStan analyses it in this class's scope (no $enabled) and wrongly collapses
|
||||
// it to always-false. The check is live at runtime.
|
||||
// @phpstan-ignore-next-line
|
||||
if ((fn () => $this->enabled ?? false)->call($instance)) {
|
||||
$instance->disable();
|
||||
$instance->enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/Core/Exceptions/InvalidArgumentException.php
Normal file
27
app/Core/Exceptions/InvalidArgumentException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Invalid argument supplied to an operation (HTTP 422 / JSON-RPC -32602 invalid params).
|
||||
*
|
||||
* Now a {@see LeantimeException}; for user-facing input validation prefer
|
||||
* {@see ValidationException}, which additionally carries a per-field error map.
|
||||
*/
|
||||
class InvalidArgumentException extends LeantimeException
|
||||
{
|
||||
protected int $rpcCode = -32602;
|
||||
|
||||
/**
|
||||
* @param string $message The exception message.
|
||||
* @param int $code HTTP status, also exposed via getStatusCode() and getCode().
|
||||
* @param Throwable|null $previous Previous throwable for chaining.
|
||||
*/
|
||||
public function __construct(string $message = '', int $code = 422, ?Throwable $previous = null)
|
||||
{
|
||||
$this->statusCode = $code;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
77
app/Core/Exceptions/LeantimeException.php
Normal file
77
app/Core/Exceptions/LeantimeException.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Leantime\Core\Exceptions\Contracts\LeantimeExceptionInterface;
|
||||
|
||||
/**
|
||||
* Base class for Leantime's first-class domain exceptions.
|
||||
*
|
||||
* Extends plain \Exception (so existing `catch (\Exception)` / `catch (SpecificException)`
|
||||
* sites keep working) and implements LeantimeExceptionInterface, which in turn extends
|
||||
* Symfony's HttpExceptionInterface — that single inheritance is what lets the global
|
||||
* ExceptionHandler honor getStatusCode()/getHeaders() with no handler changes.
|
||||
*
|
||||
* Subclasses set $statusCode and $rpcCode (and may carry $errorData / override
|
||||
* getClientMessage()). See LeantimeExceptionInterface for the design rationale.
|
||||
*/
|
||||
abstract class LeantimeException extends \Exception implements LeantimeExceptionInterface
|
||||
{
|
||||
/**
|
||||
* HTTP status for the web/REST surfaces.
|
||||
*/
|
||||
protected int $statusCode = 500;
|
||||
|
||||
/**
|
||||
* JSON-RPC 2.0 error code. Defaults to the spec's reserved "Internal error".
|
||||
*/
|
||||
protected int $rpcCode = -32603;
|
||||
|
||||
/**
|
||||
* Optional structured detail surfaced in the JSON-RPC error `data` member.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $errorData = [];
|
||||
|
||||
/**
|
||||
* Response headers to attach (HttpExceptionInterface contract).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $headers = [];
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getRpcCode(): int
|
||||
{
|
||||
return $this->rpcCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getErrorData(): array
|
||||
{
|
||||
return $this->errorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-safe message. Defaults to getMessage(); override to curate what a client sees.
|
||||
*/
|
||||
public function getClientMessage(): string
|
||||
{
|
||||
return $this->getMessage();
|
||||
}
|
||||
}
|
||||
28
app/Core/Exceptions/MissingParameterException.php
Normal file
28
app/Core/Exceptions/MissingParameterException.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* A required parameter was missing (HTTP 422 / JSON-RPC -32602 invalid params).
|
||||
*
|
||||
* A degenerate validation failure. Now a {@see LeantimeException}, so a service throwing
|
||||
* this during a JSON-RPC call surfaces as a proper -32602 "Invalid params" instead of a
|
||||
* generic server error.
|
||||
*/
|
||||
class MissingParameterException extends LeantimeException
|
||||
{
|
||||
protected int $rpcCode = -32602;
|
||||
|
||||
/**
|
||||
* @param string $message The exception message.
|
||||
* @param int $code HTTP status, also exposed via getStatusCode() and getCode().
|
||||
* @param Throwable|null $previous Previous throwable for chaining.
|
||||
*/
|
||||
public function __construct(string $message = '', int $code = 422, ?Throwable $previous = null)
|
||||
{
|
||||
$this->statusCode = $code;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
26
app/Core/Exceptions/NotFoundException.php
Normal file
26
app/Core/Exceptions/NotFoundException.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Thrown when a specifically requested resource does not exist.
|
||||
*
|
||||
* Renders as HTTP 404 on the web/REST surfaces and JSON-RPC error -32002 on /api/jsonrpc.
|
||||
*
|
||||
* Use this for a missing *single* requested entity (e.g. getTicket(99) where 99 is gone) —
|
||||
* NOT for an empty list/query result, which should still return `[]`. Throwing here keeps
|
||||
* "not found" distinct from "no permission" (today both collapse to `false`).
|
||||
*/
|
||||
class NotFoundException extends LeantimeException
|
||||
{
|
||||
protected int $statusCode = 404;
|
||||
|
||||
protected int $rpcCode = -32002;
|
||||
|
||||
public function __construct(string $message = 'The requested resource could not be found.', ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
79
app/Core/Exceptions/ReportableHandler.php
Normal file
79
app/Core/Exceptions/ReportableHandler.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Illuminate\Support\Traits\ReflectsClosures;
|
||||
use Throwable;
|
||||
|
||||
class ReportableHandler
|
||||
{
|
||||
use ReflectsClosures;
|
||||
|
||||
/**
|
||||
* The underlying callback.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected $callback;
|
||||
|
||||
/**
|
||||
* Indicates if reporting should stop after invoking this handler.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $shouldStop = false;
|
||||
|
||||
/**
|
||||
* Create a new reportable handler instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function __invoke(Throwable $e)
|
||||
{
|
||||
$result = call_user_func($this->callback, $e);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $this->shouldStop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the callback handles the given exception.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function handles(Throwable $e)
|
||||
{
|
||||
foreach ($this->firstClosureParameterTypes($this->callback) as $type) {
|
||||
if (is_a($e, $type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that report handling should stop after invoking this callback.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
$this->shouldStop = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
74
app/Core/Exceptions/ValidationException.php
Normal file
74
app/Core/Exceptions/ValidationException.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Illuminate\Translation\ArrayLoader;
|
||||
use Illuminate\Translation\Translator;
|
||||
use Illuminate\Validation\Factory as ValidationFactory;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Thrown when user-supplied input fails validation.
|
||||
*
|
||||
* Renders as HTTP 422 on the web/REST surfaces and JSON-RPC error -32602 ("Invalid params")
|
||||
* on /api/jsonrpc, with the per-field errors serialized into the JSON-RPC error `data` member.
|
||||
*
|
||||
* Per the agreed approach, this is a Leantime-owned type so the whole application sees ONE
|
||||
* validation exception — but services may still author rules with Laravel's Validator and let
|
||||
* the static validate() bridge run them and rethrow as this type. Field errors are carried as
|
||||
* a ['field' => ['message', ...]] map (the same shape Laravel's MessageBag::toArray() produces).
|
||||
*/
|
||||
class ValidationException extends LeantimeException
|
||||
{
|
||||
protected int $statusCode = 422;
|
||||
|
||||
protected int $rpcCode = -32602;
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, string>> $errors Field => messages map.
|
||||
*/
|
||||
public function __construct(array $errors = [], string $message = 'The given data was invalid.', ?Throwable $previous = null)
|
||||
{
|
||||
$this->errorData = $errors;
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build directly from a field => messages map.
|
||||
*
|
||||
* @param array<string, array<int, string>> $errors
|
||||
*/
|
||||
public static function withMessages(array $errors): self
|
||||
{
|
||||
return new self($errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Laravel validation rules and either return the validated data or throw this
|
||||
* exception. The bridge that lets services use Laravel's Validator while the rest of
|
||||
* the app only ever handles Leantime's ValidationException.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $rules
|
||||
* @param array<string, string> $messages
|
||||
* @return array<string, mixed> The validated subset of $data.
|
||||
*
|
||||
* @throws static
|
||||
*/
|
||||
public static function validate(array $data, array $rules, array $messages = []): array
|
||||
{
|
||||
// Leantime rebinds the container's "translator" to its own Language class, which is
|
||||
// NOT an Illuminate Translator — so the Validator facade cannot be constructed here.
|
||||
// Build a self-contained factory instead. (Wiring Leantime's i18n into validation
|
||||
// messages is future work; until then a message falls back to the rule key unless an
|
||||
// explicit override is passed in $messages.)
|
||||
$validator = (new ValidationFactory(new Translator(new ArrayLoader, 'en')))
|
||||
->make($data, $rules, $messages);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new self($validator->errors()->toArray());
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
}
|
||||
90
app/Core/Exceptions/WhoopsHandler.php
Normal file
90
app/Core/Exceptions/WhoopsHandler.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\Exceptions;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
|
||||
class WhoopsHandler
|
||||
{
|
||||
/**
|
||||
* Create a new Whoops handler for debug mode.
|
||||
*
|
||||
* @return \Whoops\Handler\PrettyPageHandler
|
||||
*/
|
||||
public function forDebug()
|
||||
{
|
||||
return tap(new PrettyPageHandler, function ($handler) {
|
||||
$handler->handleUnconditionally(true);
|
||||
|
||||
$this->registerApplicationPaths($handler)
|
||||
->registerBlacklist($handler)
|
||||
->registerEditor($handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application paths with the handler.
|
||||
*
|
||||
* @param \Whoops\Handler\PrettyPageHandler $handler
|
||||
* @return $this
|
||||
*/
|
||||
protected function registerApplicationPaths($handler)
|
||||
{
|
||||
$handler->setApplicationPaths(
|
||||
array_flip($this->directoriesExceptVendor())
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application paths except for the "vendor" directory.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function directoriesExceptVendor()
|
||||
{
|
||||
return Arr::except(
|
||||
array_flip((new Filesystem)->directories(APP_ROOT)),
|
||||
[APP_ROOT.'/vendor']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the blacklist with the handler.
|
||||
*
|
||||
* @param \Whoops\Handler\PrettyPageHandler $handler
|
||||
* @return $this
|
||||
*/
|
||||
protected function registerBlacklist($handler)
|
||||
{
|
||||
foreach (config('debug_blacklist', config('debug_hide', [])) as $key => $secrets) {
|
||||
foreach ($secrets as $secret) {
|
||||
$handler->blacklist($key, $secret);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the editor with the handler.
|
||||
*
|
||||
* @param \Whoops\Handler\PrettyPageHandler $handler
|
||||
* @return $this
|
||||
*/
|
||||
protected function registerEditor($handler)
|
||||
{
|
||||
|
||||
$editor = config('editor');
|
||||
if (config('editor', false)) {
|
||||
$handler->setEditor(config('editor'));
|
||||
} else {
|
||||
$handler->setEditor('phpstorm');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user