HandleExceptions::httpExceptionResponse()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 2
nop 2
dl 0
loc 23
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Middleware;
21
22
use Fig\Http\Message\RequestMethodInterface;
23
use Fig\Http\Message\StatusCodeInterface;
24
use Fisharebest\Webtrees\Http\Exceptions\HttpException;
25
use Fisharebest\Webtrees\Http\ViewResponseTrait;
26
use Fisharebest\Webtrees\Log;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Log was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Fisharebest\Webtrees\Registry;
28
use Fisharebest\Webtrees\Services\PhpService;
29
use Fisharebest\Webtrees\Services\TreeService;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Services\TreeService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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