xref: /webtrees/app/Http/Middleware/HandleExceptions.php (revision e93a8df2f8d797005750082cc3766c0e80799688)
1cfbf56adSGreg Roach<?php
23976b470SGreg Roach
3cfbf56adSGreg Roach/**
4cfbf56adSGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6cfbf56adSGreg Roach * This program is free software: you can redistribute it and/or modify
7cfbf56adSGreg Roach * it under the terms of the GNU General Public License as published by
8cfbf56adSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9cfbf56adSGreg Roach * (at your option) any later version.
10cfbf56adSGreg Roach * This program is distributed in the hope that it will be useful,
11cfbf56adSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12cfbf56adSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13cfbf56adSGreg Roach * GNU General Public License for more details.
14cfbf56adSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16cfbf56adSGreg Roach */
17fcfa147eSGreg Roach
18cfbf56adSGreg Roachdeclare(strict_types=1);
19cfbf56adSGreg Roach
20cfbf56adSGreg Roachnamespace Fisharebest\Webtrees\Http\Middleware;
21cfbf56adSGreg Roach
22b7765f6bSGreg Roachuse Fig\Http\Message\RequestMethodInterface;
23f397d0fdSGreg Roachuse Fig\Http\Message\StatusCodeInterface;
2481b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpException;
25f397d0fdSGreg Roachuse Fisharebest\Webtrees\Http\ViewResponseTrait;
26f397d0fdSGreg Roachuse Fisharebest\Webtrees\Log;
27d35568b4SGreg Roachuse Fisharebest\Webtrees\Registry;
28040e7dbaSGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
29040e7dbaSGreg Roachuse Fisharebest\Webtrees\Site;
30b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
316d585cefSGreg Roachuse League\Flysystem\FilesystemException;
32cfbf56adSGreg Roachuse Psr\Http\Message\ResponseInterface;
33cfbf56adSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
34cfbf56adSGreg Roachuse Psr\Http\Server\MiddlewareInterface;
35cfbf56adSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
36cfbf56adSGreg Roachuse Throwable;
373976b470SGreg Roach
38f397d0fdSGreg Roachuse function dirname;
39837498afSGreg Roachuse function error_get_last;
40837498afSGreg Roachuse function ini_get;
41*d5a67eccSGreg Roachuse function nl2br;
425fb051e9SGreg Roachuse function ob_end_clean;
43bb802206SGreg Roachuse function ob_get_level;
44837498afSGreg Roachuse function register_shutdown_function;
45f397d0fdSGreg Roachuse function response;
46f397d0fdSGreg Roachuse function str_replace;
47f397d0fdSGreg Roachuse function view;
483976b470SGreg Roach
49837498afSGreg Roachuse const E_ERROR;
50f397d0fdSGreg Roachuse const PHP_EOL;
51cfbf56adSGreg Roach
52cfbf56adSGreg Roach/**
53cfbf56adSGreg Roach * Middleware to handle and render errors.
54cfbf56adSGreg Roach */
5571378461SGreg Roachclass HandleExceptions implements MiddlewareInterface, StatusCodeInterface
56cfbf56adSGreg Roach{
57f397d0fdSGreg Roach    use ViewResponseTrait;
58cfbf56adSGreg Roach
59c4943cffSGreg Roach    private TreeService $tree_service;
60040e7dbaSGreg Roach
61040e7dbaSGreg Roach    /**
62040e7dbaSGreg Roach     * @param TreeService $tree_service
63040e7dbaSGreg Roach     */
64040e7dbaSGreg Roach    public function __construct(TreeService $tree_service)
65040e7dbaSGreg Roach    {
66040e7dbaSGreg Roach        $this->tree_service = $tree_service;
67040e7dbaSGreg Roach    }
68040e7dbaSGreg Roach
69cfbf56adSGreg Roach    /**
70cfbf56adSGreg Roach     * @param ServerRequestInterface  $request
71cfbf56adSGreg Roach     * @param RequestHandlerInterface $handler
72cfbf56adSGreg Roach     *
73cfbf56adSGreg Roach     * @return ResponseInterface
74f397d0fdSGreg Roach     * @throws Throwable
75cfbf56adSGreg Roach     */
76cfbf56adSGreg Roach    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
77cfbf56adSGreg Roach    {
78837498afSGreg Roach        // Fatal errors.  We may be out of memory, so do not create any variables.
798bbbe78fSGreg Roach        register_shutdown_function(static function (): void {
808bbbe78fSGreg Roach            if (error_get_last() !== null && error_get_last()['type'] & E_ERROR) {
81837498afSGreg Roach                // If PHP does not display the error, then we must display it.
82837498afSGreg Roach                if (ini_get('display_errors') !== '1') {
83837498afSGreg Roach                    echo error_get_last()['message'], '<br><br>', error_get_last()['file'], ': ', error_get_last()['line'];
84837498afSGreg Roach                }
85837498afSGreg Roach            }
86837498afSGreg Roach        });
87837498afSGreg Roach
88cfbf56adSGreg Roach        try {
89cfbf56adSGreg Roach            return $handler->handle($request);
90cfbf56adSGreg Roach        } catch (HttpException $exception) {
915fb051e9SGreg Roach            // The router added the tree attribute to the request, and we need it for the error response.
92d35568b4SGreg Roach            if (Registry::container()->has(ServerRequestInterface::class)) {
93d35568b4SGreg Roach                $request = Registry::container()->get(ServerRequestInterface::class);
946e9f3eb9SGreg Roach            } else {
95d35568b4SGreg Roach                Registry::container()->set(ServerRequestInterface::class, $request);
96ff020ee8SGreg Roach            }
97f397d0fdSGreg Roach
98f397d0fdSGreg Roach            return $this->httpExceptionResponse($request, $exception);
996d585cefSGreg Roach        } catch (FilesystemException $exception) {
10045876889SGreg Roach            // The router added the tree attribute to the request, and we need it for the error response.
101d35568b4SGreg Roach            $request = Registry::container()->get(ServerRequestInterface::class) ?? $request;
10245876889SGreg Roach
10345876889SGreg Roach            return $this->thirdPartyExceptionResponse($request, $exception);
104f397d0fdSGreg Roach        } catch (Throwable $exception) {
105bb802206SGreg Roach            // Exception thrown while buffering output?
106bb802206SGreg Roach            while (ob_get_level() > 0) {
1075fb051e9SGreg Roach                ob_end_clean();
108bb802206SGreg Roach            }
109bb802206SGreg Roach
1105fb051e9SGreg Roach            // The Router middleware may have added a tree attribute to the request.
1115fb051e9SGreg Roach            // This might be usable in the error page.
112d35568b4SGreg Roach            if (Registry::container()->has(ServerRequestInterface::class)) {
113d35568b4SGreg Roach                $request = Registry::container()->get(ServerRequestInterface::class);
1145fb051e9SGreg Roach            }
1155fb051e9SGreg Roach
1165fb051e9SGreg Roach            // Show the exception in a standard webtrees page (if we can).
1175fb051e9SGreg Roach            try {
1185fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
11928d026adSGreg Roach            } catch (Throwable) {
120c2c02b9eSGreg Roach                // That didn't work.  Try something else.
1215fb051e9SGreg Roach            }
1225fb051e9SGreg Roach
1235fb051e9SGreg Roach            // Show the exception in a tree-less webtrees page (if we can).
1245fb051e9SGreg Roach            try {
1255fb051e9SGreg Roach                $request = $request->withAttribute('tree', null);
126f397d0fdSGreg Roach
127f397d0fdSGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
12828d026adSGreg Roach            } catch (Throwable) {
129c2c02b9eSGreg Roach                // That didn't work.  Try something else.
130f397d0fdSGreg Roach            }
1315fb051e9SGreg Roach
1325fb051e9SGreg Roach            // Show the exception in an error page (if we can).
1335fb051e9SGreg Roach            try {
1345fb051e9SGreg Roach                $this->layout = 'layouts/error';
1355fb051e9SGreg Roach
1365fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
13728d026adSGreg Roach            } catch (Throwable) {
138c2c02b9eSGreg Roach                // That didn't work.  Try something else.
1395fb051e9SGreg Roach            }
1405fb051e9SGreg Roach
1415fb051e9SGreg Roach            // Show a stack dump.
142*d5a67eccSGreg Roach            return response(nl2br((string) $exception), StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
143f397d0fdSGreg Roach        }
144cfbf56adSGreg Roach    }
145cfbf56adSGreg Roach
146f397d0fdSGreg Roach    /**
147f397d0fdSGreg Roach     * @param ServerRequestInterface $request
148f397d0fdSGreg Roach     * @param HttpException          $exception
149f397d0fdSGreg Roach     *
150f397d0fdSGreg Roach     * @return ResponseInterface
151f397d0fdSGreg Roach     */
152f397d0fdSGreg Roach    private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface
153f397d0fdSGreg Roach    {
154b55cbc6bSGreg Roach        $tree    = Validator::attributes($request)->treeOptional();
155040e7dbaSGreg Roach        $default = Site::getPreference('DEFAULT_GEDCOM');
156b55cbc6bSGreg Roach        $tree    ??= $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
157040e7dbaSGreg Roach
158b8d46257SGreg Roach        $status_code = $exception->getCode();
159b8d46257SGreg Roach
160b8d46257SGreg Roach        // If this was a GET request, then we were probably fetching HTML to display, for
161b8d46257SGreg Roach        // example a chart or tab.
162b8d46257SGreg Roach        if (
163b8d46257SGreg Roach            $request->getHeaderLine('X-Requested-With') !== '' &&
164b8d46257SGreg Roach            $request->getMethod() === RequestMethodInterface::METHOD_GET
165b8d46257SGreg Roach        ) {
166db6e5963SGreg Roach            $this->layout = 'layouts/ajax';
167b8d46257SGreg Roach            $status_code = StatusCodeInterface::STATUS_OK;
16820691a3aSGreg Roach        }
169db6e5963SGreg Roach
170f397d0fdSGreg Roach        return $this->viewResponse('components/alert-danger', [
171f397d0fdSGreg Roach            'alert' => $exception->getMessage(),
172f397d0fdSGreg Roach            'title' => $exception->getMessage(),
1735fb051e9SGreg Roach            'tree'  => $tree,
174b8d46257SGreg Roach        ], $status_code);
175cfbf56adSGreg Roach    }
176f397d0fdSGreg Roach
177f397d0fdSGreg Roach    /**
178f397d0fdSGreg Roach     * @param ServerRequestInterface $request
179f397d0fdSGreg Roach     * @param Throwable              $exception
180f397d0fdSGreg Roach     *
181f397d0fdSGreg Roach     * @return ResponseInterface
182f397d0fdSGreg Roach     */
18345876889SGreg Roach    private function thirdPartyExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
18445876889SGreg Roach    {
185b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->treeOptional();
18645876889SGreg Roach
18745876889SGreg Roach        $default = Site::getPreference('DEFAULT_GEDCOM');
1883529c469SGreg Roach        $tree ??= $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
18945876889SGreg Roach
19045876889SGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
19145876889SGreg Roach            $this->layout = 'layouts/ajax';
19245876889SGreg Roach        }
19345876889SGreg Roach
19445876889SGreg Roach        return $this->viewResponse('components/alert-danger', [
19545876889SGreg Roach            'alert' => $exception->getMessage(),
19645876889SGreg Roach            'title' => $exception->getMessage(),
19745876889SGreg Roach            'tree'  => $tree,
19845876889SGreg Roach        ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
19945876889SGreg Roach    }
20045876889SGreg Roach
20145876889SGreg Roach    /**
20245876889SGreg Roach     * @param ServerRequestInterface $request
20345876889SGreg Roach     * @param Throwable              $exception
20445876889SGreg Roach     *
20545876889SGreg Roach     * @return ResponseInterface
20645876889SGreg Roach     */
207f397d0fdSGreg Roach    private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
208f397d0fdSGreg Roach    {
2095fb051e9SGreg Roach        $this->layout = 'layouts/default';
210ad602e4bSGreg Roach
211f397d0fdSGreg Roach        // Create a stack dump for the exception
212f397d0fdSGreg Roach        $base_path = dirname(__DIR__, 3);
213c133c283SGreg Roach        $trace     = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString();
214f397d0fdSGreg Roach        $trace     = str_replace($base_path, '…', $trace);
2155d5cecd5SGreg Roach        // User data may contain non UTF-8 characters.
2165d5cecd5SGreg Roach        $trace     = mb_convert_encoding($trace, 'UTF-8', 'UTF-8');
2179483aecdSGreg Roach        $trace     = e($trace);
2189483aecdSGreg Roach        $trace     = preg_replace('/^.*modules_v4.*$/m', '<b>$0</b>', $trace);
219f397d0fdSGreg Roach
220f397d0fdSGreg Roach        try {
221f397d0fdSGreg Roach            Log::addErrorLog($trace);
22228d026adSGreg Roach        } catch (Throwable) {
223f397d0fdSGreg Roach            // Must have been a problem with the database.  Nothing we can do here.
224f397d0fdSGreg Roach        }
225f397d0fdSGreg Roach
226f397d0fdSGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
227b7765f6bSGreg Roach            // If this was a GET request, then we were probably fetching HTML to display, for
228b7765f6bSGreg Roach            // example a chart or tab.
22971378461SGreg Roach            if ($request->getMethod() === RequestMethodInterface::METHOD_GET) {
23071378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_OK;
231b7765f6bSGreg Roach            } else {
23271378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR;
233b7765f6bSGreg Roach            }
234b7765f6bSGreg Roach
235b7765f6bSGreg Roach            return response(view('components/alert-danger', ['alert' => $trace]), $status_code);
236f397d0fdSGreg Roach        }
237f397d0fdSGreg Roach
23844358015SGreg Roach        try {
23944358015SGreg Roach            // Try with a full header/menu
24044358015SGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
24144358015SGreg Roach                'title'   => 'Error',
24244358015SGreg Roach                'error'   => $trace,
24344358015SGreg Roach                'request' => $request,
244b55cbc6bSGreg Roach                'tree'    => Validator::attributes($request)->treeOptional(),
24544358015SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
24628d026adSGreg Roach        } catch (Throwable) {
24744358015SGreg Roach            // Try with a minimal header/menu
248f397d0fdSGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
249f397d0fdSGreg Roach                'title'   => 'Error',
250f397d0fdSGreg Roach                'error'   => $trace,
2515fb051e9SGreg Roach                'request' => $request,
2525fb051e9SGreg Roach                'tree'    => null,
25371378461SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
254cfbf56adSGreg Roach        }
255cfbf56adSGreg Roach    }
25644358015SGreg Roach}
257