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