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