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