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