. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Http\Middleware; use Fig\Http\Message\RequestMethodInterface; use Fig\Http\Message\StatusCodeInterface; use Fisharebest\Webtrees\Http\Exceptions\HttpException; use Fisharebest\Webtrees\Http\ViewResponseTrait; use Fisharebest\Webtrees\Log; use Fisharebest\Webtrees\Services\TreeService; use Fisharebest\Webtrees\Site; use Fisharebest\Webtrees\Validator; use League\Flysystem\FilesystemException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Throwable; use function app; use function dirname; use function error_get_last; use function ini_get; use function ob_end_clean; use function ob_get_level; use function register_shutdown_function; use function response; use function str_replace; use function view; use const E_ERROR; use const PHP_EOL; /** * Middleware to handle and render errors. */ class HandleExceptions implements MiddlewareInterface, StatusCodeInterface { use ViewResponseTrait; private TreeService $tree_service; /** * HandleExceptions constructor. * * @param TreeService $tree_service */ public function __construct(TreeService $tree_service) { $this->tree_service = $tree_service; } /** * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler * * @return ResponseInterface * @throws Throwable */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // Fatal errors. We may be out of memory, so do not create any variables. register_shutdown_function(static function (): void { if (error_get_last() !== null && error_get_last()['type'] & E_ERROR) { // If PHP does not display the error, then we must display it. if (ini_get('display_errors') !== '1') { echo error_get_last()['message'], '

', error_get_last()['file'], ': ', error_get_last()['line']; } } }); try { return $handler->handle($request); } catch (HttpException $exception) { // The router added the tree attribute to the request, and we need it for the error response. if (app()->has(ServerRequestInterface::class)) { $request = app(ServerRequestInterface::class); } else { app()->instance(ServerRequestInterface::class, $request); } return $this->httpExceptionResponse($request, $exception); } catch (FilesystemException $exception) { // The router added the tree attribute to the request, and we need it for the error response. $request = app(ServerRequestInterface::class) ?? $request; return $this->thirdPartyExceptionResponse($request, $exception); } catch (Throwable $exception) { // Exception thrown while buffering output? while (ob_get_level() > 0) { ob_end_clean(); } // The Router middleware may have added a tree attribute to the request. // This might be usable in the error page. if (app()->has(ServerRequestInterface::class)) { $request = app(ServerRequestInterface::class) ?? $request; } // Show the exception in a standard webtrees page (if we can). try { return $this->unhandledExceptionResponse($request, $exception); } catch (Throwable $ignore) { // That didn't work. Try something else. } // Show the exception in a tree-less webtrees page (if we can). try { $request = $request->withAttribute('tree', null); return $this->unhandledExceptionResponse($request, $exception); } catch (Throwable $ignore) { // That didn't work. Try something else. } // Show the exception in an error page (if we can). try { $this->layout = 'layouts/error'; return $this->unhandledExceptionResponse($request, $exception); } catch (Throwable $ignore) { // That didn't work. Try something else. } // Show a stack dump. return response((string) $exception, StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); } } /** * @param ServerRequestInterface $request * @param HttpException $exception * * @return ResponseInterface */ private function httpExceptionResponse(ServerRequestInterface $request, HttpException $exception): ResponseInterface { $tree = Validator::attributes($request)->treeOptional(); $default = Site::getPreference('DEFAULT_GEDCOM'); $tree ??= $this->tree_service->all()[$default] ?? $this->tree_service->all()->first(); $status_code = $exception->getCode(); // If this was a GET request, then we were probably fetching HTML to display, for // example a chart or tab. if ( $request->getHeaderLine('X-Requested-With') !== '' && $request->getMethod() === RequestMethodInterface::METHOD_GET ) { $this->layout = 'layouts/ajax'; $status_code = StatusCodeInterface::STATUS_OK; } return $this->viewResponse('components/alert-danger', [ 'alert' => $exception->getMessage(), 'title' => $exception->getMessage(), 'tree' => $tree, ], $status_code); } /** * @param ServerRequestInterface $request * @param Throwable $exception * * @return ResponseInterface */ private function thirdPartyExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface { $tree = Validator::attributes($request)->treeOptional(); $default = Site::getPreference('DEFAULT_GEDCOM'); $tree = $tree ?? $this->tree_service->all()[$default] ?? $this->tree_service->all()->first(); if ($request->getHeaderLine('X-Requested-With') !== '') { $this->layout = 'layouts/ajax'; } return $this->viewResponse('components/alert-danger', [ 'alert' => $exception->getMessage(), 'title' => $exception->getMessage(), 'tree' => $tree, ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); } /** * @param ServerRequestInterface $request * @param Throwable $exception * * @return ResponseInterface */ private function unhandledExceptionResponse(ServerRequestInterface $request, Throwable $exception): ResponseInterface { $this->layout = 'layouts/default'; // Create a stack dump for the exception $base_path = dirname(__DIR__, 3); $trace = $exception->getMessage() . ' ' . $exception->getFile() . ':' . $exception->getLine() . PHP_EOL . $exception->getTraceAsString(); $trace = str_replace($base_path, '…', $trace); // User data may contain non UTF-8 characters. $trace = mb_convert_encoding($trace, 'UTF-8', 'UTF-8'); $trace = e($trace); $trace = preg_replace('/^.*modules_v4.*$/m', '$0', $trace); try { Log::addErrorLog($trace); } catch (Throwable $ignore) { // Must have been a problem with the database. Nothing we can do here. } if ($request->getHeaderLine('X-Requested-With') !== '') { // If this was a GET request, then we were probably fetching HTML to display, for // example a chart or tab. if ($request->getMethod() === RequestMethodInterface::METHOD_GET) { $status_code = StatusCodeInterface::STATUS_OK; } else { $status_code = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR; } return response(view('components/alert-danger', ['alert' => $trace]), $status_code); } try { // Try with a full header/menu return $this->viewResponse('errors/unhandled-exception', [ 'title' => 'Error', 'error' => $trace, 'request' => $request, 'tree' => Validator::attributes($request)->treeOptional(), ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); } catch (Throwable $ignore) { // Try with a minimal header/menu return $this->viewResponse('errors/unhandled-exception', [ 'title' => 'Error', 'error' => $trace, 'request' => $request, 'tree' => null, ], StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); } } }