xref: /webtrees/app/Http/Middleware/HandleExceptions.php (revision d501c45d339d4a2d06248f9197d7875a4df14e48)
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;
24*d501c45dSGreg 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;
29cfbf56adSGreg Roachuse Psr\Http\Message\ResponseInterface;
30cfbf56adSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
31cfbf56adSGreg Roachuse Psr\Http\Server\MiddlewareInterface;
32cfbf56adSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
33cfbf56adSGreg Roachuse Throwable;
343976b470SGreg Roach
355fb051e9SGreg Roachuse function app;
36f397d0fdSGreg Roachuse function dirname;
375fb051e9SGreg Roachuse function ob_end_clean;
38bb802206SGreg Roachuse function ob_get_level;
39f397d0fdSGreg Roachuse function response;
40f397d0fdSGreg Roachuse function str_replace;
41f397d0fdSGreg Roachuse function view;
423976b470SGreg Roach
43f397d0fdSGreg Roachuse const PHP_EOL;
44cfbf56adSGreg Roach
45cfbf56adSGreg Roach/**
46cfbf56adSGreg Roach * Middleware to handle and render errors.
47cfbf56adSGreg Roach */
4871378461SGreg Roachclass HandleExceptions implements MiddlewareInterface, StatusCodeInterface
49cfbf56adSGreg Roach{
50f397d0fdSGreg Roach    use ViewResponseTrait;
51cfbf56adSGreg Roach
52040e7dbaSGreg Roach    /** @var TreeService */
53040e7dbaSGreg Roach    private $tree_service;
54040e7dbaSGreg Roach
55040e7dbaSGreg Roach    /**
56040e7dbaSGreg Roach     * HandleExceptions constructor.
57040e7dbaSGreg Roach     *
58040e7dbaSGreg Roach     * @param TreeService $tree_service
59040e7dbaSGreg Roach     */
60040e7dbaSGreg Roach    public function __construct(TreeService $tree_service)
61040e7dbaSGreg Roach    {
62040e7dbaSGreg Roach        $this->tree_service = $tree_service;
63040e7dbaSGreg Roach    }
64040e7dbaSGreg Roach
65cfbf56adSGreg Roach    /**
66cfbf56adSGreg Roach     * @param ServerRequestInterface  $request
67cfbf56adSGreg Roach     * @param RequestHandlerInterface $handler
68cfbf56adSGreg Roach     *
69cfbf56adSGreg Roach     * @return ResponseInterface
70f397d0fdSGreg Roach     * @throws Throwable
71cfbf56adSGreg Roach     */
72cfbf56adSGreg Roach    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
73cfbf56adSGreg Roach    {
74cfbf56adSGreg Roach        try {
75cfbf56adSGreg Roach            return $handler->handle($request);
76cfbf56adSGreg Roach        } catch (HttpException $exception) {
775fb051e9SGreg Roach            // The router added the tree attribute to the request, and we need it for the error response.
785fb051e9SGreg Roach            $request = app(ServerRequestInterface::class) ?? $request;
79f397d0fdSGreg Roach
80f397d0fdSGreg Roach            return $this->httpExceptionResponse($request, $exception);
81f397d0fdSGreg Roach        } catch (Throwable $exception) {
82bb802206SGreg Roach            // Exception thrown while buffering output?
83bb802206SGreg Roach            while (ob_get_level() > 0) {
845fb051e9SGreg Roach                ob_end_clean();
85bb802206SGreg Roach            }
86bb802206SGreg Roach
875fb051e9SGreg Roach            // The Router middleware may have added a tree attribute to the request.
885fb051e9SGreg Roach            // This might be usable in the error page.
895fb051e9SGreg Roach            if (app()->has(ServerRequestInterface::class)) {
905fb051e9SGreg Roach                $request = app(ServerRequestInterface::class) ?? $request;
915fb051e9SGreg Roach            }
925fb051e9SGreg Roach
935fb051e9SGreg Roach            // Show the exception in a standard webtrees page (if we can).
945fb051e9SGreg Roach            try {
955fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
965fb051e9SGreg Roach            } catch (Throwable $e) {
97c2c02b9eSGreg Roach                // That didn't work.  Try something else.
985fb051e9SGreg Roach            }
995fb051e9SGreg Roach
1005fb051e9SGreg Roach            // Show the exception in a tree-less webtrees page (if we can).
1015fb051e9SGreg Roach            try {
1025fb051e9SGreg Roach                $request = $request->withAttribute('tree', null);
103f397d0fdSGreg Roach
104f397d0fdSGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
1055fb051e9SGreg Roach            } catch (Throwable $e) {
106c2c02b9eSGreg Roach                // That didn't work.  Try something else.
107f397d0fdSGreg Roach            }
1085fb051e9SGreg Roach
1095fb051e9SGreg Roach            // Show the exception in an error page (if we can).
1105fb051e9SGreg Roach            try {
1115fb051e9SGreg Roach                $this->layout = 'layouts/error';
1125fb051e9SGreg Roach
1135fb051e9SGreg Roach                return $this->unhandledExceptionResponse($request, $exception);
1145fb051e9SGreg Roach            } catch (Throwable $e) {
115c2c02b9eSGreg Roach                // That didn't work.  Try something else.
1165fb051e9SGreg Roach            }
1175fb051e9SGreg Roach
1185fb051e9SGreg Roach            // Show a stack dump.
1195fb051e9SGreg Roach            return response((string) $exception, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
120f397d0fdSGreg Roach        }
121cfbf56adSGreg Roach    }
122cfbf56adSGreg Roach
123f397d0fdSGreg Roach    /**
124f397d0fdSGreg Roach     * @param ServerRequestInterface $request
125f397d0fdSGreg Roach     * @param HttpException          $exception
126f397d0fdSGreg Roach     *
127f397d0fdSGreg Roach     * @return ResponseInterface
128f397d0fdSGreg Roach     */
129f397d0fdSGreg Roach    private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface
130f397d0fdSGreg Roach    {
1315fb051e9SGreg Roach        $tree = $request->getAttribute('tree');
1325fb051e9SGreg Roach
133040e7dbaSGreg Roach        $default = Site::getPreference('DEFAULT_GEDCOM');
134040e7dbaSGreg Roach        $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first();
135040e7dbaSGreg Roach
136f397d0fdSGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
137db6e5963SGreg Roach            $this->layout = 'layouts/ajax';
13820691a3aSGreg Roach        }
139db6e5963SGreg Roach
140f397d0fdSGreg Roach        return $this->viewResponse('components/alert-danger', [
141f397d0fdSGreg Roach            'alert' => $exception->getMessage(),
142f397d0fdSGreg Roach            'title' => $exception->getMessage(),
1435fb051e9SGreg Roach            'tree'  => $tree,
144*d501c45dSGreg Roach        ], $exception->getCode());
145cfbf56adSGreg Roach    }
146f397d0fdSGreg Roach
147f397d0fdSGreg Roach    /**
148f397d0fdSGreg Roach     * @param ServerRequestInterface $request
149f397d0fdSGreg Roach     * @param Throwable              $exception
150f397d0fdSGreg Roach     *
151f397d0fdSGreg Roach     * @return ResponseInterface
152f397d0fdSGreg Roach     */
153f397d0fdSGreg Roach    private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface
154f397d0fdSGreg Roach    {
1555fb051e9SGreg Roach        $this->layout = 'layouts/default';
156ad602e4bSGreg Roach
157f397d0fdSGreg Roach        // Create a stack dump for the exception
158f397d0fdSGreg Roach        $base_path = dirname(__DIR__, 3);
159c133c283SGreg Roach        $trace     = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString();
160f397d0fdSGreg Roach        $trace     = str_replace($base_path, '…', $trace);
1619483aecdSGreg Roach        $trace     = e($trace);
1629483aecdSGreg Roach        $trace     = preg_replace('/^.*modules_v4.*$/m', '<b>$0</b>', $trace);
163f397d0fdSGreg Roach
164f397d0fdSGreg Roach        try {
165f397d0fdSGreg Roach            Log::addErrorLog($trace);
166f397d0fdSGreg Roach        } catch (Throwable $exception) {
167f397d0fdSGreg Roach            // Must have been a problem with the database.  Nothing we can do here.
168f397d0fdSGreg Roach        }
169f397d0fdSGreg Roach
170f397d0fdSGreg Roach        if ($request->getHeaderLine('X-Requested-With') !== '') {
171b7765f6bSGreg Roach            // If this was a GET request, then we were probably fetching HTML to display, for
172b7765f6bSGreg Roach            // example a chart or tab.
17371378461SGreg Roach            if ($request->getMethod() === RequestMethodInterface::METHOD_GET) {
17471378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_OK;
175b7765f6bSGreg Roach            } else {
17671378461SGreg Roach                $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR;
177b7765f6bSGreg Roach            }
178b7765f6bSGreg Roach
179b7765f6bSGreg Roach            return response(view('components/alert-danger', ['alert' => $trace]), $status_code);
180f397d0fdSGreg Roach        }
181f397d0fdSGreg Roach
18244358015SGreg Roach        try {
18344358015SGreg Roach            // Try with a full header/menu
18444358015SGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
18544358015SGreg Roach                'title'   => 'Error',
18644358015SGreg Roach                'error'   => $trace,
18744358015SGreg Roach                'request' => $request,
18844358015SGreg Roach                'tree'    => $request->getAttribute('tree'),
18944358015SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
19044358015SGreg Roach        } catch (Throwable $ex) {
19144358015SGreg Roach            // Try with a minimal header/menu
192f397d0fdSGreg Roach            return $this->viewResponse('errors/unhandled-exception', [
193f397d0fdSGreg Roach                'title'   => 'Error',
194f397d0fdSGreg Roach                'error'   => $trace,
1955fb051e9SGreg Roach                'request' => $request,
1965fb051e9SGreg Roach                'tree'    => null,
19771378461SGreg Roach            ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
198cfbf56adSGreg Roach        }
199cfbf56adSGreg Roach    }
20044358015SGreg Roach}
201