1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Http\Middleware; 19 20use Fig\Http\Message\RequestMethodInterface; 21use Fig\Http\Message\StatusCodeInterface; 22use Fisharebest\Webtrees\Http\ViewResponseTrait; 23use Fisharebest\Webtrees\Log; 24use Psr\Http\Message\ResponseInterface; 25use Psr\Http\Message\ServerRequestInterface; 26use Psr\Http\Server\MiddlewareInterface; 27use Psr\Http\Server\RequestHandlerInterface; 28use Symfony\Component\HttpKernel\Exception\HttpException; 29use Throwable; 30use function dirname; 31use function ob_get_level; 32use function response; 33use function str_replace; 34use function view; 35use const PHP_EOL; 36 37/** 38 * Middleware to handle and render errors. 39 */ 40class HandleExceptions implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface 41{ 42 use ViewResponseTrait; 43 44 /** 45 * @param ServerRequestInterface $request 46 * @param RequestHandlerInterface $handler 47 * 48 * @return ResponseInterface 49 * @throws Throwable 50 */ 51 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 52 { 53 try { 54 try { 55 return $handler->handle($request); 56 } catch (HttpException $exception) { 57 $original_exception = $exception; 58 59 return $this->httpExceptionResponse($request, $exception); 60 } catch (Throwable $exception) { 61 // Exception thrown while buffering output? 62 while (ob_get_level() > 0) { 63 ob_get_clean(); 64 } 65 66 $original_exception = $exception; 67 68 return $this->unhandledExceptionResponse($request, $exception); 69 } 70 } catch (Throwable $exception) { 71 // If we can't handle the exception, rethrow it. 72 throw $original_exception; 73 } 74 } 75 76 /** 77 * @param ServerRequestInterface $request 78 * @param HttpException $exception 79 * 80 * @return ResponseInterface 81 */ 82 private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface 83 { 84 if ($request->getHeaderLine('X-Requested-With') !== '') { 85 $this->layout = 'layouts/ajax'; 86 } 87 88 return $this->viewResponse('components/alert-danger', [ 89 'alert' => $exception->getMessage(), 90 'title' => $exception->getMessage(), 91 ], $exception->getStatusCode()); 92 } 93 94 /** 95 * @param ServerRequestInterface $request 96 * @param Throwable $exception 97 * 98 * @return ResponseInterface 99 */ 100 private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface 101 { 102 // Create a stack dump for the exception 103 $base_path = dirname(__DIR__, 3); 104 $trace = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString(); 105 $trace = str_replace($base_path, '…', $trace); 106 107 try { 108 Log::addErrorLog($trace); 109 } catch (Throwable $exception) { 110 // Must have been a problem with the database. Nothing we can do here. 111 } 112 113 if ($request->getHeaderLine('X-Requested-With') !== '') { 114 // If this was a GET request, then we were probably fetching HTML to display, for 115 // example a chart or tab. 116 if ($request->getMethod() === self::METHOD_GET) { 117 $status_code = self::STATUS_OK; 118 } else { 119 $status_code = self::STATUS_INTERNAL_SERVER_ERROR; 120 } 121 122 return response(view('components/alert-danger', ['alert' => $trace]), $status_code); 123 } 124 125 return $this->viewResponse('errors/unhandled-exception', [ 126 'title' => 'Error', 127 'error' => $trace, 128 ], self::STATUS_INTERNAL_SERVER_ERROR); 129 } 130} 131