From d09de83ea5a950855696b3ff9b74cddc44636ff1 Mon Sep 17 00:00:00 2001 From: alexweissman Date: Mon, 29 May 2017 22:45:34 -0400 Subject: [PATCH] rework error-handling system --- .../AuthCompromisedExceptionHandler.php | 34 ++ .../Handler/AuthExpiredExceptionHandler.php | 50 +++ .../Handler/ForbiddenExceptionHandler.php | 31 ++ .../AuthCompromisedExceptionHandler.php | 40 -- .../Handler/AuthExpiredExceptionHandler.php | 63 ---- .../src/Handler/ForbiddenExceptionHandler.php | 66 ---- .../src/ServicesProvider/ServicesProvider.php | 6 +- .../src/Error/ExceptionHandlerManager.php | 88 +++++ .../src/Error/Handler/ExceptionHandler.php | 329 ++++++++++++++++ .../Handler/ExceptionHandlerInterface.php | 32 ++ .../Error/Handler/HttpExceptionHandler.php | 58 +++ .../Handler/PhpMailerExceptionHandler.php | 30 ++ .../core/src/Error/Renderer/ErrorRenderer.php | 64 ++++ .../Error/Renderer/ErrorRendererInterface.php | 29 ++ .../core/src/Error/Renderer/HtmlRenderer.php | 154 ++++++++ .../core/src/Error/Renderer/JsonRenderer.php | 57 +++ .../src/Error/Renderer/PlainTextRenderer.php | 65 ++++ .../core/src/Error/Renderer/XmlRenderer.php | 48 +++ .../core/src/Handler/CoreErrorHandler.php | 355 ------------------ .../core/src/Handler/ExceptionHandler.php | 103 ----- .../src/Handler/ExceptionHandlerInterface.php | 21 -- .../core/src/Handler/HttpExceptionHandler.php | 84 ----- .../core/src/Handler/PDOExceptionHandler.php | 73 ---- .../src/Handler/PhpMailerExceptionHandler.php | 73 ---- .../src/ServicesProvider/ServicesProvider.php | 15 +- .../src/{Handler => Util}/ShutdownHandler.php | 3 +- 26 files changed, 1080 insertions(+), 891 deletions(-) create mode 100644 app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php create mode 100644 app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php create mode 100644 app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php delete mode 100644 app/sprinkles/account/src/Handler/AuthCompromisedExceptionHandler.php delete mode 100644 app/sprinkles/account/src/Handler/AuthExpiredExceptionHandler.php delete mode 100644 app/sprinkles/account/src/Handler/ForbiddenExceptionHandler.php create mode 100755 app/sprinkles/core/src/Error/ExceptionHandlerManager.php create mode 100644 app/sprinkles/core/src/Error/Handler/ExceptionHandler.php create mode 100644 app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php create mode 100644 app/sprinkles/core/src/Error/Handler/HttpExceptionHandler.php create mode 100644 app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php create mode 100644 app/sprinkles/core/src/Error/Renderer/ErrorRenderer.php create mode 100755 app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php create mode 100644 app/sprinkles/core/src/Error/Renderer/HtmlRenderer.php create mode 100755 app/sprinkles/core/src/Error/Renderer/JsonRenderer.php create mode 100755 app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php create mode 100755 app/sprinkles/core/src/Error/Renderer/XmlRenderer.php delete mode 100755 app/sprinkles/core/src/Handler/CoreErrorHandler.php delete mode 100644 app/sprinkles/core/src/Handler/ExceptionHandler.php delete mode 100644 app/sprinkles/core/src/Handler/ExceptionHandlerInterface.php delete mode 100644 app/sprinkles/core/src/Handler/HttpExceptionHandler.php delete mode 100644 app/sprinkles/core/src/Handler/PDOExceptionHandler.php delete mode 100644 app/sprinkles/core/src/Handler/PhpMailerExceptionHandler.php rename app/sprinkles/core/src/{Handler => Util}/ShutdownHandler.php (96%) diff --git a/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php b/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php new file mode 100644 index 000000000..330ca65a6 --- /dev/null +++ b/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php @@ -0,0 +1,34 @@ +ci->view->getEnvironment()->loadTemplate('pages/error/compromised.html.twig'); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render()); + } +} diff --git a/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php b/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php new file mode 100644 index 000000000..c651f77df --- /dev/null +++ b/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php @@ -0,0 +1,50 @@ +writeAlerts(); + + $response = $this->response; + + // For non-AJAX requests, we forward the user to the login page. + if (!$this->request->isXhr()) { + $uri = $this->request->getUri(); + $path = $uri->getPath(); + $query = $uri->getQuery(); + $fragment = $uri->getFragment(); + + $path = $path + . ($query ? '?' . $query : '') + . ($fragment ? '#' . $fragment : ''); + + $loginPage = $this->ci->router->pathFor('login', [], [ + 'redirect' => $path + ]); + + $response = $response->withRedirect($loginPage); + } + + return $response; + } +} diff --git a/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php b/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php new file mode 100644 index 000000000..e22f02bf5 --- /dev/null +++ b/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php @@ -0,0 +1,31 @@ +logFlag = false; - - return $this->ci->view->render($response, 'pages/error/compromised.html.twig') - ->withStatus($exception->getHttpErrorCode()) - ->withHeader('Content-Type', 'text/html'); - } -} diff --git a/app/sprinkles/account/src/Handler/AuthExpiredExceptionHandler.php b/app/sprinkles/account/src/Handler/AuthExpiredExceptionHandler.php deleted file mode 100644 index 38c917412..000000000 --- a/app/sprinkles/account/src/Handler/AuthExpiredExceptionHandler.php +++ /dev/null @@ -1,63 +0,0 @@ -getUserMessages(); - - // If the status code is 500, log the exception's message - if ($exception->getHttpErrorCode() == 500) { - $this->logFlag = true; - } else { - $this->logFlag = false; - } - - foreach ($messages as $message) { - $this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters); - } - - $uri = $request->getUri(); - $path = $uri->getPath(); - $query = $uri->getQuery(); - $fragment = $uri->getFragment(); - - $path = $path - . ($query ? '?' . $query : '') - . ($fragment ? '#' . $fragment : ''); - - $loginPage = $this->ci->router->pathFor('login', [], [ - 'redirect' => $path - ]); - - return $response->withRedirect($loginPage); - } -} diff --git a/app/sprinkles/account/src/Handler/ForbiddenExceptionHandler.php b/app/sprinkles/account/src/Handler/ForbiddenExceptionHandler.php deleted file mode 100644 index e3124247e..000000000 --- a/app/sprinkles/account/src/Handler/ForbiddenExceptionHandler.php +++ /dev/null @@ -1,66 +0,0 @@ -logFlag = true; - - $this->ci->alerts->addMessageTranslated('danger', 'ERROR.404.DESCRIPTION'); - - return $response->withStatus(404); - } - - /** - * Handler for exceptions raised during "standard" requests. - * - * Pretend like we couldn't find the requested resource and return 404. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function standardHandler($request, $response, $exception) - { - $this->logFlag = false; - - // Render a custom error page, if it exists - try { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/404.html.twig"); - } catch (\Twig_Error_Loader $e) { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/default.html.twig"); - } - - return $response->withStatus(404) - ->withHeader('Content-Type', 'text/html') - ->write($template->render([])); - } -} diff --git a/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php b/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php index 249396e63..6a425d122 100644 --- a/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php +++ b/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php @@ -85,9 +85,11 @@ public function register($container) */ $container->extend('errorHandler', function ($handler, $c) { // Register the ForbiddenExceptionHandler. - $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Handler\ForbiddenExceptionHandler'); + $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Error\Handler\ForbiddenExceptionHandler'); // Register the AuthExpiredExceptionHandler - $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Handler\AuthExpiredExceptionHandler'); + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthExpiredExceptionHandler'); + // Register the AuthCompromisedExceptionHandler. + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthCompromisedExceptionHandler'); return $handler; }); diff --git a/app/sprinkles/core/src/Error/ExceptionHandlerManager.php b/app/sprinkles/core/src/Error/ExceptionHandlerManager.php new file mode 100755 index 000000000..1eca79cd8 --- /dev/null +++ b/app/sprinkles/core/src/Error/ExceptionHandlerManager.php @@ -0,0 +1,88 @@ +ci = $ci; + $this->displayErrorDetails = (bool)$displayErrorDetails; + } + + /** + * Invoke error handler + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Exception $exception The caught Exception object + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, \Exception $exception) + { + // Default exception handler class + $handlerClass = '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler'; + + // Get the last matching registered handler class, and instantiate it + foreach ($this->exceptionHandlers as $exceptionClass => $matchedHandlerClass) { + if ($exception instanceof $exceptionClass) { + $handlerClass = $matchedHandlerClass; + } + } + + $handler = new $handlerClass($this->ci, $request, $response, $exception, $this->displayErrorDetails); + + return $handler->handle(); + } + + /** + * Register an exception handler for a specified exception class. + * + * The exception handler must implement \UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface. + * + * @param string $exceptionClass The fully qualified class name of the exception to handle. + * @param string $handlerClass The fully qualified class name of the assigned handler. + * @throws InvalidArgumentException If the registered handler fails to implement ExceptionHandlerInterface + */ + public function registerHandler($exceptionClass, $handlerClass) + { + if (!is_a($handlerClass, '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandlerInterface', true)) { + throw new \InvalidArgumentException("Registered exception handler must implement ExceptionHandlerInterface!"); + } + + $this->exceptionHandlers[$exceptionClass] = $handlerClass; + } +} diff --git a/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php b/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php new file mode 100644 index 000000000..9df253aab --- /dev/null +++ b/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php @@ -0,0 +1,329 @@ +ci = $ci; + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request); + $this->renderer = $this->determineRenderer(); + } + + /** + * Handle the caught exception. + * The handler may render a detailed debugging error page, a generic error page, write to logs, and/or add messages to the alert stream. + * + * @return ResponseInterface + */ + public function handle() + { + // If displayErrorDetails is set to true, we'll halt and immediately respond with a detailed debugging page. + // We do not log errors in this case. + if ($this->displayErrorDetails) { + $response = $this->renderDebugResponse(); + } else { + // Write exception to log + $this->writeToErrorLog(); + + // Render generic error page + $response = $this->renderGenericResponse(); + } + + // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream + if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + $this->writeAlerts(); + } + + return $response; + } + + /** + * Render a detailed response with debugging information. + * + * @return ResponseInterface + */ + public function renderDebugResponse() + { + $body = $this->renderer->renderWithBody(); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } + + /** + * Render a generic, user-friendly response without sensitive debugging information. + * + * @return ResponseInterface + */ + public function renderGenericResponse() + { + $messages = $this->determineUserMessages(); + $httpCode = $this->statusCode; + + try { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); + } catch (\Twig_Error_Loader $e) { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/default.html.twig"); + } + + return $this->response + ->withStatus($httpCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render([ + 'messages' => $messages + ])); + } + + /** + * Write to the error log + * + * @return void + */ + public function writeToErrorLog() + { + $renderer = new PlainTextRenderer($this->request, $this->response, $this->exception, true); + $error = $renderer->render(); + $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($error); + } + + /** + * Write user-friendly error messages to the alert message stream. + * + * @return void + */ + public function writeAlerts() + { + $messages = $this->determineUserMessages(); + + foreach ($messages as $message) { + $this->ci->alerts->addMessageTranslated('danger', $message->message, $message->parameters); + } + } + + /** + * Determine which content type we know about is wanted using Accept header + * + * Note: This method is a bare-bones implementation designed specifically for + * Slim's error handling requirements. Consider a fully-feature solution such + * as willdurand/negotiation for any other situation. + * + * @param ServerRequestInterface $request + * @return string + */ + protected function determineContentType(ServerRequestInterface $request) + { + // For AJAX requests, if AJAX debugging is turned on, always return html + if ($this->ci->config['site.debug.ajax'] && $this->request->isXhr()) { + return 'text/html'; + } + + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + $count = count($selectedContentTypes); + + if ($count) { + $current = current($selectedContentTypes); + + /** + * Ensure other supported content types take precedence over text/plain + * when multiple content types are provided via Accept header. + */ + if ($current === 'text/plain' && $count > 1) { + return next($selectedContentTypes); + } + + return $current; + } + + if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { + $mediaType = 'application/' . $matches[1]; + if (in_array($mediaType, $this->knownContentTypes)) { + return $mediaType; + } + } + + return 'text/html'; + } + + /** + * Determine which renderer to use based on content type + * Overloaded $renderer from calling class takes precedence over all + * + * @return ErrorRendererInterface + * + * @throws \RuntimeException + */ + protected function determineRenderer() + { + $renderer = $this->renderer; + + if ((!is_null($renderer) && !class_exists($renderer)) + || (!is_null($renderer) && !in_array('UserFrosting\Sprinkle\Core\Error\Renderer\ErrorRendererInterface', class_implements($renderer))) + ) { + throw new \RuntimeException(sprintf( + 'Non compliant error renderer provided (%s). ' . + 'Renderer must implement the ErrorRendererInterface', + $renderer + )); + } + + if (is_null($renderer)) { + switch ($this->contentType) { + case 'application/json': + $renderer = JsonRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XmlRenderer::class; + break; + + case 'text/plain': + $renderer = PlainTextRenderer::class; + break; + + default: + case 'text/html': + $renderer = HtmlRenderer::class; + break; + } + } + + return new $renderer($this->request, $this->response, $this->exception, $this->displayErrorDetails); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() + { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + return [ + new UserMessage("ERROR.SERVER") + ]; + } + + /** + * Monolog logging for errors + * + * @param $message + * @return void + */ + protected function logError($message) + { + $this->ci->errorLogger->error($message); + } +} diff --git a/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php b/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php new file mode 100644 index 000000000..b210bdb1d --- /dev/null +++ b/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php @@ -0,0 +1,32 @@ +statusCode != 500) { + return; + } + + parent::writeToErrorLog(); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() + { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } elseif ($this->exception instanceof HttpException) { + return $this->exception->getHttpErrorCode(); + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + // Grab messages from the exception + return $this->exception->getUserMessages(); + } +} diff --git a/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php b/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php new file mode 100644 index 000000000..45f0e8d59 --- /dev/null +++ b/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php @@ -0,0 +1,30 @@ +request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + abstract public function render(); + + /** + * @return Body + */ + public function renderWithBody() + { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; + } +} diff --git a/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php b/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php new file mode 100755 index 000000000..7af269ab5 --- /dev/null +++ b/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php @@ -0,0 +1,29 @@ +displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderException($this->exception); + + $html .= '

Your request

'; + $html .= $this->renderRequest(); + + $html .= '

Response headers

'; + $html .= $this->renderResponseHeaders(); + + while ($exception = $this->exception->getPrevious()) { + $html .= '

Previous exception

'; + $html .= $this->renderException($exception); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render a summary of the exception. + * + * @param Exception $exception + * @return string + */ + public function renderException($exception) + { + $html = sprintf('
Type: %s
', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render HTML representation of original request. + * + * @return string + */ + public function renderRequest() + { + $method = $this->request->getMethod(); + $uri = $this->request->getUri(); + $params = $this->request->getParams(); + $requestHeaders = $this->request->getHeaders(); + + $html = '

Request URI:

'; + + $html .= sprintf('
%s %s
', $method, $uri); + + $html .= '

Request parameters:

'; + + $html .= $this->renderTable($params); + + $html .= '

Request headers:

'; + + $html .= $this->renderTable($requestHeaders); + + return $html; + } + + /** + * Render HTML representation of response headers. + * + * @return string + */ + public function renderResponseHeaders() + { + $html = '

Response headers:

'; + $html .= 'Additional response headers may have been set by Slim after the error handling routine. Please check your browser console for a complete list.
'; + + $html .= $this->renderTable($this->response->getHeaders()); + + return $html; + } + + /** + * Render HTML representation of a table of data. + * + * @param mixed[] $data the array of data to render. + * + * @return string + */ + protected function renderTable($data) + { + $html = ''; + foreach ($data as $name => $value) { + $value = print_r($value, true); + $html .= ""; + } + $html .= '
NameValue
$name$value
'; + + return $html; + } +} diff --git a/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php b/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php new file mode 100755 index 000000000..3adfd459f --- /dev/null +++ b/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php @@ -0,0 +1,57 @@ +exception->getMessage(); + return $this->formatExceptionPayload($message); + } + + /** + * @param $message + * @return string + */ + public function formatExceptionPayload($message) + { + $e = $this->exception; + $error = ['message' => $message]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->formatExceptionFragment($e); + } while ($e = $e->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * @param \Exception|\Throwable $e + * @return array + */ + public function formatExceptionFragment($e) + { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + } +} diff --git a/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php b/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php new file mode 100755 index 000000000..e8f1d2c42 --- /dev/null +++ b/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php @@ -0,0 +1,65 @@ +displayErrorDetails) { + return $this->formatExceptionBody(); + } + + return $this->exception->getMessage(); + } + + public function formatExceptionBody() + { + $e = $this->exception; + + $text = 'Slim Application Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + + while ($e = $e->getPrevious()) { + $text .= PHP_EOL . 'Previous Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + } + + return $text; + } + + /** + * @param \Exception|\Throwable $e + * @return string + */ + public function formatExceptionFragment($e) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); + + if ($code = $e->getCode()) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + if ($message = $e->getMessage()) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + if ($file = $e->getFile()) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + if ($line = $e->getLine()) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + if ($trace = $e->getTraceAsString()) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } +} diff --git a/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php b/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php new file mode 100755 index 000000000..52e71cfe8 --- /dev/null +++ b/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php @@ -0,0 +1,48 @@ +exception; + $xml = "\n UserFrosting Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($e) . "\n"; + $xml .= " " . $e->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($e->getMessage()) . "\n"; + $xml .= " " . $e->getFile() . "\n"; + $xml .= " " . $e->getLine() . "\n"; + $xml .= " \n"; + } while ($e = $e->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} diff --git a/app/sprinkles/core/src/Handler/CoreErrorHandler.php b/app/sprinkles/core/src/Handler/CoreErrorHandler.php deleted file mode 100755 index 9538a509a..000000000 --- a/app/sprinkles/core/src/Handler/CoreErrorHandler.php +++ /dev/null @@ -1,355 +0,0 @@ -ci = $ci; - $this->displayErrorDetails = (bool)$displayErrorDetails; - } - - /** - * Invoke error handler - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, \Exception $exception) - { - // Default exception handler class - $handlerClass = '\UserFrosting\Sprinkle\Core\Handler\ExceptionHandler'; - - // Get the last matching registered handler class, and instantiate it - foreach ($this->exceptionHandlers as $exceptionClass => $matchedHandlerClass) { - if ($exception instanceof $exceptionClass) { - $handlerClass = $matchedHandlerClass; - } - } - - $handler = new $handlerClass($this->ci); - - // Run either the ajaxHandler or standardHandler, depending on the request type - if ($request->isXhr()) { - $response = $this->handleAjax($handler, $request, $response, $exception); - } else { - $response = $this->handleStandard($handler, $request, $response, $exception); - } - - return $response; - } - - /** - * Register an exception handler for a specified exception class. - * - * The exception handler must implement \UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface. - * - * @param string $exceptionClass The fully qualified class name of the exception to handle. - * @param string $handlerClass The fully qualified class name of the assigned handler. - * @throws InvalidArgumentException If the registered handler fails to implement ExceptionHandlerInterface - */ - public function registerHandler($exceptionClass, $handlerClass) - { - if (!is_a($handlerClass, '\UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface', true)) { - throw new \InvalidArgumentException("Registered exception handler must implement ExceptionHandlerInterface!"); - } - - $this->exceptionHandlers[$exceptionClass] = $handlerClass; - } - - /** - * Render a complete debugging message for this error. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * @param string $contentType The format of the message to be returned. - * - * @return ResponseInterface - */ - protected function getDebugMessage(ServerRequestInterface $request, ResponseInterface $response, \Exception $exception, $contentType = "text/html") - { - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonErrorMessage($exception); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlErrorMessage($exception); - break; - - case 'text/html': - $output = $this->renderHtmlErrorReport($request, $response, $exception); - break; - - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - return $output; - } - - /** - * Handle any errors/exceptions raised by an AJAX request. - * - * @param ExceptionHandlerInterface $handler The handler to use for processing this error. - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - protected function handleAjax(ExceptionHandlerInterface $handler, ServerRequestInterface $request, ResponseInterface $response, \Exception $exception) - { - $response = $handler->ajaxHandler($request, $response, $exception); - $enableLogging = true; - - // If displayErrorDetails is set to true, we'll run the ajaxHandler like normal, but append detailed error information to the response. - if ($this->displayErrorDetails) { - // Turn off logging and clear the message stream if AJAX debug mode is enabled. - if ($this->ci->config['site.debug.ajax']) { - $enableLogging = false; - $this->ci->alerts->resetMessageStream(); - } - - $contentType = $this->determineContentType($request); - - $output = $this->getDebugMessage($request, $response, $exception, $contentType); - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - $response = $response - ->withHeader('Content-type', $contentType) - ->withBody($body); - } - - // Write exception to log, if enabled by the handler and it is appropriate for the response type - if ($handler->getLogFlag() && $enableLogging) { - $this->writeToErrorLog($exception); - } - - return $response; - } - - /** - * Handle any errors/exceptions raised by a standard (non-AJAX) request. - * - * @param ExceptionHandlerInterface $handler The handler to use for processing this error. - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - protected function handleStandard(ExceptionHandlerInterface $handler, ServerRequestInterface $request, ResponseInterface $response, \Exception $exception) - { - // If displayErrorDetails is set to true, we'll halt and immediately respond with a detailed error page. - // We do not log errors in this case. - if ($this->displayErrorDetails) { - $contentType = $this->determineContentType($request); - - $output = $this->getDebugMessage($request, $response, $exception, $contentType); - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - $response = $response - ->withStatus(500) - ->withHeader('Content-type', $contentType) - ->withBody($body); - } else { - $response = $handler->standardHandler($request, $response, $exception); - - // Write exception to log, if enabled by the handler - if ($handler->getLogFlag()) { - $this->writeToErrorLog($exception); - } - } - - return $response; - } - - /** - * Alternative logging for errors - * - * @param $message - */ - protected function logError($message) - { - $this->ci->errorLogger->error($message); - } - - /** - * Render HTML error report. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return string - */ - protected function renderHtmlErrorReport(ServerRequestInterface $request, ResponseInterface $response, \Exception $exception) - { - $title = 'UserFrosting Application Error'; - - if ($this->displayErrorDetails) { - $html = '

The application could not run because of the following error:

'; - $html .= '

Details

'; - $html .= $this->renderHtmlException($exception); - - $html .= '

Your request

'; - $html .= $this->renderHtmlRequest($request); - - $html .= '

Response headers

'; - $html .= $this->renderHtmlResponseHeaders($response); - - while ($exception = $exception->getPrevious()) { - $html .= '

Previous exception

'; - $html .= $this->renderHtmlException($exception); - } - } else { - $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; - } - - $output = sprintf( - "" . - "%s

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render HTML representation of original request. - * - * @param ServerRequestInterface $request The most recent Request object - * - * @return string - */ - protected function renderHtmlRequest(ServerRequestInterface $request) - { - $method = $request->getMethod(); - $uri = $request->getUri(); - $params = $request->getParams(); - $requestHeaders = $request->getHeaders(); - - $html = '

Request URI:

'; - - $html .= sprintf('
%s %s
', $method, $uri); - - $html .= '

Request parameters:

'; - - $html .= $this->renderHtmlTable($params); - - $html .= '

Request headers:

'; - - $html .= $this->renderHtmlTable($requestHeaders); - - return $html; - } - - /** - * Render HTML representation of response headers. - * - * @param ResponseInterface $response The most recent Response object - * - * @return string - */ - protected function renderHtmlResponseHeaders(ResponseInterface $response) - { - $html = '

Response headers:

'; - $html .= 'Additional response headers may have been set by Slim after the error handling routine. Please check your browser console for a complete list.
'; - - $html .= $this->renderHtmlTable($response->getHeaders()); - - return $html; - } - - /** - * Render HTML representation of a table of data. - * - * @param mixed[] $data the array of data to render. - * - * @return string - */ - protected function renderHtmlTable($data) - { - $html = ''; - foreach ($data as $name => $value) { - $value = print_r($value, true); - $html .= ""; - } - $html .= '
NameValue
$name$value
'; - - return $html; - } - - /** - * Write to the error log. - * - * @param \Exception|\Throwable $throwable - * - * @return void - */ - protected function writeToErrorLog($throwable) - { - $message = 'Slim Application Error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($throwable); - while ($throwable = $throwable->getPrevious()) { - $message .= PHP_EOL . 'Previous error:' . PHP_EOL; - $message .= $this->renderThrowableAsText($throwable); - } - - $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; - - $this->logError($message); - } -} diff --git a/app/sprinkles/core/src/Handler/ExceptionHandler.php b/app/sprinkles/core/src/Handler/ExceptionHandler.php deleted file mode 100644 index 894cd6530..000000000 --- a/app/sprinkles/core/src/Handler/ExceptionHandler.php +++ /dev/null @@ -1,103 +0,0 @@ -ci = $ci; - } - - /** - * Called when an exception is raised during AJAX requests. - * - * Adds a generic error to the message stream, and respond with a 500 status code. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function ajaxHandler($request, $response, $exception) - { - $message = new UserMessage("ERROR.SERVER"); - - $this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters); - - return $response->withStatus(500); - } - - /** - * Handler for exceptions raised during "standard" requests. - * - * Modifies the response, attempting to render an error page with status code 500. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function standardHandler($request, $response, $exception) - { - $messages = [ - new UserMessage("ERROR.SERVER") - ]; - $httpCode = 500; - - // Render a custom error page, if it exists - try { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); - } catch (\Twig_Error_Loader $e) { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/default.html.twig"); - } - - return $response->withStatus($httpCode) - ->withHeader('Content-Type', 'text/html') - ->write($template->render([ - "messages" => $messages - ])); - } - - /** - * Gets the logging flag for this handler. - * - * @return bool - */ - public function getLogFlag() - { - return $this->logFlag; - } - -} \ No newline at end of file diff --git a/app/sprinkles/core/src/Handler/ExceptionHandlerInterface.php b/app/sprinkles/core/src/Handler/ExceptionHandlerInterface.php deleted file mode 100644 index 4d9ab78a1..000000000 --- a/app/sprinkles/core/src/Handler/ExceptionHandlerInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -getUserMessages(); - $httpCode = $exception->getHttpErrorCode(); - - // If the status code is 500, log the exception's message - if ($httpCode == 500) { - $this->logFlag = true; - } else { - $this->logFlag = false; - } - - foreach ($messages as $message) { - $this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters); - } - - return $response->withStatus($httpCode); - } - - /** - * Handler for exceptions raised during "standard" requests. - * - * Modifies the response, attempting to render the specific error page for the HttpException's error code. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function standardHandler($request, $response, $exception) - { - $messages = $exception->getUserMessages(); - $httpCode = $exception->getHttpErrorCode(); - - // If the status code is 500, log the exception's message - if ($httpCode == 500) { - $this->logFlag = true; - } else { - $this->logFlag = false; - } - - // Render a custom error page, if it exists - try { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); - } catch (\Twig_Error_Loader $e) { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/default.html.twig"); - } - - return $response->withStatus($httpCode) - ->withHeader('Content-Type', 'text/html') - ->write($template->render([ - "messages" => $messages - ])); - } -} diff --git a/app/sprinkles/core/src/Handler/PDOExceptionHandler.php b/app/sprinkles/core/src/Handler/PDOExceptionHandler.php deleted file mode 100644 index f666f3a5f..000000000 --- a/app/sprinkles/core/src/Handler/PDOExceptionHandler.php +++ /dev/null @@ -1,73 +0,0 @@ -logFlag = true; - - $this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters); - - return $response->withStatus(500); - } - - /** - * Handler for exceptions raised during "standard" requests. - * - * Modifies the response, attempting to render the specific error page for the HttpException's error code. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function standardHandler($request, $response, $exception) - { - $messages = [ - new UserMessage("ERROR.SERVER") - ]; - $httpCode = 500; - - $this->logFlag = true; - - $view = $this->ci->view; - - $response = $view->render($response, 'pages/error/default.html.twig', [ - "messages" => $messages - ]) - ->withStatus($httpCode) - ->withHeader('Content-Type', 'text/html'); - - return $response; - } - -} \ No newline at end of file diff --git a/app/sprinkles/core/src/Handler/PhpMailerExceptionHandler.php b/app/sprinkles/core/src/Handler/PhpMailerExceptionHandler.php deleted file mode 100644 index 55ab5de0d..000000000 --- a/app/sprinkles/core/src/Handler/PhpMailerExceptionHandler.php +++ /dev/null @@ -1,73 +0,0 @@ -logFlag = true; - - $this->ci->alerts->addMessageTranslated("danger", $message->message, $message->parameters); - - return $response->withStatus(500); - } - - /** - * Handler for exceptions raised during "standard" requests. - * - * Modifies the response, attempting to render the specific error page for the HttpException's error code. - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object - * - * @return ResponseInterface - */ - public function standardHandler($request, $response, $exception) - { - $messages = [ - new UserMessage("MAIL_ERROR") - ]; - $httpCode = 500; - - $this->logFlag = true; - - $view = $this->ci->view; - - $response = $view->render($response, 'pages/error/default.html.twig', [ - "messages" => $messages - ]) - ->withStatus($httpCode) - ->withHeader('Content-Type', 'text/html'); - - return $response; - } - -} diff --git a/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php b/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php index 220658588..fa8228da8 100755 --- a/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php +++ b/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php @@ -37,17 +37,17 @@ use UserFrosting\Assets\UrlBuilder\CompiledAssetUrlBuilder; use UserFrosting\I18n\MessageTranslator; use UserFrosting\Session\Session; -use UserFrosting\Sprinkle\Core\Twig\CoreExtension; -use UserFrosting\Sprinkle\Core\Handler\ShutdownHandler; -use UserFrosting\Sprinkle\Core\Handler\CoreErrorHandler; +use UserFrosting\Sprinkle\Core\Error\ExceptionHandlerManager; use UserFrosting\Sprinkle\Core\Log\MixedFormatter; use UserFrosting\Sprinkle\Core\Mail\Mailer; use UserFrosting\Sprinkle\Core\MessageStream; use UserFrosting\Sprinkle\Core\Router; use UserFrosting\Sprinkle\Core\Throttle\Throttler; use UserFrosting\Sprinkle\Core\Throttle\ThrottleRule; +use UserFrosting\Sprinkle\Core\Twig\CoreExtension; use UserFrosting\Sprinkle\Core\Util\CheckEnvironment; use UserFrosting\Sprinkle\Core\Util\ClassMapper; +use UserFrosting\Sprinkle\Core\Util\ShutdownHandler; use UserFrosting\Support\Exception\BadRequestException; /** @@ -332,16 +332,13 @@ public function register(ContainerInterface $container) $container['errorHandler'] = function ($c) { $settings = $c->settings; - $handler = new CoreErrorHandler($c, $settings['displayErrorDetails']); + $handler = new ExceptionHandlerManager($c, $settings['displayErrorDetails']); // Register the HttpExceptionHandler. - $handler->registerHandler('\UserFrosting\Support\Exception\HttpException', '\UserFrosting\Sprinkle\Core\Handler\HttpExceptionHandler'); - - // Register the PDOExceptionHandler. - $handler->registerHandler('\PDOException', '\UserFrosting\Sprinkle\Core\Handler\PDOExceptionHandler'); + $handler->registerHandler('\UserFrosting\Support\Exception\HttpException', '\UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler'); // Register the PhpMailerExceptionHandler. - $handler->registerHandler('\phpmailerException', '\UserFrosting\Sprinkle\Core\Handler\PhpMailerExceptionHandler'); + $handler->registerHandler('\phpmailerException', '\UserFrosting\Sprinkle\Core\Error\Handler\PhpMailerExceptionHandler'); return $handler; }; diff --git a/app/sprinkles/core/src/Handler/ShutdownHandler.php b/app/sprinkles/core/src/Util/ShutdownHandler.php similarity index 96% rename from app/sprinkles/core/src/Handler/ShutdownHandler.php rename to app/sprinkles/core/src/Util/ShutdownHandler.php index ffba4ff97..0d9f3bf90 100755 --- a/app/sprinkles/core/src/Handler/ShutdownHandler.php +++ b/app/sprinkles/core/src/Util/ShutdownHandler.php @@ -3,10 +3,9 @@ * UserFrosting (http://www.userfrosting.com) * * @link https://github.com/userfrosting/UserFrosting - * @copyright Copyright (c) 2013-2016 Alexander Weissman * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) */ -namespace UserFrosting\Sprinkle\Core\Handler; +namespace UserFrosting\Sprinkle\Core\Util; use Interop\Container\ContainerInterface; /**