<?php namespace Illuminate\Foundation\Exceptions; use Exception; use Throwable; use Whoops\Run as Whoops; use Illuminate\Support\Arr; use Psr\Log\LoggerInterface; use Illuminate\Http\Response; use Illuminate\Routing\Router; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\RedirectResponse; use Whoops\Handler\PrettyPageHandler; use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Support\Responsable; use Illuminate\Session\TokenMismatchException; use Illuminate\Validation\ValidationException; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Exceptions\HttpResponseException; use Symfony\Component\Debug\Exception\FlattenException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Console\Application as ConsoleApplication; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Debug\ExceptionHandler as SymfonyExceptionHandler; use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract; use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse; class Handler implements ExceptionHandlerContract { /** * The container implementation. * * @var \Illuminate\Contracts\Container\Container */ protected $container; /** * A list of the exception types that are not reported. * * @var array */ protected $dontReport = []; /** * A list of the internal exception types that should not be reported. * * @var array */ protected $internalDontReport = [ AuthenticationException::class, AuthorizationException::class, HttpException::class, HttpResponseException::class, ModelNotFoundException::class, TokenMismatchException::class, ValidationException::class, ]; /** * A list of the inputs that are never flashed for validation exceptions. * * @var array */ protected $dontFlash = [ 'password', 'password_confirmation', ]; /** * Create a new exception handler instance. * * @param \Illuminate\Contracts\Container\Container $container * @return void */ public function __construct(Container $container) { $this->container = $container; } /** * Report or log an exception. * * @param \Exception $e * @return mixed * * @throws \Exception */ public function report(Exception $e) { if ($this->shouldntReport($e)) { return; } if (method_exists($e, 'report')) { return $e->report(); } try { $logger = $this->container->make(LoggerInterface::class); } catch (Exception $ex) { throw $e; // throw the original exception } $logger->error( $e->getMessage(), array_merge($this->context(), ['exception' => $e] )); } /** * Determine if the exception should be reported. * * @param \Exception $e * @return bool */ public function shouldReport(Exception $e) { return ! $this->shouldntReport($e); } /** * Determine if the exception is in the "do not report" list. * * @param \Exception $e * @return bool */ protected function shouldntReport(Exception $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 context variables for logging. * * @return array */ protected function context() { try { return array_filter([ 'userId' => Auth::id(), 'email' => Auth::user() ? Auth::user()->email : null, ]); } catch (Throwable $e) { return []; } } /** * Render an exception into a response. * * @param \Illuminate\Http\Request $request * @param \Exception $e * @return \Symfony\Component\HttpFoundation\Response */ public function render($request, Exception $e) { if (method_exists($e, 'render') && $response = $e->render($request)) { return Router::toResponse($request, $response); } elseif ($e instanceof Responsable) { return $e->toResponse($request); } $e = $this->prepareException($e); if ($e instanceof HttpResponseException) { return $e->getResponse(); } elseif ($e instanceof AuthenticationException) { return $this->unauthenticated($request, $e); } elseif ($e instanceof ValidationException) { return $this->convertValidationExceptionToResponse($e, $request); } return $request->expectsJson() ? $this->prepareJsonResponse($request, $e) : $this->prepareResponse($request, $e); } /** * Prepare exception for rendering. * * @param \Exception $e * @return \Exception */ protected function prepareException(Exception $e) { if ($e instanceof ModelNotFoundException) { $e = new NotFoundHttpException($e->getMessage(), $e); } elseif ($e instanceof AuthorizationException) { $e = new AccessDeniedHttpException($e->getMessage(), $e); } elseif ($e instanceof TokenMismatchException) { $e = new HttpException(419, $e->getMessage(), $e); } return $e; } /** * Convert an authentication exception into a response. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Auth\AuthenticationException $exception * @return \Illuminate\Http\Response */ protected function unauthenticated($request, AuthenticationException $exception) { return $request->expectsJson() ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->guest(route('login')); } /** * Create a response object from the given validation exception. * * @param \Illuminate\Validation\ValidationException $e * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response */ protected function convertValidationExceptionToResponse(ValidationException $e, $request) { if ($e->response) { return $e->response; } return $request->expectsJson() ? $this->invalidJson($request, $e) : $this->invalid($request, $e); } /** * Convert a validation exception into a response. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Validation\ValidationException $exception * @return \Illuminate\Http\Response */ protected function invalid($request, ValidationException $exception) { $url = $exception->redirectTo ?? url()->previous(); return redirect($url) ->withInput($request->except($this->dontFlash)) ->withErrors( $exception->errors(), $exception->errorBag ); } /** * Convert a validation exception into a JSON response. * * @param \Illuminate\Http\Request $request * @param \Illuminate\Validation\ValidationException $exception * @return \Illuminate\Http\JsonResponse */ protected function invalidJson($request, ValidationException $exception) { return response()->json([ 'message' => $exception->getMessage(), 'errors' => $exception->errors(), ], $exception->status); } /** * Prepare a response for the given exception. * * @param \Illuminate\Http\Request $request * @param \Exception $e * @return \Symfony\Component\HttpFoundation\Response */ protected function prepareResponse($request, Exception $e) { if (! $this->isHttpException($e) && config('app.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. * * @param \Exception $e * @return \Symfony\Component\HttpFoundation\Response */ protected function convertExceptionToResponse(Exception $e) { $headers = $this->isHttpException($e) ? $e->getHeaders() : []; $statusCode = $this->isHttpException($e) ? $e->getStatusCode() : 500; try { $content = config('app.debug') && class_exists(Whoops::class) ? $this->renderExceptionWithWhoops($e) : $this->renderExceptionWithSymfony($e, config('app.debug')); } catch (Exception $e) { $content = $content ?? $this->renderExceptionWithSymfony($e, config('app.debug')); } return SymfonyResponse::create( $content, $statusCode, $headers ); } /** * Render an exception to a string using "Whoops". * * @param \Exception $e * @return string */ protected function renderExceptionWithWhoops(Exception $e) { return tap(new Whoops, function ($whoops) { $whoops->pushHandler($this->whoopsHandler()); $whoops->writeToOutput(false); $whoops->allowQuit(false); } )->handleException($e); } /** * Render an exception to a string using Symfony. * * @param \Exception $e * @param bool $debug * @return string */ protected function renderExceptionWithSymfony(Exception $e, $debug) { return (new SymfonyExceptionHandler($debug))->getHtml( FlattenException::create($e) ); } /** * Get the Whoops handler for the application. * * @return \Whoops\Handler\Handler */ protected function whoopsHandler() { return tap(new PrettyPageHandler, function ($handler) { $files = new Filesystem; $handler->handleUnconditionally(true); foreach (config('app.debug_blacklist', []) as $key => $secrets) { foreach ($secrets as $secret) { $handler->blacklist($key, $secret); } } if (config('app.editor', false)) { $handler->setEditor(config('app.editor')); } $handler->setApplicationPaths( array_flip(Arr::except( array_flip($files->directories(base_path())), [base_path('vendor')] )) ); }); } /** * Render the given HttpException. * * @param \Symfony\Component\HttpKernel\Exception\HttpException $e * @return \Symfony\Component\HttpFoundation\Response */ protected function renderHttpException(HttpException $e) { $status = $e->getStatusCode(); $paths = collect(config('view.paths')); view()->replaceNamespace('errors', $paths->map(function ($path) { return "{$path}/errors"; })->push(__DIR__.'/views')->all()); if (view()->exists($view = "errors::{$status}")) { return response()->view($view, ['exception' => $e], $status, $e->getHeaders()); } return $this->convertExceptionToResponse($e); } /** * Map the given exception into an Illuminate response. * * @param \Symfony\Component\HttpFoundation\Response $response * @param \Exception $e * @return \Illuminate\Http\Response */ protected function toIlluminateResponse($response, Exception $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 * @param \Exception $e * @return \Illuminate\Http\JsonResponse */ protected function prepareJsonResponse($request, Exception $e) { $status = $this->isHttpException($e) ? $e->getStatusCode() : 500; $headers = $this->isHttpException($e) ? $e->getHeaders() : []; return new JsonResponse( $this->convertExceptionToArray($e), $status, $headers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); } /** * Convert the given exception to an array. * * @param \Exception $e * @return array */ protected function convertExceptionToArray(Exception $e) { return config('app.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(), ] : [ 'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error', ]; } /** * Render an exception to the console. * * @param \Symfony\Component\Console\Output\OutputInterface $output * @param \Exception $e * @return void */ public function renderForConsole($output, Exception $e) { (new ConsoleApplication)->renderException($e, $output); } /** * Determine if the given exception is an HTTP exception. * * @param \Exception $e * @return bool */ protected function isHttpException(Exception $e) { return $e instanceof HttpException; } }