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