xref: /webtrees/app/Http/Middleware/HandleExceptions.php (revision 21bbc77713ed863881cf331fd2cd1edbe3ddfd82)
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 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Http\Middleware;
21
22use Fig\Http\Message\RequestMethodInterface;
23use Fig\Http\Message\StatusCodeInterface;
24use Fisharebest\Webtrees\Exceptions\HttpException;
25use Fisharebest\Webtrees\Http\ViewResponseTrait;
26use Fisharebest\Webtrees\Log;
27use Fisharebest\Webtrees\Services\TreeService;
28use Fisharebest\Webtrees\Site;
29use League\Flysystem\NotSupportedException;
30use Psr\Http\Message\ResponseInterface;
31use Psr\Http\Message\ServerRequestInterface;
32use Psr\Http\Server\MiddlewareInterface;
33use Psr\Http\Server\RequestHandlerInterface;
34use Throwable;
35
36use function app;
37use function dirname;
38use function error_get_last;
39use function ini_get;
40use function ob_end_clean;
41use function ob_get_level;
42use function register_shutdown_function;
43use function response;
44use function str_replace;
45use function strpos;
46use function view;
47
48use const E_ERROR;
49use const PHP_EOL;
50
51/**
52 * Middleware to handle and render errors.
53 */
54class HandleExceptions implements MiddlewareInterface, StatusCodeInterface
55{
56    use ViewResponseTrait;
57
58    /** @var TreeService */
59    private $tree_service;
60
61    /**
62     * HandleExceptions constructor.
63     *
64     * @param TreeService $tree_service
65     */
66    public function __construct(TreeService $tree_service)
67    {
68        $this->tree_service = $tree_service;
69    }
70
71    /**
72     * @param ServerRequestInterface  $request
73     * @param RequestHandlerInterface $handler
74     *
75     * @return ResponseInterface
76     * @throws Throwable
77     */
78    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
79    {
80        // Fatal errors.  We may be out of memory, so do not create any variables.
81        register_shutdown_function(static function (): void {
82            if (error_get_last() !== null && error_get_last()['type'] & E_ERROR) {
83                // If PHP does not display the error, then we must display it.
84                if (ini_get('display_errors') !== '1') {
85                    echo error_get_last()['message'], '<br><br>', error_get_last()['file'] , ': ', error_get_last()['line'];
86                }
87                // Not our fault?
88                if (strpos(error_get_last()['file'], '/modules_v4/') !== false) {
89                    echo '<br><br>This is an error in a webtrees module.  Upgrade it or disable it.';
90                }
91            }
92        });
93
94        try {
95            return $handler->handle($request);
96        } catch (HttpException $exception) {
97            // The router added the tree attribute to the request, and we need it for the error response.
98            $request = app(ServerRequestInterface::class) ?? $request;
99
100            return $this->httpExceptionResponse($request, $exception);
101        } catch (NotSupportedException $exception) {
102            // The router added the tree attribute to the request, and we need it for the error response.
103            $request = app(ServerRequestInterface::class) ?? $request;
104
105            return $this->thirdPartyExceptionResponse($request, $exception);
106        } catch (Throwable $exception) {
107            // Exception thrown while buffering output?
108            while (ob_get_level() > 0) {
109                ob_end_clean();
110            }
111
112            // The Router middleware may have added a tree attribute to the request.
113            // This might be usable in the error page.
114            if (app()->has(ServerRequestInterface::class)) {
115                $request = app(ServerRequestInterface::class) ?? $request;
116            }
117
118            // Show the exception in a standard webtrees page (if we can).
119            try {
120                return $this->unhandledExceptionResponse($request, $exception);
121            } catch (Throwable $e) {
122                // That didn't work.  Try something else.
123            }
124
125            // Show the exception in a tree-less webtrees page (if we can).
126            try {
127                $request = $request->withAttribute('tree', null);
128
129                return $this->unhandledExceptionResponse($request, $exception);
130            } catch (Throwable $e) {
131                // That didn't work.  Try something else.
132            }
133
134            // Show the exception in an error page (if we can).
135            try {
136                $this->layout = 'layouts/error';
137
138                return $this->unhandledExceptionResponse($request, $exception);
139            } catch (Throwable $e) {
140                // That didn't work.  Try something else.
141            }
142
143            // Show a stack dump.
144            return response((string) $exception, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
145        }
146    }
147
148    /**
149     * @param ServerRequestInterface $request
150     * @param HttpException          $exception
151     *
152     * @return ResponseInterface
153     */
154    private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface
155    {
156        $tree = $request->getAttribute('tree');
157
158        $default = Site::getPreference('DEFAULT_GEDCOM');
159        $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
160
161        $status_code = $exception->getCode();
162
163        // If this was a GET request, then we were probably fetching HTML to display, for
164        // example a chart or tab.
165        if (
166            $request->getHeaderLine('X-Requested-With') !== '' &&
167            $request->getMethod() === RequestMethodInterface::METHOD_GET
168        ) {
169            $this->layout = 'layouts/ajax';
170            $status_code = StatusCodeInterface::STATUS_OK;
171        }
172
173        return $this->viewResponse('components/alert-danger', [
174            'alert' => $exception->getMessage(),
175            'title' => $exception->getMessage(),
176            'tree'  => $tree,
177        ], $status_code);
178    }
179
180    /**
181     * @param ServerRequestInterface $request
182     * @param Throwable              $exception
183     *
184     * @return ResponseInterface
185     */
186    private function thirdPartyExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
187    {
188        $tree = $request->getAttribute('tree');
189
190        $default = Site::getPreference('DEFAULT_GEDCOM');
191        $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
192
193        if ($request->getHeaderLine('X-Requested-With') !== '') {
194            $this->layout = 'layouts/ajax';
195        }
196
197        return $this->viewResponse('components/alert-danger', [
198            'alert' => $exception->getMessage(),
199            'title' => $exception->getMessage(),
200            'tree'  => $tree,
201        ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
202    }
203
204    /**
205     * @param ServerRequestInterface $request
206     * @param Throwable              $exception
207     *
208     * @return ResponseInterface
209     */
210    private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
211    {
212        $this->layout = 'layouts/default';
213
214        // Create a stack dump for the exception
215        $base_path = dirname(__DIR__, 3);
216        $trace     = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString();
217        $trace     = str_replace($base_path, '…', $trace);
218        // User data may contain non UTF-8 characters.
219        $trace     = mb_convert_encoding($trace, 'UTF-8', 'UTF-8');
220        $trace     = e($trace);
221        $trace     = preg_replace('/^.*modules_v4.*$/m', '<b>$0</b>', $trace);
222
223        try {
224            Log::addErrorLog($trace);
225        } catch (Throwable $exception) {
226            // Must have been a problem with the database.  Nothing we can do here.
227        }
228
229        if ($request->getHeaderLine('X-Requested-With') !== '') {
230            // If this was a GET request, then we were probably fetching HTML to display, for
231            // example a chart or tab.
232            if ($request->getMethod() === RequestMethodInterface::METHOD_GET) {
233                $status_code = StatusCodeInterface::STATUS_OK;
234            } else {
235                $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR;
236            }
237
238            return response(view('components/alert-danger', ['alert' => $trace]), $status_code);
239        }
240
241        try {
242            // Try with a full header/menu
243            return $this->viewResponse('errors/unhandled-exception', [
244                'title'   => 'Error',
245                'error'   => $trace,
246                'request' => $request,
247                'tree'    => $request->getAttribute('tree'),
248            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
249        } catch (Throwable $ex) {
250            // Try with a minimal header/menu
251            return $this->viewResponse('errors/unhandled-exception', [
252                'title'   => 'Error',
253                'error'   => $trace,
254                'request' => $request,
255                'tree'    => null,
256            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
257        }
258    }
259}
260