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