xref: /webtrees/app/Http/Middleware/HandleExceptions.php (revision 837498afc3364a5951f08b19afa2beae26d4a4a7)
1cfbf56adSGreg Roach<?php
23976b470SGreg Roach
3cfbf56adSGreg Roach/**
4cfbf56adSGreg Roach * webtrees: online genealogy
5cfbf56adSGreg Roach * Copyright (C) 2019 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
15cfbf56adSGreg Roach * along with this program. If not, see <http://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;
24d501c45dSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpException;
25f397d0fdSGreg Roachuse Fisharebest\Webtrees\Http\ViewResponseTrait;
26f397d0fdSGreg Roachuse Fisharebest\Webtrees\Log;
27040e7dbaSGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
28040e7dbaSGreg Roachuse Fisharebest\Webtrees\Site;
2945876889SGreg Roachuse League\Flysystem\NotSupportedException;
30cfbf56adSGreg Roachuse Psr\Http\Message\ResponseInterface;
31cfbf56adSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
32cfbf56adSGreg Roachuse Psr\Http\Server\MiddlewareInterface;
33cfbf56adSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
34cfbf56adSGreg Roachuse Throwable;
353976b470SGreg Roach
365fb051e9SGreg Roachuse function app;
37f397d0fdSGreg Roachuse function dirname;
38*837498afSGreg Roachuse function error_get_last;
39*837498afSGreg Roachuse function ini_get;
405fb051e9SGreg Roachuse function ob_end_clean;
41bb802206SGreg Roachuse function ob_get_level;
42*837498afSGreg Roachuse function register_shutdown_function;
43f397d0fdSGreg Roachuse function response;
44f397d0fdSGreg Roachuse function str_replace;
45*837498afSGreg Roachuse function strpos;
46f397d0fdSGreg Roachuse function view;
473976b470SGreg Roach
48*837498afSGreg Roachuse const E_ERROR;
49f397d0fdSGreg Roachuse const PHP_EOL;
50cfbf56adSGreg Roach
51cfbf56adSGreg Roach/**
52cfbf56adSGreg Roach * Middleware to handle and render errors.
53cfbf56adSGreg Roach */
5471378461SGreg Roachclass HandleExceptions implements MiddlewareInterface, StatusCodeInterface
55cfbf56adSGreg Roach{
56f397d0fdSGreg Roach    use ViewResponseTrait;
57cfbf56adSGreg Roach
58040e7dbaSGreg Roach    /** @var TreeService */
59040e7dbaSGreg Roach    private $tree_service;
60040e7dbaSGreg Roach
61040e7dbaSGreg Roach    /**
62040e7dbaSGreg Roach     * HandleExceptions constructor.
63040e7dbaSGreg Roach     *
64040e7dbaSGreg Roach     * @param TreeService $tree_service
65040e7dbaSGreg Roach     */
66040e7dbaSGreg Roach    public function __construct(TreeService $tree_service)
67040e7dbaSGreg Roach    {
68040e7dbaSGreg Roach        $this->tree_service = $tree_service;
69040e7dbaSGreg Roach    }
70040e7dbaSGreg Roach
71cfbf56adSGreg Roach    /**
72cfbf56adSGreg Roach     * @param ServerRequestInterface  $request
73cfbf56adSGreg Roach     * @param RequestHandlerInterface $handler
74cfbf56adSGreg Roach     *
75cfbf56adSGreg Roach     * @return ResponseInterface
76f397d0fdSGreg Roach     * @throws Throwable
77cfbf56adSGreg Roach     */
78cfbf56adSGreg Roach    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
79cfbf56adSGreg Roach    {
80*837498afSGreg Roach        // Fatal errors.  We may be out of memory, so do not create any variables.
81*837498afSGreg Roach        register_shutdown_function(static function () {
82*837498afSGreg Roach            if (error_get_last()['type'] & E_ERROR) {
83*837498afSGreg Roach                // If PHP does not display the error, then we must display it.
84*837498afSGreg Roach                if (ini_get('display_errors') !== '1') {
85*837498afSGreg Roach                    echo error_get_last()['message'], '<br><br>', error_get_last()['file'] , ': ', error_get_last()['line'];
86*837498afSGreg Roach                }
87*837498afSGreg Roach                // Not our fault?
88*837498afSGreg Roach                if (strpos(error_get_last()['file'], '/modules_v4/') !== false) {
89*837498afSGreg Roach                    echo '<br><br>This is an error in a webtrees module.  Upgrade it or disable it.';
90*837498afSGreg Roach                }
91*837498afSGreg Roach            }
92*837498afSGreg Roach        });
93*837498afSGreg Roach
94cfbf56adSGreg Roach        try {
95cfbf56adSGreg Roach            return $handler->handle($request);
96cfbf56adSGreg Roach        } catch (HttpException $exception) {
975fb051e9SGreg Roach            // The router added the tree attribute to the request, and we need it for the error response.
985fb051e9SGreg Roach            $request = app(ServerRequestInterface::class) ?? $request;
99f397d0fdSGreg Roach
100f397d0fdSGreg Roach            return $this->httpExceptionResponse($request, $exception);
10145876889SGreg Roach        } catch (NotSupportedException $exception) {
10245876889SGreg Roach            // The router added the tree attribute to the request, and we need it for the error response.
10345876889SGreg Roach            $request = app(ServerRequestInterface::class) ?? $request;
10445876889SGreg Roach
10545876889SGreg Roach            return $this->thirdPartyExceptionResponse($request, $exception);
106f397d0fdSGreg Roach        } catch (Throwable $exception) {
107bb802206SGreg Roach            // Exception thrown while buffering output?
108bb802206SGreg Roach            while (ob_get_level() > 0) {
1095fb051e9SGreg Roach                ob_end_clean();
110bb802206SGreg Roach            }
111bb802206SGreg Roach
1125fb051e9SGreg Roach            // The Router middleware may have added a tree attribute to the request.
1135fb051e9SGreg Roach            // This might be usable in the error page.
1145fb051e9SGreg Roach            if (app()->has(ServerRequestInterface::class)) {
1155fb051e9SGreg Roach                $request = app(ServerRequestInterface::class) ?? $request;
1165fb051e9SGreg Roach            }
1175fb051e9SGreg Roach
1185fb051e9SGreg Roach            // Show the exception in a standard webtrees page (if we can).
1195fb051e9SGreg Roach            try {
1205fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
1215fb051e9SGreg Roach            } catch (Throwable $e) {
122c2c02b9eSGreg Roach                // That didn't work.  Try something else.
1235fb051e9SGreg Roach            }
1245fb051e9SGreg Roach
1255fb051e9SGreg Roach            // Show the exception in a tree-less webtrees page (if we can).
1265fb051e9SGreg Roach            try {
1275fb051e9SGreg Roach                $request = $request->withAttribute('tree', null);
128f397d0fdSGreg Roach
129f397d0fdSGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
1305fb051e9SGreg Roach            } catch (Throwable $e) {
131c2c02b9eSGreg Roach                // That didn't work.  Try something else.
132f397d0fdSGreg Roach            }
1335fb051e9SGreg Roach
1345fb051e9SGreg Roach            // Show the exception in an error page (if we can).
1355fb051e9SGreg Roach            try {
1365fb051e9SGreg Roach                $this->layout = 'layouts/error';
1375fb051e9SGreg Roach
1385fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
1395fb051e9SGreg Roach            } catch (Throwable $e) {
140c2c02b9eSGreg Roach                // That didn't work.  Try something else.
1415fb051e9SGreg Roach            }
1425fb051e9SGreg Roach
1435fb051e9SGreg Roach            // Show a stack dump.
1445fb051e9SGreg Roach            return response((string) $exception, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
145f397d0fdSGreg Roach        }
146cfbf56adSGreg Roach    }
147cfbf56adSGreg Roach
148f397d0fdSGreg Roach    /**
149f397d0fdSGreg Roach     * @param ServerRequestInterface $request
150f397d0fdSGreg Roach     * @param HttpException          $exception
151f397d0fdSGreg Roach     *
152f397d0fdSGreg Roach     * @return ResponseInterface
153f397d0fdSGreg Roach     */
154f397d0fdSGreg Roach    private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface
155f397d0fdSGreg Roach    {
1565fb051e9SGreg Roach        $tree = $request->getAttribute('tree');
1575fb051e9SGreg Roach
158040e7dbaSGreg Roach        $default = Site::getPreference('DEFAULT_GEDCOM');
159040e7dbaSGreg Roach        $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
160040e7dbaSGreg Roach
161b8d46257SGreg Roach        $status_code = $exception->getCode();
162b8d46257SGreg Roach
163b8d46257SGreg Roach        // If this was a GET request, then we were probably fetching HTML to display, for
164b8d46257SGreg Roach        // example a chart or tab.
165b8d46257SGreg Roach        if (
166b8d46257SGreg Roach            $request->getHeaderLine('X-Requested-With') !== '' &&
167b8d46257SGreg Roach            $request->getMethod() === RequestMethodInterface::METHOD_GET
168b8d46257SGreg Roach        ) {
169db6e5963SGreg Roach            $this->layout = 'layouts/ajax';
170b8d46257SGreg Roach            $status_code = StatusCodeInterface::STATUS_OK;
17120691a3aSGreg Roach        }
172db6e5963SGreg Roach
173f397d0fdSGreg Roach        return $this->viewResponse('components/alert-danger', [
174f397d0fdSGreg Roach            'alert' => $exception->getMessage(),
175f397d0fdSGreg Roach            'title' => $exception->getMessage(),
1765fb051e9SGreg Roach            'tree'  => $tree,
177b8d46257SGreg Roach        ], $status_code);
178cfbf56adSGreg Roach    }
179f397d0fdSGreg Roach
180f397d0fdSGreg Roach    /**
181f397d0fdSGreg Roach     * @param ServerRequestInterface $request
182f397d0fdSGreg Roach     * @param Throwable              $exception
183f397d0fdSGreg Roach     *
184f397d0fdSGreg Roach     * @return ResponseInterface
185f397d0fdSGreg Roach     */
18645876889SGreg Roach    private function thirdPartyExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
18745876889SGreg Roach    {
18845876889SGreg Roach        $tree = $request->getAttribute('tree');
18945876889SGreg Roach
19045876889SGreg Roach        $default = Site::getPreference('DEFAULT_GEDCOM');
19145876889SGreg Roach        $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
19245876889SGreg Roach
19345876889SGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
19445876889SGreg Roach            $this->layout = 'layouts/ajax';
19545876889SGreg Roach        }
19645876889SGreg Roach
19745876889SGreg Roach        return $this->viewResponse('components/alert-danger', [
19845876889SGreg Roach            'alert' => $exception->getMessage(),
19945876889SGreg Roach            'title' => $exception->getMessage(),
20045876889SGreg Roach            'tree'  => $tree,
20145876889SGreg Roach        ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
20245876889SGreg Roach    }
20345876889SGreg Roach
20445876889SGreg Roach    /**
20545876889SGreg Roach     * @param ServerRequestInterface $request
20645876889SGreg Roach     * @param Throwable              $exception
20745876889SGreg Roach     *
20845876889SGreg Roach     * @return ResponseInterface
20945876889SGreg Roach     */
210f397d0fdSGreg Roach    private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
211f397d0fdSGreg Roach    {
2125fb051e9SGreg Roach        $this->layout = 'layouts/default';
213ad602e4bSGreg Roach
214f397d0fdSGreg Roach        // Create a stack dump for the exception
215f397d0fdSGreg Roach        $base_path = dirname(__DIR__, 3);
216c133c283SGreg Roach        $trace     = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString();
217f397d0fdSGreg Roach        $trace     = str_replace($base_path, '…', $trace);
2185d5cecd5SGreg Roach        // User data may contain non UTF-8 characters.
2195d5cecd5SGreg Roach        $trace     = mb_convert_encoding($trace, 'UTF-8', 'UTF-8');
2209483aecdSGreg Roach        $trace     = e($trace);
2219483aecdSGreg Roach        $trace     = preg_replace('/^.*modules_v4.*$/m', '<b>$0</b>', $trace);
222f397d0fdSGreg Roach
223f397d0fdSGreg Roach        try {
224f397d0fdSGreg Roach            Log::addErrorLog($trace);
225f397d0fdSGreg Roach        } catch (Throwable $exception) {
226f397d0fdSGreg Roach            // Must have been a problem with the database.  Nothing we can do here.
227f397d0fdSGreg Roach        }
228f397d0fdSGreg Roach
229f397d0fdSGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
230b7765f6bSGreg Roach            // If this was a GET request, then we were probably fetching HTML to display, for
231b7765f6bSGreg Roach            // example a chart or tab.
23271378461SGreg Roach            if ($request->getMethod() === RequestMethodInterface::METHOD_GET) {
23371378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_OK;
234b7765f6bSGreg Roach            } else {
23571378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR;
236b7765f6bSGreg Roach            }
237b7765f6bSGreg Roach
238b7765f6bSGreg Roach            return response(view('components/alert-danger', ['alert' => $trace]), $status_code);
239f397d0fdSGreg Roach        }
240f397d0fdSGreg Roach
24144358015SGreg Roach        try {
24244358015SGreg Roach            // Try with a full header/menu
24344358015SGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
24444358015SGreg Roach                'title'   => 'Error',
24544358015SGreg Roach                'error'   => $trace,
24644358015SGreg Roach                'request' => $request,
24744358015SGreg Roach                'tree'    => $request->getAttribute('tree'),
24844358015SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
24944358015SGreg Roach        } catch (Throwable $ex) {
25044358015SGreg Roach            // Try with a minimal header/menu
251f397d0fdSGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
252f397d0fdSGreg Roach                'title'   => 'Error',
253f397d0fdSGreg Roach                'error'   => $trace,
2545fb051e9SGreg Roach                'request' => $request,
2555fb051e9SGreg Roach                'tree'    => null,
25671378461SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
257cfbf56adSGreg Roach        }
258cfbf56adSGreg Roach    }
25944358015SGreg Roach}
260