1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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 <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Http\Middleware; 21 22use Fig\Http\Message\RequestMethodInterface; 23use Fig\Http\Message\StatusCodeInterface; 24use Fisharebest\Localization\Locale\LocaleEnUs; 25use Fisharebest\Webtrees\Http\ViewResponseTrait; 26use Fisharebest\Webtrees\Log; 27use Psr\Http\Message\ResponseInterface; 28use Psr\Http\Message\ServerRequestInterface; 29use Psr\Http\Server\MiddlewareInterface; 30use Psr\Http\Server\RequestHandlerInterface; 31use Symfony\Component\HttpKernel\Exception\HttpException; 32use Throwable; 33 34use function app; 35use function dirname; 36use function ob_end_clean; 37use function ob_get_level; 38use function response; 39use function str_replace; 40use function view; 41 42use const PHP_EOL; 43 44/** 45 * Middleware to handle and render errors. 46 */ 47class HandleExceptions implements MiddlewareInterface, StatusCodeInterface 48{ 49 use ViewResponseTrait; 50 51 /** 52 * @param ServerRequestInterface $request 53 * @param RequestHandlerInterface $handler 54 * 55 * @return ResponseInterface 56 * @throws Throwable 57 */ 58 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 59 { 60 try { 61 return $handler->handle($request); 62 } catch (HttpException $exception) { 63 // The router added the tree attribute to the request, and we need it for the error response. 64 $request = app(ServerRequestInterface::class) ?? $request; 65 66 return $this->httpExceptionResponse($request, $exception); 67 } catch (Throwable $exception) { 68 // Exception thrown while buffering output? 69 while (ob_get_level() > 0) { 70 ob_end_clean(); 71 } 72 73 // The Router middleware may have added a tree attribute to the request. 74 // This might be usable in the error page. 75 if (app()->has(ServerRequestInterface::class)) { 76 $request = app(ServerRequestInterface::class) ?? $request; 77 } 78 79 // No locale set in the request? 80 if ($request->getAttribute('locale') === null) { 81 $request = $request->withAttribute('locale', new LocaleEnUs()); 82 app()->instance(ServerRequestInterface::class, $request); 83 } 84 85 // Show the exception in a standard webtrees page (if we can). 86 try { 87 return $this->unhandledExceptionResponse($request, $exception); 88 } catch (Throwable $e) { 89 // That didn't work. Try something else. 90 } 91 92 // Show the exception in a tree-less webtrees page (if we can). 93 try { 94 $request = $request->withAttribute('tree', null); 95 96 return $this->unhandledExceptionResponse($request, $exception); 97 } catch (Throwable $e) { 98 // That didn't work. Try something else. 99 } 100 101 // Show the exception in an error page (if we can). 102 try { 103 $this->layout = 'layouts/error'; 104 105 return $this->unhandledExceptionResponse($request, $exception); 106 } catch (Throwable $e) { 107 // That didn't work. Try something else. 108 } 109 110 // Show a stack dump. 111 return response((string) $exception, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); 112 } 113 } 114 115 /** 116 * @param ServerRequestInterface $request 117 * @param HttpException $exception 118 * 119 * @return ResponseInterface 120 */ 121 private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface 122 { 123 $tree = $request->getAttribute('tree'); 124 125 if ($request->getHeaderLine('X-Requested-With') !== '') { 126 $this->layout = 'layouts/ajax'; 127 } 128 129 return $this->viewResponse('components/alert-danger', [ 130 'alert' => $exception->getMessage(), 131 'title' => $exception->getMessage(), 132 'tree' => $tree, 133 ], $exception->getStatusCode()); 134 } 135 136 /** 137 * @param ServerRequestInterface $request 138 * @param Throwable $exception 139 * 140 * @return ResponseInterface 141 */ 142 private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface 143 { 144 $this->layout = 'layouts/default'; 145 146 // Create a stack dump for the exception 147 $base_path = dirname(__DIR__, 3); 148 $trace = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString(); 149 $trace = str_replace($base_path, '…', $trace); 150 $trace = e($trace); 151 $trace = preg_replace('/^.*modules_v4.*$/m', '<b>$0</b>', $trace); 152 153 try { 154 Log::addErrorLog($trace); 155 } catch (Throwable $exception) { 156 // Must have been a problem with the database. Nothing we can do here. 157 } 158 159 if ($request->getHeaderLine('X-Requested-With') !== '') { 160 // If this was a GET request, then we were probably fetching HTML to display, for 161 // example a chart or tab. 162 if ($request->getMethod() === RequestMethodInterface::METHOD_GET) { 163 $status_code = StatusCodeInterface::STATUS_OK; 164 } else { 165 $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR; 166 } 167 168 return response(view('components/alert-danger', ['alert' => $trace]), $status_code); 169 } 170 171 return $this->viewResponse('errors/unhandled-exception', [ 172 'title' => 'Error', 173 'error' => $trace, 174 'request' => $request, 175 'tree' => null, 176 ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); 177 } 178} 179