xref: /webtrees/app/Http/Middleware/HandleExceptions.php (revision 3976b4703df669696105ed6b024b96d433c8fbdb)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17declare(strict_types=1);
18
19namespace Fisharebest\Webtrees\Http\Middleware;
20
21use Fig\Http\Message\RequestMethodInterface;
22use Fig\Http\Message\StatusCodeInterface;
23use Fisharebest\Webtrees\Http\ViewResponseTrait;
24use Fisharebest\Webtrees\Log;
25use Psr\Http\Message\ResponseInterface;
26use Psr\Http\Message\ServerRequestInterface;
27use Psr\Http\Server\MiddlewareInterface;
28use Psr\Http\Server\RequestHandlerInterface;
29use Symfony\Component\HttpKernel\Exception\HttpException;
30use Throwable;
31
32use function dirname;
33use function ob_get_level;
34use function response;
35use function str_replace;
36use function view;
37
38use const PHP_EOL;
39
40/**
41 * Middleware to handle and render errors.
42 */
43class HandleExceptions implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface
44{
45    use ViewResponseTrait;
46
47    /**
48     * @param ServerRequestInterface  $request
49     * @param RequestHandlerInterface $handler
50     *
51     * @return ResponseInterface
52     * @throws Throwable
53     */
54    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
55    {
56        try {
57            try {
58                return $handler->handle($request);
59            } catch (HttpException $exception) {
60                $original_exception = $exception;
61
62                return $this->httpExceptionResponse($request, $exception);
63            } catch (Throwable $exception) {
64                // Exception thrown while buffering output?
65                while (ob_get_level() > 0) {
66                    ob_get_clean();
67                }
68
69                $original_exception = $exception;
70
71                return $this->unhandledExceptionResponse($request, $exception);
72            }
73        } catch (Throwable $exception) {
74            // If we can't handle the exception, rethrow it.
75            throw $original_exception;
76        }
77    }
78
79    /**
80     * @param ServerRequestInterface $request
81     * @param HttpException          $exception
82     *
83     * @return ResponseInterface
84     */
85    private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface
86    {
87        if ($request->getHeaderLine('X-Requested-With') !== '') {
88            $this->layout = 'layouts/ajax';
89        }
90
91        return $this->viewResponse('components/alert-danger', [
92            'alert' => $exception->getMessage(),
93            'title' => $exception->getMessage(),
94        ], $exception->getStatusCode());
95    }
96
97    /**
98     * @param ServerRequestInterface $request
99     * @param Throwable              $exception
100     *
101     * @return ResponseInterface
102     */
103    private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
104    {
105        // Create a stack dump for the exception
106        $base_path = dirname(__DIR__, 3);
107        $trace     = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString();
108        $trace     = str_replace($base_path, '…', $trace);
109
110        try {
111            Log::addErrorLog($trace);
112        } catch (Throwable $exception) {
113            // Must have been a problem with the database.  Nothing we can do here.
114        }
115
116        if ($request->getHeaderLine('X-Requested-With') !== '') {
117            // If this was a GET request, then we were probably fetching HTML to display, for
118            // example a chart or tab.
119            if ($request->getMethod() === self::METHOD_GET) {
120                $status_code = self::STATUS_OK;
121            } else {
122                $status_code = self::STATUS_INTERNAL_SERVER_ERROR;
123            }
124
125            return response(view('components/alert-danger', ['alert' => $trace]), $status_code);
126        }
127
128        return $this->viewResponse('errors/unhandled-exception', [
129            'title' => 'Error',
130            'error' => $trace,
131        ], self::STATUS_INTERNAL_SERVER_ERROR);
132    }
133}
134