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; 38837498afSGreg Roachuse function error_get_last; 39837498afSGreg Roachuse function ini_get; 405fb051e9SGreg Roachuse function ob_end_clean; 41bb802206SGreg Roachuse function ob_get_level; 42837498afSGreg Roachuse function register_shutdown_function; 43f397d0fdSGreg Roachuse function response; 44f397d0fdSGreg Roachuse function str_replace; 45837498afSGreg Roachuse function strpos; 46f397d0fdSGreg Roachuse function view; 473976b470SGreg Roach 48837498afSGreg 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 { 80837498afSGreg Roach // Fatal errors. We may be out of memory, so do not create any variables. 81*8bbbe78fSGreg Roach register_shutdown_function(static function (): void { 82*8bbbe78fSGreg Roach if (error_get_last() !== null && error_get_last()['type'] & E_ERROR) { 83837498afSGreg Roach // If PHP does not display the error, then we must display it. 84837498afSGreg Roach if (ini_get('display_errors') !== '1') { 85837498afSGreg Roach echo error_get_last()['message'], '<br><br>', error_get_last()['file'] , ': ', error_get_last()['line']; 86837498afSGreg Roach } 87837498afSGreg Roach // Not our fault? 88837498afSGreg Roach if (strpos(error_get_last()['file'], '/modules_v4/') !== false) { 89837498afSGreg Roach echo '<br><br>This is an error in a webtrees module. Upgrade it or disable it.'; 90837498afSGreg Roach } 91837498afSGreg Roach } 92837498afSGreg Roach }); 93837498afSGreg 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