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 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Http\Middleware; 20 21use Fig\Http\Message\RequestMethodInterface; 22use Fig\Http\Message\StatusCodeInterface; 23use Fisharebest\Webtrees\Http\ViewResponseTrait; 24use Fisharebest\Webtrees\Log; 25use Psr\Http\Message\ResponseInterface; 26use Psr\Http\Message\ServerRequestInterface; 27use Psr\Http\Server\MiddlewareInterface; 28use Psr\Http\Server\RequestHandlerInterface; 29use Symfony\Component\HttpKernel\Exception\HttpException; 30use Throwable; 31 32use function dirname; 33use function ob_get_level; 34use function response; 35use function str_replace; 36use function view; 37 38use const PHP_EOL; 39 40/** 41 * Middleware to handle and render errors. 42 */ 43class HandleExceptions implements MiddlewareInterface, StatusCodeInterface 44{ 45 use ViewResponseTrait; 46 47 /** 48 * @param ServerRequestInterface $request 49 * @param RequestHandlerInterface $handler 50 * 51 * @return ResponseInterface 52 * @throws Throwable 53 */ 54 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 55 { 56 try { 57 try { 58 return $handler->handle($request); 59 } catch (HttpException $exception) { 60 $original_exception = $exception; 61 62 return $this->httpExceptionResponse($request, $exception); 63 } catch (Throwable $exception) { 64 // Exception thrown while buffering output? 65 while (ob_get_level() > 0) { 66 ob_get_clean(); 67 } 68 69 $original_exception = $exception; 70 71 return $this->unhandledExceptionResponse($request, $exception); 72 } 73 } catch (Throwable $exception) { 74 // If we can't handle the exception, rethrow it. 75 throw $original_exception; 76 } 77 } 78 79 /** 80 * @param ServerRequestInterface $request 81 * @param HttpException $exception 82 * 83 * @return ResponseInterface 84 */ 85 private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface 86 { 87 if ($request->getHeaderLine('X-Requested-With') !== '') { 88 $this->layout = 'layouts/ajax'; 89 } 90 91 return $this->viewResponse('components/alert-danger', [ 92 'alert' => $exception->getMessage(), 93 'title' => $exception->getMessage(), 94 ], $exception->getStatusCode()); 95 } 96 97 /** 98 * @param ServerRequestInterface $request 99 * @param Throwable $exception 100 * 101 * @return ResponseInterface 102 */ 103 private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface 104 { 105 // Create a stack dump for the exception 106 $base_path = dirname(__DIR__, 3); 107 $trace = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString(); 108 $trace = str_replace($base_path, '…', $trace); 109 110 try { 111 Log::addErrorLog($trace); 112 } catch (Throwable $exception) { 113 // Must have been a problem with the database. Nothing we can do here. 114 } 115 116 if ($request->getHeaderLine('X-Requested-With') !== '') { 117 // If this was a GET request, then we were probably fetching HTML to display, for 118 // example a chart or tab. 119 if ($request->getMethod() === RequestMethodInterface::METHOD_GET) { 120 $status_code = StatusCodeInterface::STATUS_OK; 121 } else { 122 $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR; 123 } 124 125 return response(view('components/alert-danger', ['alert' => $trace]), $status_code); 126 } 127 128 return $this->viewResponse('errors/unhandled-exception', [ 129 'title' => 'Error', 130 'error' => $trace, 131 ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); 132 } 133} 134