diff --git a/.travis.yml b/.travis.yml index 2ee616260..9f5de8386 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,3 +24,5 @@ after_script: notifications: slack: slimphp:0RNzx2JuhkAqIf0MXcUZ0asT + +dist: trusty diff --git a/Slim/App.php b/Slim/App.php index 187f9f990..147a9d2ed 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -8,22 +8,21 @@ */ namespace Slim; -use Exception; use FastRoute\Dispatcher; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Slim\Exception\InvalidMethodException; -use Slim\Exception\MethodNotAllowedException; -use Slim\Exception\NotFoundException; -use Slim\Handlers\Error; -use Slim\Handlers\NotAllowed; -use Slim\Handlers\NotFound; -use Slim\Handlers\PhpError; +use Slim\Exception\HttpException; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpNotAllowedException; +use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; +use Slim\Interfaces\ErrorHandlerInterface; use Slim\Interfaces\RouteGroupInterface; use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouterInterface; +use BadMethodCallException; +use Exception; use Throwable; /** @@ -54,26 +53,6 @@ class App */ protected $router; - /** - * @var callable - */ - protected $notFoundHandler; - - /** - * @var callable - */ - protected $notAllowedHandler; - - /** - * @var callable - */ - protected $errorHandler; - - /** - * @var callable - */ - protected $phpErrorHandler; - /** * @var array */ @@ -84,6 +63,8 @@ class App 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, + 'defaultErrorHandler' => null, + 'errorHandlers' => [] ]; /******************************************************************************** @@ -97,6 +78,7 @@ class App */ public function __construct(array $settings = []) { + $this->addSettings($settings); $this->container = new Container(); } @@ -156,7 +138,7 @@ public function __call($method, $args) } } - throw new \BadMethodCallException("Method $method is not a valid method"); + throw new BadMethodCallException("Method $method is not a valid method"); } /******************************************************************************** @@ -278,144 +260,181 @@ public function getRouter() } /** - * Set callable to handle scenarios where a suitable - * route does not match the current request. + * Set callable to handle scenarios where an error + * occurs when processing the current request. * * This service MUST return a callable that accepts - * two arguments: + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string $type + * @param callable $callable + * + * @throws \RuntimeException */ - public function setNotFoundHandler(callable $handler) + public function setErrorHandler($type, $callable) { - $this->notFoundHandler = $handler; + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); + $handlers = $this->getErrorHandlers(); + $handlers[$type] = $handler; + $this->addSetting('errorHandlers', $handlers); } /** - * Get callable to handle scenarios where a suitable - * route does not match the current request. + * Get callable to handle scenarios where an error + * occurs when processing the current request. + * + * @param string $type + * @return callable * - * @return callable|Error + * @throws \RuntimeException */ - public function getNotFoundHandler() + public function getErrorHandler($type) { - if (!$this->notFoundHandler) { - $this->notFoundHandler = new NotFound(); + $handlers = $this->getErrorHandlers(); + + if (isset($handlers[$type])) { + $handler = $handlers[$type]; + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); } - return $this->notFoundHandler; + return $this->getDefaultErrorHandler(); } /** - * Set callable to handle scenarios where a suitable - * route matches the request URI but not the request method. + * Retrieve error handler array from settings + * + * @returns array + * + * @throws \RuntimeException + */ + protected function getErrorHandlers() + { + $handlers = $this->getSetting('errorHandlers', []); + + if (!is_array($handlers)) { + throw new \RuntimeException('Slim application setting "errorHandlers" should be an array.'); + } + + return $handlers; + } + + + /** + * Set callable as the default Slim application error handler. * * This service MUST return a callable that accepts - * three arguments: + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Array of allowed HTTP methods + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param callable $callable + * + * @throws \RuntimeException */ - public function setNotAllowedHandler(callable $handler) + public function setDefaultErrorHandler($callable) { - $this->notAllowedHandler = $handler; + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); + $this->addSetting('defaultErrorHandler', $handler); } /** - * Get callable to handle scenarios where a suitable - * route matches the request URI but not the request method. + * Get the default error handler from settings. * - * @return callable|Error + * @return callable|ErrorHandler */ - public function getNotAllowedHandler() + public function getDefaultErrorHandler() { - if (!$this->notAllowedHandler) { - $this->notAllowedHandler = new NotAllowed(); + $handler = $this->getSetting('defaultErrorHandler', null); + + if (!is_null($handler)) { + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); } - return $this->notAllowedHandler; + return new ErrorHandler(); } /** - * Set callable to handle scenarios where an error - * occurs when processing the current request. + * Set callable to handle scenarios where a suitable + * route does not match the current request. * - * This service MUST return a callable that accepts three arguments: + * This service MUST return a callable that accepts + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Error + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * * @param callable $handler */ - public function setErrorHandler(callable $handler) + public function setNotFoundHandler($handler) { - $this->errorHandler = $handler; + $this->setErrorHandler(HttpNotFoundException::class, $handler); } /** - * Get callable to handle scenarios where an error - * occurs when processing the current request. + * Get callable to handle scenarios where a suitable + * route does not match the current request. * - * @return callable|Error + * @return callable|ErrorHandlerInterface */ - public function getErrorHandler() + public function getNotFoundHandler() { - if (!$this->errorHandler) { - $this->errorHandler = new Error($this->getSetting('displayErrorDetails')); - } - - return $this->errorHandler; + return $this->getErrorHandler(HttpNotFoundException::class); } /** - * Set callable to handle scenarios where a PHP error - * occurs when processing the current request. + * Set callable to handle scenarios where a suitable + * route matches the request URI but not the request method. * - * This service MUST return a callable that accepts three arguments: + * This service MUST return a callable that accepts + * three arguments optionally four arguments. * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Error + * 3. Instance of \Exception + * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string|callable $handler */ - public function setPhpErrorHandler(callable $handler) + public function setNotAllowedHandler($handler) { - $this->phpErrorHandler = $handler; + $this->setErrorHandler(HttpNotAllowedException::class, $handler); } /** - * Get callable to handle scenarios where a PHP error - * occurs when processing the current request. + * Get callable to handle scenarios where a suitable + * route matches the request URI but not the request method. * - * @return callable|Error + * @return callable */ - public function getPhpErrorHandler() + public function getNotAllowedHandler() { - if (!$this->phpErrorHandler) { - $this->phpErrorHandler = new PhpError($this->getSetting('displayErrorDetails')); - } - - return $this->phpErrorHandler; + return $this->getErrorHandler(HttpNotAllowedException::class); } /******************************************************************************** @@ -575,17 +594,18 @@ public function group($pattern, $callable) * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function run($silent = false) { $response = $this->container->get('response'); + $request = $this->container->get('request'); try { - $response = $this->process($this->container->get('request'), $response); - } catch (InvalidMethodException $e) { - $response = $this->processInvalidMethod($e->getRequest(), $response); + $response = $this->process($request, $response); + } catch (Exception $e) { + $response = $this->handleException($e, $request, $response); + } catch (Throwable $e) { + $response = $this->handleException($e, $request, $response); } if (!$silent) { @@ -595,35 +615,6 @@ public function run($silent = false) return $response; } - /** - * Pull route info for a request with a bad method to decide whether to - * return a not-found error (default) or a bad-method error, then run - * the handler for that error, returning the resulting response. - * - * Used for cases where an incoming request has an unrecognized method, - * rather than throwing an exception and not catching it all the way up. - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - */ - protected function processInvalidMethod(ServerRequestInterface $request, ResponseInterface $response) - { - $router = $this->getRouter(); - $request = $this->dispatchRouterAndPrepareRoute($request, $router); - $routeInfo = $request->getAttribute('routeInfo', [RouterInterface::DISPATCH_STATUS => Dispatcher::NOT_FOUND]); - - if ($routeInfo[RouterInterface::DISPATCH_STATUS] === Dispatcher::METHOD_NOT_ALLOWED) { - return $this->handleException( - new MethodNotAllowedException($request, $response, $routeInfo[RouterInterface::ALLOWED_METHODS]), - $request, - $response - ); - } - - return $this->handleException(new NotFoundException($request, $response), $request, $response); - } - /** * Process a request * @@ -635,8 +626,6 @@ protected function processInvalidMethod(ServerRequestInterface $request, Respons * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function process(ServerRequestInterface $request, ResponseInterface $response) { @@ -649,14 +638,7 @@ public function process(ServerRequestInterface $request, ResponseInterface $resp } // Traverse middleware stack - try { - $response = $this->callMiddlewareStack($request, $response); - } catch (Exception $e) { - $response = $this->handleException($e, $request, $response); - } catch (Throwable $e) { - $response = $this->handlePhpError($e, $request, $response); - } - + $response = $this->callMiddlewareStack($request, $response); $response = $this->finalize($response); return $response; @@ -750,16 +732,25 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $routeInfo = $request->getAttribute('routeInfo'); } - if ($routeInfo[0] === Dispatcher::FOUND) { - $route = $router->lookupRoute($routeInfo[1]); - return $route->run($request, $response); - } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - $notAllowedHandler = $this->getNotAllowedHandler(); - return $notAllowedHandler($request, $response, $routeInfo[1]); + $exception = null; + switch ($routeInfo[0]) { + case Dispatcher::FOUND: + $route = $router->lookupRoute($routeInfo[1]); + return $route->run($request, $response); + + case Dispatcher::METHOD_NOT_ALLOWED: + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods($routeInfo[1]); + $exception->setRequest($request); + break; + + case Dispatcher::NOT_FOUND: + $exception = new HttpNotFoundException; + $exception->setRequest($request); + break; } - $notFoundHandler = $this->getNotFoundHandler(); - return $notFoundHandler($request, $response); + return $this->handleException($exception, $request, $response); } /** @@ -791,13 +782,59 @@ protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request return $request->withAttribute('routeInfo', $routeInfo); } + /** + * Resolve custom error handler from container or use default ErrorHandler + * @param Exception|Throwable $exception + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ + public function handleException($exception, ServerRequestInterface $request, ResponseInterface $response) + { + $exceptionType = get_class($exception); + $handler = $this->getErrorHandler($exceptionType); + $displayErrorDetails = $this->getSetting('displayErrorDetails'); + + /** + * Retrieve request object from exception + * and replace current request object if not null + */ + if (method_exists($exception, 'getRequest')) { + $r = $exception->getRequest(); + if (!is_null($r)) { + $request = $r; + } + } + + /** + * Check if exception is instance of HttpException + * If so, check if it is not recoverable + * End request immediately in case it is not recoverable + */ + $recoverable = true; + if ($exception instanceof HttpException) { + $recoverable = $exception->isRecoverable(); + } + + $params = [ + $request, + $response, + $exception, + $displayErrorDetails + ]; + + return $recoverable + ? call_user_func_array($handler, $params) + : $response; + } + /** * Finalize response * * @param ResponseInterface $response * @return ResponseInterface * - * @throws RuntimeException + * @throws \RuntimeException */ protected function finalize(ResponseInterface $response) { @@ -840,50 +877,4 @@ protected function isEmptyResponse(ResponseInterface $response) return in_array($response->getStatusCode(), [204, 205, 304]); } - - /** - * Call relevant handler from the Container if needed. If it doesn't exist, - * then just re-throw. - * - * @param Exception $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * - * @return ResponseInterface - * @throws Exception if a handler is needed and not found - */ - protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response) - { - if ($e instanceof MethodNotAllowedException) { - $handler = $this->getNotAllowedHandler(); - $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()]; - } elseif ($e instanceof NotFoundException) { - $handler = $this->getNotFoundHandler(); - $params = [$e->getRequest(), $e->getResponse()]; - } else { - // Other exception, use $request and $response params - $handler = $this->getErrorHandler(); - $params = [$request, $response, $e]; - } - - return call_user_func_array($handler, $params); - } - - /** - * Call relevant handler from the Container if needed. If it doesn't exist, - * then just re-throw. - * - * @param Throwable $e - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @return ResponseInterface - * @throws Throwable - */ - protected function handlePhpError(Throwable $e, ServerRequestInterface $request, ResponseInterface $response) - { - $handler = $this->getPhpErrorHandler(); - $params = [$request, $response, $e]; - - return call_user_func_array($handler, $params); - } } diff --git a/Slim/DefaultServicesProvider.php b/Slim/DefaultServicesProvider.php index 36475de87..f973cf613 100644 --- a/Slim/DefaultServicesProvider.php +++ b/Slim/DefaultServicesProvider.php @@ -104,85 +104,6 @@ public function register($container) }; } - if (!isset($container['phpErrorHandler'])) { - /** - * This service MUST return a callable - * that accepts three arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Error - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @param Container $container - * - * @return callable - */ - $container['phpErrorHandler'] = function ($container) { - return new PhpError($container->get('settings')['displayErrorDetails']); - }; - } - - if (!isset($container['errorHandler'])) { - /** - * This service MUST return a callable - * that accepts three arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of \Exception - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @param Container $container - * - * @return callable - */ - $container['errorHandler'] = function ($container) { - return new Error($container->get('settings')['displayErrorDetails']); - }; - } - - if (!isset($container['notFoundHandler'])) { - /** - * This service MUST return a callable - * that accepts two arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @return callable - */ - $container['notFoundHandler'] = function () { - return new NotFound; - }; - } - - if (!isset($container['notAllowedHandler'])) { - /** - * This service MUST return a callable - * that accepts three arguments: - * - * 1. Instance of \Psr\Http\Message\ServerRequestInterface - * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Array of allowed HTTP methods - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @return callable - */ - $container['notAllowedHandler'] = function () { - return new NotAllowed; - }; - } - if (!isset($container['callableResolver'])) { /** * Instance of \Slim\Interfaces\CallableResolverInterface diff --git a/Slim/Exception/HttpBadGatewayException.php b/Slim/Exception/HttpBadGatewayException.php new file mode 100644 index 000000000..30563c2c3 --- /dev/null +++ b/Slim/Exception/HttpBadGatewayException.php @@ -0,0 +1,10 @@ +details = $details; + } + } + + /** + * @param ServerRequestInterface $request + */ + public function setRequest(ServerRequestInterface $request) + { + $this->request = $request; + } + + /** + * @param ResponseInterface $response + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } + + /** + * @param array $details + */ + public function setDetails(array $details) + { + $this->details = $details; + } + + /** + * @param string $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + public function notRecoverable() + { + $this->recoverable = false; + } + + /** + * @return null|ServerRequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * @return null|ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * @return array|null|string + */ + public function getDetails() + { + return $this->details; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isRecoverable() + { + return $this->recoverable; + } +} diff --git a/Slim/Exception/HttpForbiddenException.php b/Slim/Exception/HttpForbiddenException.php new file mode 100644 index 000000000..a9f64173a --- /dev/null +++ b/Slim/Exception/HttpForbiddenException.php @@ -0,0 +1,10 @@ +getDetails(); + + if (isset($details['allowedMethods'])) { + $allowedMethods = $details['allowedMethods']; + + if (is_array($allowedMethods)) { + return implode(', ', $allowedMethods); + } elseif (is_string($allowedMethods)) { + return $allowedMethods; + } + + return ''; + } + } + + /** + * @param string|array $methods + */ + public function setAllowedMethods($methods) + { + if (is_null($this->details)) { + $this->details = []; + } + + $this->details['allowedMethods'] = $methods; + } +} diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php new file mode 100644 index 000000000..614c001ce --- /dev/null +++ b/Slim/Exception/HttpNotFoundException.php @@ -0,0 +1,10 @@ +request = $request; - parent::__construct(sprintf('Unsupported HTTP method "%s" provided', $method)); - } - - public function getRequest() - { - return $this->request; - } -} diff --git a/Slim/Exception/MethodNotAllowedException.php b/Slim/Exception/MethodNotAllowedException.php deleted file mode 100644 index a737ab941..000000000 --- a/Slim/Exception/MethodNotAllowedException.php +++ /dev/null @@ -1,81 +0,0 @@ -request = $request; - $this->response = $response; - $this->allowedMethods = $allowedMethods; - } - - /** - * Get request - * - * @return ServerRequestInterface - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response - * - * @return ResponseInterface - */ - public function getResponse() - { - return $this->response; - } - - /** - * Get allowed methods - * - * @return string[] - */ - public function getAllowedMethods() - { - return $this->allowedMethods; - } -} diff --git a/Slim/Exception/NotFoundException.php b/Slim/Exception/NotFoundException.php deleted file mode 100644 index 527ba6629..000000000 --- a/Slim/Exception/NotFoundException.php +++ /dev/null @@ -1,62 +0,0 @@ -request = $request; - $this->response = $response; - } - - /** - * Get request - * - * @return ServerRequestInterface - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response - * - * @return ResponseInterface - */ - public function getResponse() - { - return $this->response; - } -} diff --git a/Slim/Handlers/AbstractError.php b/Slim/Handlers/AbstractError.php deleted file mode 100644 index 42f8dde3d..000000000 --- a/Slim/Handlers/AbstractError.php +++ /dev/null @@ -1,99 +0,0 @@ -displayErrorDetails = (bool) $displayErrorDetails; - } - - /** - * Write to the error log if displayErrorDetails is false - * - * @param \Exception|\Throwable $throwable - * - * @return void - */ - protected function writeToErrorLog($throwable) - { - if ($this->displayErrorDetails) { - return; - } - - $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); - } - - /** - * Render error as Text. - * - * @param \Exception|\Throwable $throwable - * - * @return string - */ - protected function renderThrowableAsText($throwable) - { - $text = sprintf('Type: %s' . PHP_EOL, get_class($throwable)); - - if ($code = $throwable->getCode()) { - $text .= sprintf('Code: %s' . PHP_EOL, $code); - } - - if ($message = $throwable->getMessage()) { - $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); - } - - if ($file = $throwable->getFile()) { - $text .= sprintf('File: %s' . PHP_EOL, $file); - } - - if ($line = $throwable->getLine()) { - $text .= sprintf('Line: %s' . PHP_EOL, $line); - } - - if ($trace = $throwable->getTraceAsString()) { - $text .= sprintf('Trace: %s', $trace); - } - - return $text; - } - - /** - * Wraps the error_log function so that this can be easily tested - * - * @param $message - */ - protected function logError($message) - { - error_log($message); - } -} diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php new file mode 100644 index 000000000..73fa9764c --- /dev/null +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -0,0 +1,249 @@ +displayErrorDetails = $displayErrorDetails; + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->method = $request->getMethod(); + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request); + $this->renderer = $this->determineRenderer(); + + if (!$this->displayErrorDetails) { + $this->writeToErrorLog(); + } + + return $this->formatResponse(); + } + + /** + * 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) + { + $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('Slim\Interfaces\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 = JsonErrorRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XmlErrorRenderer::class; + break; + + case 'text/plain': + $renderer = PlainTextErrorRenderer::class; + break; + + default: + case 'text/html': + $renderer = HtmlErrorRenderer::class; + break; + } + } + + return new $renderer($this->exception, $this->displayErrorDetails); + } + + /** + * @return int + */ + protected function determineStatusCode() + { + if ($this->method === 'OPTIONS') { + return 200; + } elseif ($this->exception instanceof HttpException) { + return $this->exception->getCode(); + } + return 500; + } + + /** + * @return ResponseInterface + */ + protected function formatResponse() + { + $e = $this->exception; + $response = $this->response; + $body = $this->renderer->renderWithBody(); + + if ($e instanceof HttpNotAllowedException) { + $response = $response->withHeader('Allow', $e->getAllowedMethods()); + } + + return $response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } + + /** + * Write to the error log if displayErrorDetails is false + * + * @return void + */ + protected function writeToErrorLog() + { + $renderer = new PlainTextErrorRenderer($this->exception, true); + $error = $renderer->render(); + $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($error); + } + + /** + * Wraps the error_log function so that this can be easily tested + * + * @param string $error + */ + protected function logError($error) + { + error_log($error); + } +} diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php new file mode 100644 index 000000000..bcdf48bb2 --- /dev/null +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -0,0 +1,51 @@ +exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + /** + * @return Body + */ + public function renderWithBody() + { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; + } +} diff --git a/Slim/Handlers/AbstractHandler.php b/Slim/Handlers/AbstractHandler.php deleted file mode 100644 index b166a1564..000000000 --- a/Slim/Handlers/AbstractHandler.php +++ /dev/null @@ -1,59 +0,0 @@ -getHeaderLine('Accept'); - $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); - - if (count($selectedContentTypes)) { - return current($selectedContentTypes); - } - - // handle +json and +xml specially - if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { - $mediaType = 'application/' . $matches[1]; - if (in_array($mediaType, $this->knownContentTypes)) { - return $mediaType; - } - } - - return 'text/html'; - } -} diff --git a/Slim/Handlers/Error.php b/Slim/Handlers/Error.php deleted file mode 100644 index dd0bc8d4e..000000000 --- a/Slim/Handlers/Error.php +++ /dev/null @@ -1,224 +0,0 @@ -determineContentType($request); - 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->renderHtmlErrorMessage($exception); - break; - - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $this->writeToErrorLog($exception); - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - return $response - ->withStatus(500) - ->withHeader('Content-type', $contentType) - ->withBody($body); - } - - /** - * Render HTML error page - * - * @param \Exception $exception - * - * @return string - */ - protected function renderHtmlErrorMessage(\Exception $exception) - { - $title = 'Slim Application Error'; - - if ($this->displayErrorDetails) { - $html = '
The application could not run because of the following error:
'; - $html .= 'A website error has occurred. Sorry for the temporary inconvenience.
'; - } - - $output = sprintf( - "" . - "%s', htmlentities($trace)); - } - - return $html; - } - - /** - * Render JSON error - * - * @param \Exception $exception - * - * @return string - */ - protected function renderJsonErrorMessage(\Exception $exception) - { - $error = [ - 'message' => 'Slim Application Error', - ]; - - if ($this->displayErrorDetails) { - $error['exception'] = []; - - do { - $error['exception'][] = [ - 'type' => get_class($exception), - 'code' => $exception->getCode(), - 'message' => $exception->getMessage(), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => explode("\n", $exception->getTraceAsString()), - ]; - } while ($exception = $exception->getPrevious()); - } - - return json_encode($error, JSON_PRETTY_PRINT); - } - - /** - * Render XML error - * - * @param \Exception $exception - * - * @return string - */ - protected function renderXmlErrorMessage(\Exception $exception) - { - $xml = "
" . $exception->getCode() . "
\n";
- $xml .= " The application could not run because of the following error:
'; + $html .= 'A website error has occurred. Sorry for the temporary inconvenience.
'; + } + + return $this->renderHtmlBody($title, $html); + } + + /** + * @param string $title + * @param string $html + * @return string + */ + public function renderHtmlBody($title = '', $html = '') + { + return sprintf( + "" . + " " . + " " . + "%s', htmlentities($trace)); + } + + return $html; + } +} diff --git a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php new file mode 100644 index 000000000..6e3f715e4 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -0,0 +1,50 @@ +exception; + $error = ['message' => $e->getMessage()]; + + 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 + */ + private function formatExceptionFragment($e) + { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + } +} diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php new file mode 100644 index 000000000..fa876d75a --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -0,0 +1,62 @@ +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 + */ + private 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/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php new file mode 100644 index 000000000..ed83742e2 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php @@ -0,0 +1,51 @@ +exception; + $xml = "
" . $e->getCode() . "
\n";
+ $xml .= " Method not allowed. Must be one of: $allow
- -