Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Foundation\Exceptions;
4
 
5
use Closure;
6
use Exception;
7
use Illuminate\Auth\Access\AuthorizationException;
8
use Illuminate\Auth\AuthenticationException;
9
use Illuminate\Console\View\Components\BulletList;
10
use Illuminate\Console\View\Components\Error;
11
use Illuminate\Contracts\Container\Container;
12
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
13
use Illuminate\Contracts\Foundation\ExceptionRenderer;
14
use Illuminate\Contracts\Support\Responsable;
15
use Illuminate\Database\Eloquent\ModelNotFoundException;
16
use Illuminate\Database\MultipleRecordsFoundException;
17
use Illuminate\Database\RecordsNotFoundException;
18
use Illuminate\Http\Exceptions\HttpResponseException;
19
use Illuminate\Http\JsonResponse;
20
use Illuminate\Http\RedirectResponse;
21
use Illuminate\Http\Response;
22
use Illuminate\Routing\Exceptions\BackedEnumCaseNotFoundException;
23
use Illuminate\Routing\Router;
24
use Illuminate\Session\TokenMismatchException;
25
use Illuminate\Support\Arr;
26
use Illuminate\Support\Facades\Auth;
27
use Illuminate\Support\Reflector;
28
use Illuminate\Support\Traits\ReflectsClosures;
29
use Illuminate\Support\ViewErrorBag;
30
use Illuminate\Validation\ValidationException;
31
use InvalidArgumentException;
32
use Psr\Log\LoggerInterface;
33
use Psr\Log\LogLevel;
34
use Symfony\Component\Console\Application as ConsoleApplication;
35
use Symfony\Component\Console\Exception\CommandNotFoundException;
36
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
37
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
38
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
39
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
40
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
41
use Symfony\Component\HttpKernel\Exception\HttpException;
42
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
43
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
44
use Throwable;
45
 
46
class Handler implements ExceptionHandlerContract
47
{
48
    use ReflectsClosures;
49
 
50
    /**
51
     * The container implementation.
52
     *
53
     * @var \Illuminate\Contracts\Container\Container
54
     */
55
    protected $container;
56
 
57
    /**
58
     * A list of the exception types that are not reported.
59
     *
60
     * @var array<int, class-string<\Throwable>>
61
     */
62
    protected $dontReport = [];
63
 
64
    /**
65
     * The callbacks that should be used during reporting.
66
     *
67
     * @var \Illuminate\Foundation\Exceptions\ReportableHandler[]
68
     */
69
    protected $reportCallbacks = [];
70
 
71
    /**
72
     * A map of exceptions with their corresponding custom log levels.
73
     *
74
     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
75
     */
76
    protected $levels = [];
77
 
78
    /**
79
     * The callbacks that should be used during rendering.
80
     *
81
     * @var \Closure[]
82
     */
83
    protected $renderCallbacks = [];
84
 
85
    /**
86
     * The registered exception mappings.
87
     *
88
     * @var array<string, \Closure>
89
     */
90
    protected $exceptionMap = [];
91
 
92
    /**
93
     * A list of the internal exception types that should not be reported.
94
     *
95
     * @var array<int, class-string<\Throwable>>
96
     */
97
    protected $internalDontReport = [
98
        AuthenticationException::class,
99
        AuthorizationException::class,
100
        BackedEnumCaseNotFoundException::class,
101
        HttpException::class,
102
        HttpResponseException::class,
103
        ModelNotFoundException::class,
104
        MultipleRecordsFoundException::class,
105
        RecordsNotFoundException::class,
106
        SuspiciousOperationException::class,
107
        TokenMismatchException::class,
108
        ValidationException::class,
109
    ];
110
 
111
    /**
112
     * A list of the inputs that are never flashed for validation exceptions.
113
     *
114
     * @var array<int, string>
115
     */
116
    protected $dontFlash = [
117
        'current_password',
118
        'password',
119
        'password_confirmation',
120
    ];
121
 
122
    /**
123
     * Create a new exception handler instance.
124
     *
125
     * @param  \Illuminate\Contracts\Container\Container  $container
126
     * @return void
127
     */
128
    public function __construct(Container $container)
129
    {
130
        $this->container = $container;
131
 
132
        $this->register();
133
    }
134
 
135
    /**
136
     * Register the exception handling callbacks for the application.
137
     *
138
     * @return void
139
     */
140
    public function register()
141
    {
142
        //
143
    }
144
 
145
    /**
146
     * Register a reportable callback.
147
     *
148
     * @param  callable  $reportUsing
149
     * @return \Illuminate\Foundation\Exceptions\ReportableHandler
150
     */
151
    public function reportable(callable $reportUsing)
152
    {
153
        if (! $reportUsing instanceof Closure) {
154
            $reportUsing = Closure::fromCallable($reportUsing);
155
        }
156
 
157
        return tap(new ReportableHandler($reportUsing), function ($callback) {
158
            $this->reportCallbacks[] = $callback;
159
        });
160
    }
161
 
162
    /**
163
     * Register a renderable callback.
164
     *
165
     * @param  callable  $renderUsing
166
     * @return $this
167
     */
168
    public function renderable(callable $renderUsing)
169
    {
170
        if (! $renderUsing instanceof Closure) {
171
            $renderUsing = Closure::fromCallable($renderUsing);
172
        }
173
 
174
        $this->renderCallbacks[] = $renderUsing;
175
 
176
        return $this;
177
    }
178
 
179
    /**
180
     * Register a new exception mapping.
181
     *
182
     * @param  \Closure|string  $from
183
     * @param  \Closure|string|null  $to
184
     * @return $this
185
     *
186
     * @throws \InvalidArgumentException
187
     */
188
    public function map($from, $to = null)
189
    {
190
        if (is_string($to)) {
191
            $to = fn ($exception) => new $to('', 0, $exception);
192
        }
193
 
194
        if (is_callable($from) && is_null($to)) {
195
            $from = $this->firstClosureParameterType($to = $from);
196
        }
197
 
198
        if (! is_string($from) || ! $to instanceof Closure) {
199
            throw new InvalidArgumentException('Invalid exception mapping.');
200
        }
201
 
202
        $this->exceptionMap[$from] = $to;
203
 
204
        return $this;
205
    }
206
 
207
    /**
208
     * Indicate that the given exception type should not be reported.
209
     *
210
     * @param  string  $class
211
     * @return $this
212
     */
213
    public function ignore(string $class)
214
    {
215
        $this->dontReport[] = $class;
216
 
217
        return $this;
218
    }
219
 
220
    /**
221
     * Set the log level for the given exception type.
222
     *
223
     * @param  class-string<\Throwable>  $type
224
     * @param  \Psr\Log\LogLevel::*  $level
225
     * @return $this
226
     */
227
    public function level($type, $level)
228
    {
229
        $this->levels[$type] = $level;
230
 
231
        return $this;
232
    }
233
 
234
    /**
235
     * Report or log an exception.
236
     *
237
     * @param  \Throwable  $e
238
     * @return void
239
     *
240
     * @throws \Throwable
241
     */
242
    public function report(Throwable $e)
243
    {
244
        $e = $this->mapException($e);
245
 
246
        if ($this->shouldntReport($e)) {
247
            return;
248
        }
249
 
250
        if (Reflector::isCallable($reportCallable = [$e, 'report']) &&
251
            $this->container->call($reportCallable) !== false) {
252
            return;
253
        }
254
 
255
        foreach ($this->reportCallbacks as $reportCallback) {
256
            if ($reportCallback->handles($e) && $reportCallback($e) === false) {
257
                return;
258
            }
259
        }
260
 
261
        try {
262
            $logger = $this->container->make(LoggerInterface::class);
263
        } catch (Exception $ex) {
264
            throw $e;
265
        }
266
 
267
        $level = Arr::first(
268
            $this->levels, fn ($level, $type) => $e instanceof $type, LogLevel::ERROR
269
        );
270
 
271
        $context = $this->buildExceptionContext($e);
272
 
273
        method_exists($logger, $level)
274
            ? $logger->{$level}($e->getMessage(), $context)
275
            : $logger->log($level, $e->getMessage(), $context);
276
    }
277
 
278
    /**
279
     * Determine if the exception should be reported.
280
     *
281
     * @param  \Throwable  $e
282
     * @return bool
283
     */
284
    public function shouldReport(Throwable $e)
285
    {
286
        return ! $this->shouldntReport($e);
287
    }
288
 
289
    /**
290
     * Determine if the exception is in the "do not report" list.
291
     *
292
     * @param  \Throwable  $e
293
     * @return bool
294
     */
295
    protected function shouldntReport(Throwable $e)
296
    {
297
        $dontReport = array_merge($this->dontReport, $this->internalDontReport);
298
 
299
        return ! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type));
300
    }
301
 
302
    /**
303
     * Create the context array for logging the given exception.
304
     *
305
     * @param  \Throwable  $e
306
     * @return array
307
     */
308
    protected function buildExceptionContext(Throwable $e)
309
    {
310
        return array_merge(
311
            $this->exceptionContext($e),
312
            $this->context(),
313
            ['exception' => $e]
314
        );
315
    }
316
 
317
    /**
318
     * Get the default exception context variables for logging.
319
     *
320
     * @param  \Throwable  $e
321
     * @return array
322
     */
323
    protected function exceptionContext(Throwable $e)
324
    {
325
        if (method_exists($e, 'context')) {
326
            return $e->context();
327
        }
328
 
329
        return [];
330
    }
331
 
332
    /**
333
     * Get the default context variables for logging.
334
     *
335
     * @return array
336
     */
337
    protected function context()
338
    {
339
        try {
340
            return array_filter([
341
                'userId' => Auth::id(),
342
            ]);
343
        } catch (Throwable $e) {
344
            return [];
345
        }
346
    }
347
 
348
    /**
349
     * Render an exception into an HTTP response.
350
     *
351
     * @param  \Illuminate\Http\Request  $request
352
     * @param  \Throwable  $e
353
     * @return \Symfony\Component\HttpFoundation\Response
354
     *
355
     * @throws \Throwable
356
     */
357
    public function render($request, Throwable $e)
358
    {
359
        if (method_exists($e, 'render') && $response = $e->render($request)) {
360
            return Router::toResponse($request, $response);
361
        }
362
 
363
        if ($e instanceof Responsable) {
364
            return $e->toResponse($request);
365
        }
366
 
367
        $e = $this->prepareException($this->mapException($e));
368
 
369
        if ($response = $this->renderViaCallbacks($request, $e)) {
370
            return $response;
371
        }
372
 
373
        return match (true) {
374
            $e instanceof HttpResponseException => $e->getResponse(),
375
            $e instanceof AuthenticationException => $this->unauthenticated($request, $e),
376
            $e instanceof ValidationException => $this->convertValidationExceptionToResponse($e, $request),
377
            default => $this->renderExceptionResponse($request, $e),
378
        };
379
    }
380
 
381
    /**
382
     * Prepare exception for rendering.
383
     *
384
     * @param  \Throwable  $e
385
     * @return \Throwable
386
     */
387
    protected function prepareException(Throwable $e)
388
    {
389
        return match (true) {
390
            $e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
391
            $e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
392
            $e instanceof AuthorizationException && $e->hasStatus() => new HttpException(
393
                $e->status(), $e->response()?->message() ?: (Response::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e
394
            ),
395
            $e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e),
396
            $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),
397
            $e instanceof SuspiciousOperationException => new NotFoundHttpException('Bad hostname provided.', $e),
398
            $e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e),
399
            default => $e,
400
        };
401
    }
402
 
403
    /**
404
     * Map the exception using a registered mapper if possible.
405
     *
406
     * @param  \Throwable  $e
407
     * @return \Throwable
408
     */
409
    protected function mapException(Throwable $e)
410
    {
411
        if (method_exists($e, 'getInnerException') &&
412
            ($inner = $e->getInnerException()) instanceof Throwable) {
413
            return $inner;
414
        }
415
 
416
        foreach ($this->exceptionMap as $class => $mapper) {
417
            if (is_a($e, $class)) {
418
                return $mapper($e);
419
            }
420
        }
421
 
422
        return $e;
423
    }
424
 
425
    /**
426
     * Try to render a response from request and exception via render callbacks.
427
     *
428
     * @param  \Illuminate\Http\Request  $request
429
     * @param  \Throwable  $e
430
     * @return mixed
431
     *
432
     * @throws \ReflectionException
433
     */
434
    protected function renderViaCallbacks($request, Throwable $e)
435
    {
436
        foreach ($this->renderCallbacks as $renderCallback) {
437
            foreach ($this->firstClosureParameterTypes($renderCallback) as $type) {
438
                if (is_a($e, $type)) {
439
                    $response = $renderCallback($e, $request);
440
 
441
                    if (! is_null($response)) {
442
                        return $response;
443
                    }
444
                }
445
            }
446
        }
447
    }
448
 
449
    /**
450
     * Render a default exception response if any.
451
     *
452
     * @param  \Illuminate\Http\Request  $request
453
     * @param  \Throwable  $e
454
     * @return \Symfony\Component\HttpFoundation\Response
455
     */
456
    protected function renderExceptionResponse($request, Throwable $e)
457
    {
458
        return $this->shouldReturnJson($request, $e)
459
                    ? $this->prepareJsonResponse($request, $e)
460
                    : $this->prepareResponse($request, $e);
461
    }
462
 
463
    /**
464
     * Convert an authentication exception into a response.
465
     *
466
     * @param  \Illuminate\Http\Request  $request
467
     * @param  \Illuminate\Auth\AuthenticationException  $exception
468
     * @return \Symfony\Component\HttpFoundation\Response
469
     */
470
    protected function unauthenticated($request, AuthenticationException $exception)
471
    {
472
        return $this->shouldReturnJson($request, $exception)
473
                    ? response()->json(['message' => $exception->getMessage()], 401)
474
                    : redirect()->guest($exception->redirectTo() ?? route('login'));
475
    }
476
 
477
    /**
478
     * Create a response object from the given validation exception.
479
     *
480
     * @param  \Illuminate\Validation\ValidationException  $e
481
     * @param  \Illuminate\Http\Request  $request
482
     * @return \Symfony\Component\HttpFoundation\Response
483
     */
484
    protected function convertValidationExceptionToResponse(ValidationException $e, $request)
485
    {
486
        if ($e->response) {
487
            return $e->response;
488
        }
489
 
490
        return $this->shouldReturnJson($request, $e)
491
                    ? $this->invalidJson($request, $e)
492
                    : $this->invalid($request, $e);
493
    }
494
 
495
    /**
496
     * Convert a validation exception into a response.
497
     *
498
     * @param  \Illuminate\Http\Request  $request
499
     * @param  \Illuminate\Validation\ValidationException  $exception
500
     * @return \Illuminate\Http\Response
501
     */
502
    protected function invalid($request, ValidationException $exception)
503
    {
504
        return redirect($exception->redirectTo ?? url()->previous())
505
                    ->withInput(Arr::except($request->input(), $this->dontFlash))
506
                    ->withErrors($exception->errors(), $request->input('_error_bag', $exception->errorBag));
507
    }
508
 
509
    /**
510
     * Convert a validation exception into a JSON response.
511
     *
512
     * @param  \Illuminate\Http\Request  $request
513
     * @param  \Illuminate\Validation\ValidationException  $exception
514
     * @return \Illuminate\Http\JsonResponse
515
     */
516
    protected function invalidJson($request, ValidationException $exception)
517
    {
518
        return response()->json([
519
            'message' => $exception->getMessage(),
520
            'errors' => $exception->errors(),
521
        ], $exception->status);
522
    }
523
 
524
    /**
525
     * Determine if the exception handler response should be JSON.
526
     *
527
     * @param  \Illuminate\Http\Request  $request
528
     * @param  \Throwable  $e
529
     * @return bool
530
     */
531
    protected function shouldReturnJson($request, Throwable $e)
532
    {
533
        return $request->expectsJson();
534
    }
535
 
536
    /**
537
     * Prepare a response for the given exception.
538
     *
539
     * @param  \Illuminate\Http\Request  $request
540
     * @param  \Throwable  $e
541
     * @return \Symfony\Component\HttpFoundation\Response
542
     */
543
    protected function prepareResponse($request, Throwable $e)
544
    {
545
        if (! $this->isHttpException($e) && config('app.debug')) {
546
            return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e)->prepare($request);
547
        }
548
 
549
        if (! $this->isHttpException($e)) {
550
            $e = new HttpException(500, $e->getMessage(), $e);
551
        }
552
 
553
        return $this->toIlluminateResponse(
554
            $this->renderHttpException($e), $e
555
        )->prepare($request);
556
    }
557
 
558
    /**
559
     * Create a Symfony response for the given exception.
560
     *
561
     * @param  \Throwable  $e
562
     * @return \Symfony\Component\HttpFoundation\Response
563
     */
564
    protected function convertExceptionToResponse(Throwable $e)
565
    {
566
        return new SymfonyResponse(
567
            $this->renderExceptionContent($e),
568
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
569
            $this->isHttpException($e) ? $e->getHeaders() : []
570
        );
571
    }
572
 
573
    /**
574
     * Get the response content for the given exception.
575
     *
576
     * @param  \Throwable  $e
577
     * @return string
578
     */
579
    protected function renderExceptionContent(Throwable $e)
580
    {
581
        try {
582
            return config('app.debug') && app()->has(ExceptionRenderer::class)
583
                        ? $this->renderExceptionWithCustomRenderer($e)
584
                        : $this->renderExceptionWithSymfony($e, config('app.debug'));
585
        } catch (Throwable $e) {
586
            return $this->renderExceptionWithSymfony($e, config('app.debug'));
587
        }
588
    }
589
 
590
    /**
591
     * Render an exception to a string using the registered `ExceptionRenderer`.
592
     *
593
     * @param  \Throwable  $e
594
     * @return string
595
     */
596
    protected function renderExceptionWithCustomRenderer(Throwable $e)
597
    {
598
        return app(ExceptionRenderer::class)->render($e);
599
    }
600
 
601
    /**
602
     * Render an exception to a string using Symfony.
603
     *
604
     * @param  \Throwable  $e
605
     * @param  bool  $debug
606
     * @return string
607
     */
608
    protected function renderExceptionWithSymfony(Throwable $e, $debug)
609
    {
610
        $renderer = new HtmlErrorRenderer($debug);
611
 
612
        return $renderer->render($e)->getAsString();
613
    }
614
 
615
    /**
616
     * Render the given HttpException.
617
     *
618
     * @param  \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface  $e
619
     * @return \Symfony\Component\HttpFoundation\Response
620
     */
621
    protected function renderHttpException(HttpExceptionInterface $e)
622
    {
623
        $this->registerErrorViewPaths();
624
 
625
        if ($view = $this->getHttpExceptionView($e)) {
626
            return response()->view($view, [
627
                'errors' => new ViewErrorBag,
628
                'exception' => $e,
629
            ], $e->getStatusCode(), $e->getHeaders());
630
        }
631
 
632
        return $this->convertExceptionToResponse($e);
633
    }
634
 
635
    /**
636
     * Register the error template hint paths.
637
     *
638
     * @return void
639
     */
640
    protected function registerErrorViewPaths()
641
    {
642
        (new RegisterErrorViewPaths)();
643
    }
644
 
645
    /**
646
     * Get the view used to render HTTP exceptions.
647
     *
648
     * @param  \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface  $e
649
     * @return string|null
650
     */
651
    protected function getHttpExceptionView(HttpExceptionInterface $e)
652
    {
653
        $view = 'errors::'.$e->getStatusCode();
654
 
655
        if (view()->exists($view)) {
656
            return $view;
657
        }
658
 
659
        $view = substr($view, 0, -2).'xx';
660
 
661
        if (view()->exists($view)) {
662
            return $view;
663
        }
664
 
665
        return null;
666
    }
667
 
668
    /**
669
     * Map the given exception into an Illuminate response.
670
     *
671
     * @param  \Symfony\Component\HttpFoundation\Response  $response
672
     * @param  \Throwable  $e
673
     * @return \Illuminate\Http\Response
674
     */
675
    protected function toIlluminateResponse($response, Throwable $e)
676
    {
677
        if ($response instanceof SymfonyRedirectResponse) {
678
            $response = new RedirectResponse(
679
                $response->getTargetUrl(), $response->getStatusCode(), $response->headers->all()
680
            );
681
        } else {
682
            $response = new Response(
683
                $response->getContent(), $response->getStatusCode(), $response->headers->all()
684
            );
685
        }
686
 
687
        return $response->withException($e);
688
    }
689
 
690
    /**
691
     * Prepare a JSON response for the given exception.
692
     *
693
     * @param  \Illuminate\Http\Request  $request
694
     * @param  \Throwable  $e
695
     * @return \Illuminate\Http\JsonResponse
696
     */
697
    protected function prepareJsonResponse($request, Throwable $e)
698
    {
699
        return new JsonResponse(
700
            $this->convertExceptionToArray($e),
701
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
702
            $this->isHttpException($e) ? $e->getHeaders() : [],
703
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
704
        );
705
    }
706
 
707
    /**
708
     * Convert the given exception to an array.
709
     *
710
     * @param  \Throwable  $e
711
     * @return array
712
     */
713
    protected function convertExceptionToArray(Throwable $e)
714
    {
715
        return config('app.debug') ? [
716
            'message' => $e->getMessage(),
717
            'exception' => get_class($e),
718
            'file' => $e->getFile(),
719
            'line' => $e->getLine(),
720
            'trace' => collect($e->getTrace())->map(fn ($trace) => Arr::except($trace, ['args']))->all(),
721
        ] : [
722
            'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
723
        ];
724
    }
725
 
726
    /**
727
     * Render an exception to the console.
728
     *
729
     * @param  \Symfony\Component\Console\Output\OutputInterface  $output
730
     * @param  \Throwable  $e
731
     * @return void
732
     *
733
     * @internal This method is not meant to be used or overwritten outside the framework.
734
     */
735
    public function renderForConsole($output, Throwable $e)
736
    {
737
        if ($e instanceof CommandNotFoundException) {
738
            $message = str($e->getMessage())->explode('.')->first();
739
 
740
            if (! empty($alternatives = $e->getAlternatives())) {
741
                $message .= '. Did you mean one of these?';
742
 
743
                with(new Error($output))->render($message);
744
                with(new BulletList($output))->render($e->getAlternatives());
745
 
746
                $output->writeln('');
747
            } else {
748
                with(new Error($output))->render($message);
749
            }
750
 
751
            return;
752
        }
753
 
754
        (new ConsoleApplication)->renderThrowable($e, $output);
755
    }
756
 
757
    /**
758
     * Determine if the given exception is an HTTP exception.
759
     *
760
     * @param  \Throwable  $e
761
     * @return bool
762
     */
763
    protected function isHttpException(Throwable $e)
764
    {
765
        return $e instanceof HttpExceptionInterface;
766
    }
767
}