From 44b1f59a6a8eb08bb13beba44b31e803924fb451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Tue, 9 May 2017 23:39:53 -0600 Subject: [PATCH 01/36] Refactor error handling and rendering --- Slim/App.php | 211 ++++++++---------- Slim/Container.php | 1 + Slim/Exception/HttpBadRequestException.php | 8 + Slim/Exception/HttpException.php | 103 +++++++++ .../HttpInternalServerErrorException.php | 8 + Slim/Exception/HttpNotAllowedException.php | 40 ++++ Slim/Exception/HttpNotFoundException.php | 8 + .../Exception/HttpNotImplementedException.php | 8 + Slim/Exception/InvalidMethodException.php | 21 -- Slim/Exception/MethodNotAllowedException.php | 81 ------- Slim/Exception/NotFoundException.php | 62 ----- Slim/Exception/PhpException.php | 26 +++ Slim/Handlers/AbstractErrorRenderer.php | 54 +++++ Slim/Handlers/AbstractHandler.php | 122 +++++++++- Slim/Handlers/ErrorHandler.php | 42 ++++ .../ErrorRenderers/HTMLErrorRenderer.php | 91 ++++++++ .../ErrorRenderers/JSONErrorRenderer.php | 45 ++++ .../ErrorRenderers/PlainTextErrorRender.php | 25 +++ .../ErrorRenderers/XMLErrorRenderer.php | 58 +++++ Slim/Http/Request.php | 22 +- Slim/Interfaces/ErrorHandlerInterface.php | 25 +++ Slim/Interfaces/ErrorRendererInterface.php | 21 ++ 22 files changed, 781 insertions(+), 301 deletions(-) create mode 100644 Slim/Exception/HttpBadRequestException.php create mode 100644 Slim/Exception/HttpException.php create mode 100644 Slim/Exception/HttpInternalServerErrorException.php create mode 100644 Slim/Exception/HttpNotAllowedException.php create mode 100644 Slim/Exception/HttpNotFoundException.php create mode 100644 Slim/Exception/HttpNotImplementedException.php delete mode 100644 Slim/Exception/InvalidMethodException.php delete mode 100644 Slim/Exception/MethodNotAllowedException.php delete mode 100644 Slim/Exception/NotFoundException.php create mode 100644 Slim/Exception/PhpException.php create mode 100644 Slim/Handlers/AbstractErrorRenderer.php create mode 100644 Slim/Handlers/ErrorHandler.php create mode 100644 Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php create mode 100644 Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php create mode 100644 Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php create mode 100644 Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php create mode 100644 Slim/Interfaces/ErrorHandlerInterface.php create mode 100644 Slim/Interfaces/ErrorRendererInterface.php diff --git a/Slim/App.php b/Slim/App.php index ad2eaafa6..d8749a36d 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -8,22 +8,22 @@ */ namespace Slim; +use BadMethodCallException; use Exception; use FastRoute\Dispatcher; use Interop\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\Exception\PhpException; +use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteGroupInterface; use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouterInterface; +use RuntimeException; use Throwable; /** @@ -157,7 +157,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"); } /******************************************************************************** @@ -295,7 +295,7 @@ public function getRouter() */ public function setNotFoundHandler(callable $handler) { - $this->notFoundHandler = $handler; + $this->getSetting('errorHandlers')['HttpNotFoundException'] = $handler; } /** @@ -306,11 +306,7 @@ public function setNotFoundHandler(callable $handler) */ public function getNotFoundHandler() { - if (!$this->notFoundHandler) { - $this->notFoundHandler = new NotFound(); - } - - return $this->notFoundHandler; + return $this->getErrorHandler('HttpNotFoundException'); } /** @@ -331,7 +327,7 @@ public function getNotFoundHandler() */ public function setNotAllowedHandler(callable $handler) { - $this->notAllowedHandler = $handler; + $this->getSetting('errorHandlers')['HttpNotAllowedException'] = $handler; } /** @@ -342,11 +338,7 @@ public function setNotAllowedHandler(callable $handler) */ public function getNotAllowedHandler() { - if (!$this->notAllowedHandler) { - $this->notAllowedHandler = new NotAllowed(); - } - - return $this->notAllowedHandler; + return $this->getErrorHandler('HttpNotAllowedException'); } /** @@ -362,26 +354,32 @@ public function getNotAllowedHandler() * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * + * @param string $type * @param callable $handler */ - public function setErrorHandler(callable $handler) + public function setErrorHandler($type, callable $handler) { - $this->errorHandler = $handler; + $this->getSetting('errorHandlers')[$type] = $handler; } /** * Get callable to handle scenarios where an error * occurs when processing the current request. * + * @param null|string $type * @return callable|Error */ - public function getErrorHandler() + public function getErrorHandler($type = null) { - if (!$this->errorHandler) { - $this->errorHandler = new Error($this->getSetting('displayErrorDetails')); + $handlers = $this->getSetting('errorHandlers'); + $displayErrorDetails = $this->getSetting('displayErrorDetails', false); + + if (!is_null($type) && isset($handlers[$type])) { + $handler = get_class($handlers[$type]); + return new $handler($displayErrorDetails); } - return $this->errorHandler; + return new ErrorHandler($displayErrorDetails); } /** @@ -401,7 +399,7 @@ public function getErrorHandler() */ public function setPhpErrorHandler(callable $handler) { - $this->phpErrorHandler = $handler; + $this->getSetting('errorHandlers')['PhpException'] = $handler; } /** @@ -412,11 +410,7 @@ public function setPhpErrorHandler(callable $handler) */ public function getPhpErrorHandler() { - if (!$this->phpErrorHandler) { - $this->phpErrorHandler = new PhpError($this->getSetting('displayErrorDetails')); - } - - return $this->phpErrorHandler; + return $this->getErrorHandler('PhpException'); } /******************************************************************************** @@ -587,11 +581,14 @@ public function group($pattern, $callable) 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(new PhpException($e), $request, $response); } if (!$silent) { @@ -601,35 +598,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 * @@ -641,8 +609,6 @@ protected function processInvalidMethod(ServerRequestInterface $request, Respons * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function process(ServerRequestInterface $request, ResponseInterface $response) { @@ -659,8 +625,6 @@ public function process(ServerRequestInterface $request, ResponseInterface $resp $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->finalize($response); @@ -756,16 +720,24 @@ 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(['method' => $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); } /** @@ -797,6 +769,45 @@ protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request return $request->withAttribute('routeInfo', $routeInfo); } + /** + * Resolve custom error handler from container or use default ErrorHandler + * @param Exception $exception + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return mixed + */ + public function handleException(Exception $exception, ServerRequestInterface $request, ResponseInterface $response) + { + $exceptionType = get_class($exception); + $handler = $this->getErrorHandler($exceptionType); + + /** + * 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(); + } + + return $recoverable + ? call_user_func_array($handler, [$request, $response, $exception]) + : $response; + } + /** * Finalize response * @@ -817,7 +828,7 @@ protected function finalize(ResponseInterface $response) // Add Content-Length header if `addContentLengthHeader` setting is set if ($this->getSetting('addContentLengthHeader') == true) { if (ob_get_length() > 0) { - throw new \RuntimeException("Unexpected data in output buffer. " . + throw new RuntimeException("Unexpected data in output buffer. " . "Maybe you have characters before an opening getBody()->getSize(); @@ -846,50 +857,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/Container.php b/Slim/Container.php index ac0d9179c..7a599b1a4 100644 --- a/Slim/Container.php +++ b/Slim/Container.php @@ -57,6 +57,7 @@ class Container extends PimpleContainer implements ContainerInterface 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, + 'errorHandlers' => [], ]; /** diff --git a/Slim/Exception/HttpBadRequestException.php b/Slim/Exception/HttpBadRequestException.php new file mode 100644 index 000000000..db4e998bd --- /dev/null +++ b/Slim/Exception/HttpBadRequestException.php @@ -0,0 +1,8 @@ +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 bool $recoverable + */ + public function setRecoverable($recoverable) + { + $this->recoverable = $recoverable; + } + + /** + * @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 bool + */ + public function isRecoverable() + { + return $this->recoverable; + } +} \ No newline at end of file diff --git a/Slim/Exception/HttpInternalServerErrorException.php b/Slim/Exception/HttpInternalServerErrorException.php new file mode 100644 index 000000000..07356c03a --- /dev/null +++ b/Slim/Exception/HttpInternalServerErrorException.php @@ -0,0 +1,8 @@ +getDetails(); + + if (isset($details['allowedMethods'])) { + $allowedMethods = $details['allowedMethods']; + + if (is_array($allowedMethods)) { + return implode(',', $allowedMethods); + } else if (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; + } +} \ No newline at end of file diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php new file mode 100644 index 000000000..f3002b727 --- /dev/null +++ b/Slim/Exception/HttpNotFoundException.php @@ -0,0 +1,8 @@ +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/Exception/PhpException.php b/Slim/Exception/PhpException.php new file mode 100644 index 000000000..ef569920d --- /dev/null +++ b/Slim/Exception/PhpException.php @@ -0,0 +1,26 @@ +exception = $exception; + } + + /** + * @return Exception + */ + public function getException() + { + return $this->exception; + } +} \ No newline at end of file diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php new file mode 100644 index 000000000..d44611a97 --- /dev/null +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -0,0 +1,54 @@ +exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + /** + * @return string + */ + public function render() + { + if ($this->exception instanceof Throwable) { + return $this->renderThrowableOutput(); + } + + return $this->renderGenericOutput(); + } +} diff --git a/Slim/Handlers/AbstractHandler.php b/Slim/Handlers/AbstractHandler.php index b166a1564..8e0c4539e 100644 --- a/Slim/Handlers/AbstractHandler.php +++ b/Slim/Handlers/AbstractHandler.php @@ -8,12 +8,21 @@ */ namespace Slim\Handlers; +use Exception; +use Slim\Interfaces\ErrorHandlerInterface; +use UnexpectedValueException; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpException; /** - * Abstract Slim application handler + * Default Slim application error handler + * + * It outputs the error message and diagnostic information in either JSON, XML, + * or HTML based on the Accept header. */ -abstract class AbstractHandler +abstract class AbstractHandler implements ErrorHandlerInterface { /** * Known handled content types @@ -26,6 +35,69 @@ abstract class AbstractHandler 'text/xml', 'text/html', ]; + /** + * @var bool + */ + protected $displayErrorDetails = false; + /** + * @var string + */ + protected $contentType = 'text/plain'; + /** + * @var string + */ + protected $method; + /** + * @var ServerRequestInterface + */ + protected $request; + /** + * @var ResponseInterface + */ + protected $response; + /** + * @var Exception + */ + protected $exception; + /** + * @var HTMLErrorRenderer|JSONErrorRenderer|XMLErrorRenderer|PlainTextErrorRenderer + */ + protected $renderer; + /** + * @var int + */ + protected $statusCode; + + /** + * AbstractHandler constructor. + * @param bool $displayErrorDetails + */ + public function __construct($displayErrorDetails = true) + { + $this->displayErrorDetails = $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) + { + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->method = $request->getMethod(); + $this->contentType = $this->resolveContentType(); + $this->renderer = $this->resolveRenderer(); + $this->statusCode = $this->resolveStatusCode(); + + return $this->respond(); + } /** * Determine which content type we know about is wanted using Accept header @@ -34,12 +106,11 @@ abstract class AbstractHandler * 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) + protected function resolveContentType() { - $acceptHeader = $request->getHeaderLine('Accept'); + $acceptHeader = $this->request->getHeaderLine('Accept'); $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); if (count($selectedContentTypes)) { @@ -56,4 +127,45 @@ protected function determineContentType(ServerRequestInterface $request) return 'text/html'; } + + /** + * @return HTMLErrorRenderer|JSONErrorRenderer|XMLErrorRenderer|PlainTextErrorRenderer + * @throws HttpBadRequestException + */ + protected function resolveRenderer() + { + if ($this->method === 'OPTIONS') { + return PlainTextErrorRenderer::class; + } + + switch ($this->contentType) { + case 'application/json': + return JSONErrorRenderer::class; + + case 'text/xml': + case 'application/xml': + return XMLErrorRenderer::class; + + case 'text/html': + return HTMLErrorRenderer::class; + + default: + throw new UnexpectedValueException(sprintf('Cannot render unknown content type: %s', $this->contentType)); + } + } + + /** + * @return int + */ + protected function resolveStatusCode() + { + $statusCode = 500; + + if ($this->exception instanceof HttpException) + { + $statusCode = $this->exception->getCode(); + } + + return $statusCode; + } } diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php new file mode 100644 index 000000000..dde2434ac --- /dev/null +++ b/Slim/Handlers/ErrorHandler.php @@ -0,0 +1,42 @@ +renderer($this->exception, $this->displayErrorDetails); + $output = $renderer->render(); + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + if ($this->exception instanceof HttpNotAllowedException) { + $this->response->withHeader('Allow', $this->exception->getAllowedMethods()); + } + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } +} diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php new file mode 100644 index 000000000..1d6f581c3 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -0,0 +1,91 @@ +exception; + + if ($this->displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderExceptionFragment($e); + + while ($e = $e->getPrevious()) { + $html .= '

Previous exception

'; + $html .= $this->renderExceptionFragment($e); + } + } else { + $html = '

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

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

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + public function renderGenericOutput() + { + + } + + /** + * @param Exception $exception + * @return string + */ + public function renderExceptionFragment($exception) + { + if (!$exception instanceof Exception && !$exception instanceof Error) { + throw new RuntimeException('Unexpected type. Expected Exception or Error.'); + } + + $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; + } +} \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php new file mode 100644 index 000000000..9f22d3a88 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -0,0 +1,45 @@ +exception; + $error = ['message' => 'Slim Application Error']; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + + do { + $error['exception'][] = [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => explode("\n", $e->getTraceAsString()), + ]; + } while ($e = $e->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + public function renderGenericOutput() + { + $error = ['message' => $this->exception->getMessage()]; + + return json_encode($error, JSON_PRETTY_PRINT); + } +} \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php new file mode 100644 index 000000000..4aeacff5a --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php @@ -0,0 +1,25 @@ +exception->getMessage(); + } + + public function renderGenericOutput() + { + return $this->exception->getMessage(); + } +} \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php new file mode 100644 index 000000000..5710a7325 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php @@ -0,0 +1,58 @@ +exception; + $xml = "\n Slim 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 .= " " . $this->createCdataSection($e->getTraceAsString()) . "\n"; + $xml .= " \n"; + } while ($e = $e->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * @return string + */ + public function renderGenericOutput() + { + return "{$this->exception->getMessage()}"; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} \ No newline at end of file diff --git a/Slim/Http/Request.php b/Slim/Http/Request.php index 39a14ce49..31e0ac9bd 100644 --- a/Slim/Http/Request.php +++ b/Slim/Http/Request.php @@ -16,7 +16,7 @@ use Psr\Http\Message\UriInterface; use Psr\Http\Message\StreamInterface; use Slim\Collection; -use Slim\Exception\InvalidMethodException; +use Slim\Exception\HttpNotImplementedException; use Slim\Interfaces\Http\HeadersInterface; /** @@ -150,7 +150,7 @@ public static function createFromEnvironment(Environment $environment) * @param array $serverParams The server environment variables * @param StreamInterface $body The request body object * @param array $uploadedFiles The request uploadedFiles collection - * @throws InvalidMethodException on invalid HTTP method + * @throws HttpNotImplementedException on invalid HTTP method */ public function __construct( $method, @@ -163,7 +163,7 @@ public function __construct( ) { try { $this->originalMethod = $this->filterMethod($method); - } catch (InvalidMethodException $e) { + } catch (HttpNotImplementedException $e) { $this->originalMethod = $method; } @@ -223,7 +223,7 @@ public function __construct( }); // if the request had an invalid method, we can throw it now - if (isset($e) && $e instanceof InvalidMethodException) { + if (isset($e) && $e instanceof HttpNotImplementedException) { throw $e; } } @@ -315,26 +315,30 @@ public function withMethod($method) * * @param null|string $method * @return null|string - * @throws \InvalidArgumentException on invalid HTTP method. + * @throws HttpNotImplementedException on invalid HTTP method. */ protected function filterMethod($method) { if ($method === null) { return $method; } - if (!is_string($method)) { throw new InvalidArgumentException(sprintf( 'Unsupported HTTP method; must be a string, received %s', (is_object($method) ? get_class($method) : gettype($method)) )); } - $method = strtoupper($method); if (preg_match("/^[!#$%&'*+.^_`|~0-9a-z-]+$/i", $method) !== 1) { - throw new InvalidMethodException($this, $method); - } + $e = new HttpNotImplementedException(sprintf( + 'Unsupported HTTP method "%s" provided', + $method + )); + $e->setRequest($this); + $e->setDetails(['method' => $method]); + throw $e; + } return $method; } diff --git a/Slim/Interfaces/ErrorHandlerInterface.php b/Slim/Interfaces/ErrorHandlerInterface.php new file mode 100644 index 000000000..6ab296ae0 --- /dev/null +++ b/Slim/Interfaces/ErrorHandlerInterface.php @@ -0,0 +1,25 @@ + Date: Wed, 10 May 2017 02:37:01 -0600 Subject: [PATCH 02/36] Fix bugs from initial commit --- Slim/App.php | 42 +++++++++--------- Slim/Container.php | 1 - Slim/Exception/HttpBadRequestException.php | 2 + Slim/Exception/HttpException.php | 43 +++++++++++++++++-- .../HttpInternalServerErrorException.php | 2 + Slim/Exception/HttpNotAllowedException.php | 2 + Slim/Exception/HttpNotFoundException.php | 4 +- .../Exception/HttpNotImplementedException.php | 2 + Slim/Exception/PhpException.php | 6 +-- Slim/Handlers/AbstractErrorRenderer.php | 8 ++-- Slim/Handlers/AbstractHandler.php | 12 ++++-- Slim/Handlers/ErrorHandler.php | 6 ++- .../ErrorRenderers/HTMLErrorRenderer.php | 34 +++++++++++++-- .../ErrorRenderers/JSONErrorRenderer.php | 9 ++-- .../ErrorRenderers/PlainTextErrorRenderer.php | 27 ++++++++++++ .../ErrorRenderers/XMLErrorRenderer.php | 8 ++-- Slim/Interfaces/ErrorRendererInterface.php | 4 +- 17 files changed, 162 insertions(+), 50 deletions(-) create mode 100644 Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php diff --git a/Slim/App.php b/Slim/App.php index d8749a36d..ae9680c9a 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -18,6 +18,7 @@ use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\PhpException; +use Slim\Handlers\AbstractHandler; use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteGroupInterface; @@ -98,6 +99,7 @@ class App */ public function __construct(array $settings = []) { + $this->addSettings($settings); $this->container = new Container(); } @@ -295,7 +297,7 @@ public function getRouter() */ public function setNotFoundHandler(callable $handler) { - $this->getSetting('errorHandlers')['HttpNotFoundException'] = $handler; + $this->addErrorHandler(HttpNotFoundException::class, $handler); } /** @@ -306,7 +308,7 @@ public function setNotFoundHandler(callable $handler) */ public function getNotFoundHandler() { - return $this->getErrorHandler('HttpNotFoundException'); + return $this->getErrorHandler(HttpNotFoundException::class); } /** @@ -327,7 +329,7 @@ public function getNotFoundHandler() */ public function setNotAllowedHandler(callable $handler) { - $this->getSetting('errorHandlers')['HttpNotAllowedException'] = $handler; + $this->addErrorHandler(HttpNotAllowedException::class, $handler); } /** @@ -338,7 +340,7 @@ public function setNotAllowedHandler(callable $handler) */ public function getNotAllowedHandler() { - return $this->getErrorHandler('HttpNotAllowedException'); + return $this->getErrorHandler(HttpNotAllowedException::class); } /** @@ -359,7 +361,9 @@ public function getNotAllowedHandler() */ public function setErrorHandler($type, callable $handler) { - $this->getSetting('errorHandlers')[$type] = $handler; + $handlers = $this->getSetting('errorHandlers', []); + $handlers[$type] = $handler; + $this->addSetting('errorHandlers', $handlers); } /** @@ -367,16 +371,22 @@ public function setErrorHandler($type, callable $handler) * occurs when processing the current request. * * @param null|string $type - * @return callable|Error + * @return callable|ErrorHandler */ public function getErrorHandler($type = null) { $handlers = $this->getSetting('errorHandlers'); - $displayErrorDetails = $this->getSetting('displayErrorDetails', false); + $displayErrorDetails = $this->getSetting('displayErrorDetails'); if (!is_null($type) && isset($handlers[$type])) { - $handler = get_class($handlers[$type]); - return new $handler($displayErrorDetails); + $handler = $handlers[$type]; + + if (is_callable($handler)) { + return $handler; + } else if ($handler instanceof AbstractHandler) { + $handler = get_class($handler); + return new $handler($displayErrorDetails); + } } return new ErrorHandler($displayErrorDetails); @@ -399,7 +409,7 @@ public function getErrorHandler($type = null) */ public function setPhpErrorHandler(callable $handler) { - $this->getSetting('errorHandlers')['PhpException'] = $handler; + $this->setErrorHandler(PhpException::class, $handler); } /** @@ -410,7 +420,7 @@ public function setPhpErrorHandler(callable $handler) */ public function getPhpErrorHandler() { - return $this->getErrorHandler('PhpException'); + return $this->getErrorHandler(PhpException::class); } /******************************************************************************** @@ -575,8 +585,6 @@ public function group($pattern, $callable) * @return ResponseInterface * * @throws Exception - * @throws MethodNotAllowedException - * @throws NotFoundException */ public function run($silent = false) { @@ -621,12 +629,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); - } - + $response = $this->callMiddlewareStack($request, $response); $response = $this->finalize($response); return $response; @@ -787,7 +790,6 @@ public function handleException(Exception $exception, ServerRequestInterface $re */ if (method_exists($exception, 'getRequest')) { $r = $exception->getRequest(); - if (!is_null($r)) { $request = $r; } diff --git a/Slim/Container.php b/Slim/Container.php index 7a599b1a4..ac0d9179c 100644 --- a/Slim/Container.php +++ b/Slim/Container.php @@ -57,7 +57,6 @@ class Container extends PimpleContainer implements ContainerInterface 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, - 'errorHandlers' => [], ]; /** diff --git a/Slim/Exception/HttpBadRequestException.php b/Slim/Exception/HttpBadRequestException.php index db4e998bd..08e18b77b 100644 --- a/Slim/Exception/HttpBadRequestException.php +++ b/Slim/Exception/HttpBadRequestException.php @@ -5,4 +5,6 @@ class HttpBadRequestException extends HttpException { protected $code = 400; protected $message = 'Bad request.'; + protected $title = '400 Bad Request'; + protected $description = 'The server cannot or will not process the request due to an apparent client error.'; } \ No newline at end of file diff --git a/Slim/Exception/HttpException.php b/Slim/Exception/HttpException.php index 82f82d691..e35b89f7b 100644 --- a/Slim/Exception/HttpException.php +++ b/Slim/Exception/HttpException.php @@ -19,6 +19,14 @@ abstract class HttpException extends Exception * @var array|null */ protected $details = null; + /** + * @var string|null + */ + protected $title = ''; + /** + * @var string + */ + protected $description = ''; /** * @var bool */ @@ -62,11 +70,24 @@ public function setDetails(array $details) } /** - * @param bool $recoverable + * @param string $title */ - public function setRecoverable($recoverable) + public function setTitle($title) + { + $this->title = $title; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + public function notRecoverable() { - $this->recoverable = $recoverable; + $this->recoverable = false; } /** @@ -93,6 +114,22 @@ public function getDetails() return $this->details; } + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + /** * @return bool */ diff --git a/Slim/Exception/HttpInternalServerErrorException.php b/Slim/Exception/HttpInternalServerErrorException.php index 07356c03a..528f3c492 100644 --- a/Slim/Exception/HttpInternalServerErrorException.php +++ b/Slim/Exception/HttpInternalServerErrorException.php @@ -5,4 +5,6 @@ class HttpInternalServerErrorException extends HttpException { protected $code = 500; protected $message = 'Internal server error.'; + protected $title = '500 Internal Server Error'; + protected $description = 'A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.'; } \ No newline at end of file diff --git a/Slim/Exception/HttpNotAllowedException.php b/Slim/Exception/HttpNotAllowedException.php index 1b860bb40..2ea066ddf 100644 --- a/Slim/Exception/HttpNotAllowedException.php +++ b/Slim/Exception/HttpNotAllowedException.php @@ -5,6 +5,8 @@ class HttpNotAllowedException extends HttpException { protected $code = 405; protected $message = 'Method not allowed.'; + protected $title = '405 Method Not Allowed'; + protected $description = 'The request method is not supported for the requested resource.'; /** * @return string diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php index f3002b727..912e27699 100644 --- a/Slim/Exception/HttpNotFoundException.php +++ b/Slim/Exception/HttpNotFoundException.php @@ -4,5 +4,7 @@ class HttpNotFoundException extends HttpException { protected $code = 404; - protected $message = 'The page you requested cannot be found.'; + protected $message = 'Not found.'; + protected $title = '404 Not Found'; + protected $description = 'The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.'; } \ No newline at end of file diff --git a/Slim/Exception/HttpNotImplementedException.php b/Slim/Exception/HttpNotImplementedException.php index 2383ac646..3144957da 100644 --- a/Slim/Exception/HttpNotImplementedException.php +++ b/Slim/Exception/HttpNotImplementedException.php @@ -5,4 +5,6 @@ class HttpNotImplementedException extends HttpException { protected $code = 501; protected $message = 'Not implemented.'; + protected $title = '501 Not Implemented'; + protected $description = 'The server either does not recognize the request method, or it lacks the ability to fulfil the request.'; } \ No newline at end of file diff --git a/Slim/Exception/PhpException.php b/Slim/Exception/PhpException.php index ef569920d..ef93bbf49 100644 --- a/Slim/Exception/PhpException.php +++ b/Slim/Exception/PhpException.php @@ -9,15 +9,15 @@ class PhpException extends Exception /** * PhpException constructor. - * @param Exception $exception + * @param $exception */ - public function __construct(Exception $exception) + public function __construct($exception) { $this->exception = $exception; } /** - * @return Exception + * @return mixed */ public function getException() { diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index d44611a97..36908da97 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -9,7 +9,7 @@ namespace Slim\Handlers; use Exception; -use Throwable; +use Slim\Exception\PhpException; use Slim\Interfaces\ErrorRendererInterface; /** @@ -45,10 +45,10 @@ public function __construct(Exception $exception, $displayErrorDetails = false) */ public function render() { - if ($this->exception instanceof Throwable) { - return $this->renderThrowableOutput(); + if ($this->exception instanceof PhpException) { + return $this->renderLanguageExceptionOutput(); } - return $this->renderGenericOutput(); + return $this->renderGenericExceptionOutput(); } } diff --git a/Slim/Handlers/AbstractHandler.php b/Slim/Handlers/AbstractHandler.php index 8e0c4539e..a61629d3a 100644 --- a/Slim/Handlers/AbstractHandler.php +++ b/Slim/Handlers/AbstractHandler.php @@ -9,12 +9,16 @@ namespace Slim\Handlers; use Exception; -use Slim\Interfaces\ErrorHandlerInterface; use UnexpectedValueException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpException; +use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; +use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; +use Slim\Handlers\ErrorRenderers\XMLErrorRenderer; +use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; +use Slim\Interfaces\ErrorHandlerInterface; /** * Default Slim application error handler @@ -117,7 +121,6 @@ protected function resolveContentType() 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)) { @@ -135,6 +138,8 @@ protected function resolveContentType() protected function resolveRenderer() { if ($this->method === 'OPTIONS') { + $this->statusCode = 200; + $this->contentType = 'text/plain'; return PlainTextErrorRenderer::class; } @@ -161,8 +166,7 @@ protected function resolveStatusCode() { $statusCode = 500; - if ($this->exception instanceof HttpException) - { + if ($this->exception instanceof HttpException) { $statusCode = $this->exception->getCode(); } diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php index dde2434ac..7064f605b 100644 --- a/Slim/Handlers/ErrorHandler.php +++ b/Slim/Handlers/ErrorHandler.php @@ -10,6 +10,7 @@ use Psr\Http\Message\ResponseInterface; use Slim\Exception\HttpNotAllowedException; +use Slim\Exception\PhpException; use Slim\Http\Body; /** @@ -25,13 +26,14 @@ class ErrorHandler extends AbstractHandler */ public function respond() { - $renderer = new $this->renderer($this->exception, $this->displayErrorDetails); + $e = $this->exception; + $renderer = new $this->renderer($e, $this->displayErrorDetails); $output = $renderer->render(); $body = new Body(fopen('php://temp', 'r+')); $body->write($output); if ($this->exception instanceof HttpNotAllowedException) { - $this->response->withHeader('Allow', $this->exception->getAllowedMethods()); + $this->response->withHeader('Allow', $e->getAllowedMethods()); } return $this->response diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php index 1d6f581c3..c82b2b803 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -6,26 +6,30 @@ * @copyright Copyright (c) 2011-2017 Josh Lockhart * @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License) */ -namespace Slim\Handlers; +namespace Slim\Handlers\ErrorRenderers; use Error; use Exception; use RuntimeException; +use Slim\Handlers\AbstractErrorRenderer; /** * Default Slim application HTML Error Renderer */ class HTMLErrorRenderer extends AbstractErrorRenderer { - public function renderThrowableOutput() + public function renderLanguageExceptionOutput() { $title = 'Slim Application Error'; $e = $this->exception; + $re = $e->getException(); if ($this->displayErrorDetails) { $html = '

The application could not run because of the following error:

'; $html .= '

Details

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

Previous exception

'; + $html .= $this->renderExceptionFragment($re); while ($e = $e->getPrevious()) { $html .= '

Previous exception

'; @@ -48,9 +52,33 @@ public function renderThrowableOutput() return $output; } - public function renderGenericOutput() + public function renderGenericExceptionOutput() { + $e = $this->exception; + + $title = $e->getMessage(); + if (method_exists($e, 'getTitle')) { + $title = $e->getTitle(); + } + $description = ''; + if (method_exists($e, 'getDescription')) { + $description = $e->getDescription(); + } + + $output = sprintf( + "%s

%s

%s

" . + "Go Back", + $title, + $title, + $description + ); + + return $output; } /** diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php index 9f22d3a88..a2858baa5 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -6,14 +6,16 @@ * @copyright Copyright (c) 2011-2017 Josh Lockhart * @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License) */ -namespace Slim\Handlers; +namespace Slim\Handlers\ErrorRenderers; + +use Slim\Handlers\AbstractErrorRenderer; /** * Default Slim application JSON Error Renderer */ class JSONErrorRenderer extends AbstractErrorRenderer { - public function renderThrowableOutput() + public function renderLanguageExceptionOutput() { $e = $this->exception; $error = ['message' => 'Slim Application Error']; @@ -36,10 +38,9 @@ public function renderThrowableOutput() return json_encode($error, JSON_PRETTY_PRINT); } - public function renderGenericOutput() + public function renderGenericExceptionOutput() { $error = ['message' => $this->exception->getMessage()]; - return json_encode($error, JSON_PRETTY_PRINT); } } \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php new file mode 100644 index 000000000..cb2716aff --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -0,0 +1,27 @@ +exception->getMessage(); + } + + public function renderGenericExceptionOutput() + { + return $this->exception->getMessage(); + } +} \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php index 5710a7325..b984f3550 100644 --- a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php @@ -6,7 +6,9 @@ * @copyright Copyright (c) 2011-2017 Josh Lockhart * @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License) */ -namespace Slim\Handlers; +namespace Slim\Handlers\ErrorRenderers; + +use Slim\Handlers\AbstractErrorRenderer; /** * Default Slim application XML Error Renderer @@ -16,7 +18,7 @@ class XMLErrorRenderer extends AbstractErrorRenderer /** * @return string */ - public function renderThrowableOutput() + public function renderLanguageExceptionOutput() { $e = $this->exception; $xml = "\n Slim Application Error\n"; @@ -40,7 +42,7 @@ public function renderThrowableOutput() /** * @return string */ - public function renderGenericOutput() + public function renderGenericExceptionOutput() { return "{$this->exception->getMessage()}"; } diff --git a/Slim/Interfaces/ErrorRendererInterface.php b/Slim/Interfaces/ErrorRendererInterface.php index 97b9dff34..2da2b17c1 100644 --- a/Slim/Interfaces/ErrorRendererInterface.php +++ b/Slim/Interfaces/ErrorRendererInterface.php @@ -16,6 +16,6 @@ */ interface ErrorRendererInterface { - public function renderThrowableOutput(); - public function renderGenericOutput(); + public function renderLanguageExceptionOutput(); + public function renderGenericExceptionOutput(); } From d1e5146228dbb43bdd46be1e858c6f8a52b3f8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Wed, 10 May 2017 03:09:37 -0600 Subject: [PATCH 03/36] Removed unused files and variable references from App class --- Slim/App.php | 21 +- Slim/Handlers/AbstractError.php | 99 -------- Slim/Handlers/Error.php | 224 ------------------ .../ErrorRenderers/PlainTextErrorRender.php | 25 -- Slim/Handlers/NotAllowed.php | 147 ------------ Slim/Handlers/NotFound.php | 126 ---------- Slim/Handlers/PhpError.php | 205 ---------------- 7 files changed, 1 insertion(+), 846 deletions(-) delete mode 100644 Slim/Handlers/AbstractError.php delete mode 100644 Slim/Handlers/Error.php delete mode 100644 Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php delete mode 100644 Slim/Handlers/NotAllowed.php delete mode 100644 Slim/Handlers/NotFound.php delete mode 100644 Slim/Handlers/PhpError.php diff --git a/Slim/App.php b/Slim/App.php index ae9680c9a..896546b3b 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -55,26 +55,6 @@ class App */ protected $router; - /** - * @var callable - */ - protected $notFoundHandler; - - /** - * @var callable - */ - protected $notAllowedHandler; - - /** - * @var callable - */ - protected $errorHandler; - - /** - * @var callable - */ - protected $phpErrorHandler; - /** * @var array */ @@ -86,6 +66,7 @@ class App 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, + 'errorHandlers' => [] ]; /******************************************************************************** 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/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 .= '

Details

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

Previous exception

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

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

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

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render exception as HTML. - * - * Provided for backwards compatibility; use renderHtmlExceptionOrError(). - * - * @param \Exception $exception - * - * @return string - */ - protected function renderHtmlException(\Exception $exception) - { - return $this->renderHtmlExceptionOrError($exception); - } - - /** - * Render exception or error as HTML. - * - * @param \Exception|\Error $exception - * - * @return string - */ - protected function renderHtmlExceptionOrError($exception) - { - if (!$exception instanceof \Exception && !$exception instanceof \Error) { - throw new \RuntimeException("Unexpected type. Expected Exception or Error."); - } - - $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 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 = "\n Slim Application Error\n"; - if ($this->displayErrorDetails) { - do { - $xml .= " \n"; - $xml .= " " . get_class($exception) . "\n"; - $xml .= " " . $exception->getCode() . "\n"; - $xml .= " " . $this->createCdataSection($exception->getMessage()) . "\n"; - $xml .= " " . $exception->getFile() . "\n"; - $xml .= " " . $exception->getLine() . "\n"; - $xml .= " " . $this->createCdataSection($exception->getTraceAsString()) . "\n"; - $xml .= " \n"; - } while ($exception = $exception->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/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php deleted file mode 100644 index 4aeacff5a..000000000 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php +++ /dev/null @@ -1,25 +0,0 @@ -exception->getMessage(); - } - - public function renderGenericOutput() - { - return $this->exception->getMessage(); - } -} \ No newline at end of file diff --git a/Slim/Handlers/NotAllowed.php b/Slim/Handlers/NotAllowed.php deleted file mode 100644 index 9f382c45c..000000000 --- a/Slim/Handlers/NotAllowed.php +++ /dev/null @@ -1,147 +0,0 @@ -getMethod() === 'OPTIONS') { - $status = 200; - $contentType = 'text/plain'; - $output = $this->renderPlainNotAllowedMessage($methods); - } else { - $status = 405; - $contentType = $this->determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonNotAllowedMessage($methods); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlNotAllowedMessage($methods); - break; - - case 'text/html': - $output = $this->renderHtmlNotAllowedMessage($methods); - break; - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - } - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - $allow = implode(', ', $methods); - - return $response - ->withStatus($status) - ->withHeader('Content-type', $contentType) - ->withHeader('Allow', $allow) - ->withBody($body); - } - - /** - * Render PLAIN not allowed message - * - * @param array $methods - * @return string - */ - protected function renderPlainNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return 'Allowed methods: ' . $allow; - } - - /** - * Render JSON not allowed message - * - * @param array $methods - * @return string - */ - protected function renderJsonNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return '{"message":"Method not allowed. Must be one of: ' . $allow . '"}'; - } - - /** - * Render XML not allowed message - * - * @param array $methods - * @return string - */ - protected function renderXmlNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - - return "Method not allowed. Must be one of: $allow"; - } - - /** - * Render HTML not allowed message - * - * @param array $methods - * @return string - */ - protected function renderHtmlNotAllowedMessage($methods) - { - $allow = implode(', ', $methods); - $output = << - - Method not allowed - - - -

Method not allowed

-

Method not allowed. Must be one of: $allow

- - -END; - - return $output; - } -} diff --git a/Slim/Handlers/NotFound.php b/Slim/Handlers/NotFound.php deleted file mode 100644 index d4a9dec4e..000000000 --- a/Slim/Handlers/NotFound.php +++ /dev/null @@ -1,126 +0,0 @@ -determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonNotFoundOutput(); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlNotFoundOutput(); - break; - - case 'text/html': - $output = $this->renderHtmlNotFoundOutput($request); - break; - - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - return $response->withStatus(404) - ->withHeader('Content-Type', $contentType) - ->withBody($body); - } - - /** - * Return a response for application/json content not found - * - * @return ResponseInterface - */ - protected function renderJsonNotFoundOutput() - { - return '{"message":"Not found"}'; - } - - /** - * Return a response for xml content not found - * - * @return ResponseInterface - */ - protected function renderXmlNotFoundOutput() - { - return 'Not found'; - } - - /** - * Return a response for text/html content not found - * - * @param ServerRequestInterface $request The most recent Request object - * - * @return ResponseInterface - */ - protected function renderHtmlNotFoundOutput(ServerRequestInterface $request) - { - $homeUrl = (string)($request->getUri()->withPath('')->withQuery('')->withFragment('')); - return << - - Page Not Found - - - -

Page Not Found

-

- The page you are looking for could not be found. Check the address bar - to ensure your URL is spelled correctly. If all else fails, you can - visit our home page at the link below. -

- Visit the Home Page - - -END; - } -} diff --git a/Slim/Handlers/PhpError.php b/Slim/Handlers/PhpError.php deleted file mode 100644 index 3ecce30cf..000000000 --- a/Slim/Handlers/PhpError.php +++ /dev/null @@ -1,205 +0,0 @@ -determineContentType($request); - switch ($contentType) { - case 'application/json': - $output = $this->renderJsonErrorMessage($error); - break; - - case 'text/xml': - case 'application/xml': - $output = $this->renderXmlErrorMessage($error); - break; - - case 'text/html': - $output = $this->renderHtmlErrorMessage($error); - break; - default: - throw new UnexpectedValueException('Cannot render unknown content type ' . $contentType); - } - - $this->writeToErrorLog($error); - - $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 \Throwable $error - * - * @return string - */ - protected function renderHtmlErrorMessage(\Throwable $error) - { - $title = 'Slim Application Error'; - - if ($this->displayErrorDetails) { - $html = '

The application could not run because of the following error:

'; - $html .= '

Details

'; - $html .= $this->renderHtmlError($error); - - while ($error = $error->getPrevious()) { - $html .= '

Previous error

'; - $html .= $this->renderHtmlError($error); - } - } else { - $html = '

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

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

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render error as HTML. - * - * @param \Throwable $error - * - * @return string - */ - protected function renderHtmlError(\Throwable $error) - { - $html = sprintf('
Type: %s
', get_class($error)); - - if (($code = $error->getCode())) { - $html .= sprintf('
Code: %s
', $code); - } - - if (($message = $error->getMessage())) { - $html .= sprintf('
Message: %s
', htmlentities($message)); - } - - if (($file = $error->getFile())) { - $html .= sprintf('
File: %s
', $file); - } - - if (($line = $error->getLine())) { - $html .= sprintf('
Line: %s
', $line); - } - - if (($trace = $error->getTraceAsString())) { - $html .= '

Trace

'; - $html .= sprintf('
%s
', htmlentities($trace)); - } - - return $html; - } - - /** - * Render JSON error - * - * @param \Throwable $error - * - * @return string - */ - protected function renderJsonErrorMessage(\Throwable $error) - { - $json = [ - 'message' => 'Slim Application Error', - ]; - - if ($this->displayErrorDetails) { - $json['error'] = []; - - do { - $json['error'][] = [ - 'type' => get_class($error), - 'code' => $error->getCode(), - 'message' => $error->getMessage(), - 'file' => $error->getFile(), - 'line' => $error->getLine(), - 'trace' => explode("\n", $error->getTraceAsString()), - ]; - } while ($error = $error->getPrevious()); - } - - return json_encode($json, JSON_PRETTY_PRINT); - } - - /** - * Render XML error - * - * @param \Throwable $error - * - * @return string - */ - protected function renderXmlErrorMessage(\Throwable $error) - { - $xml = "\n Slim Application Error\n"; - if ($this->displayErrorDetails) { - do { - $xml .= " \n"; - $xml .= " " . get_class($error) . "\n"; - $xml .= " " . $error->getCode() . "\n"; - $xml .= " " . $this->createCdataSection($error->getMessage()) . "\n"; - $xml .= " " . $error->getFile() . "\n"; - $xml .= " " . $error->getLine() . "\n"; - $xml .= " " . $this->createCdataSection($error->getTraceAsString()) . "\n"; - $xml .= " \n"; - } while ($error = $error->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)); - } -} From 51b20513f6f25b6fbe4d58d597e8f7e9f25fb7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Wed, 10 May 2017 03:32:36 -0600 Subject: [PATCH 04/36] Rename AbstractHandler to AbstractErrorHandler --- ...ctHandler.php => AbstractErrorHandler.php} | 31 +++++++++++++++++-- Slim/Handlers/ErrorHandler.php | 22 +------------ 2 files changed, 30 insertions(+), 23 deletions(-) rename Slim/Handlers/{AbstractHandler.php => AbstractErrorHandler.php} (84%) diff --git a/Slim/Handlers/AbstractHandler.php b/Slim/Handlers/AbstractErrorHandler.php similarity index 84% rename from Slim/Handlers/AbstractHandler.php rename to Slim/Handlers/AbstractErrorHandler.php index a61629d3a..d93918cdf 100644 --- a/Slim/Handlers/AbstractHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -14,10 +14,12 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpException; +use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; use Slim\Handlers\ErrorRenderers\XMLErrorRenderer; use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; +use Slim\Http\Body; use Slim\Interfaces\ErrorHandlerInterface; /** @@ -26,7 +28,7 @@ * It outputs the error message and diagnostic information in either JSON, XML, * or HTML based on the Accept header. */ -abstract class AbstractHandler implements ErrorHandlerInterface +abstract class AbstractErrorHandler implements ErrorHandlerInterface { /** * Known handled content types @@ -66,7 +68,7 @@ abstract class AbstractHandler implements ErrorHandlerInterface /** * @var HTMLErrorRenderer|JSONErrorRenderer|XMLErrorRenderer|PlainTextErrorRenderer */ - protected $renderer; + protected $renderer = null; /** * @var int */ @@ -143,6 +145,10 @@ protected function resolveRenderer() return PlainTextErrorRenderer::class; } + if (!is_null($this->renderer)) { + return $this->renderer; + } + switch ($this->contentType) { case 'application/json': return JSONErrorRenderer::class; @@ -172,4 +178,25 @@ protected function resolveStatusCode() return $statusCode; } + + /** + * @return ResponseInterface + */ + public function respond() + { + $e = $this->exception; + $renderer = new $this->renderer($e, $this->displayErrorDetails); + $output = $renderer->render(); + $body = new Body(fopen('php://temp', 'r+')); + $body->write($output); + + if ($this->exception instanceof HttpNotAllowedException) { + $this->response->withHeader('Allow', $e->getAllowedMethods()); + } + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } } diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php index 7064f605b..e4d96fdb3 100644 --- a/Slim/Handlers/ErrorHandler.php +++ b/Slim/Handlers/ErrorHandler.php @@ -19,26 +19,6 @@ * It outputs the error message and diagnostic information in either JSON, XML, * or HTML based on the Accept header. */ -class ErrorHandler extends AbstractHandler +class ErrorHandler extends AbstractErrorHandler { - /** - * @return ResponseInterface - */ - public function respond() - { - $e = $this->exception; - $renderer = new $this->renderer($e, $this->displayErrorDetails); - $output = $renderer->render(); - $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); - - if ($this->exception instanceof HttpNotAllowedException) { - $this->response->withHeader('Allow', $e->getAllowedMethods()); - } - - return $this->response - ->withStatus($this->statusCode) - ->withHeader('Content-type', $this->contentType) - ->withBody($body); - } } From b4194294f77a02c28cb8f1289f0671e3e141e07f Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 11:17:55 -0600 Subject: [PATCH 05/36] Fix bug in named error handler shortcuts and refactor ErrorRendererInterface methods --- Slim/App.php | 9 +++++---- Slim/Handlers/AbstractErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php | 2 +- Slim/Interfaces/ErrorRendererInterface.php | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index 896546b3b..435306fb7 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -18,6 +18,7 @@ use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\PhpException; +use Slim\Handlers\AbstractErrorHandler; use Slim\Handlers\AbstractHandler; use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; @@ -278,7 +279,7 @@ public function getRouter() */ public function setNotFoundHandler(callable $handler) { - $this->addErrorHandler(HttpNotFoundException::class, $handler); + $this->setErrorHandler(HttpNotFoundException::class, $handler); } /** @@ -310,14 +311,14 @@ public function getNotFoundHandler() */ public function setNotAllowedHandler(callable $handler) { - $this->addErrorHandler(HttpNotAllowedException::class, $handler); + $this->setErrorHandler(HttpNotAllowedException::class, $handler); } /** * Get callable to handle scenarios where a suitable * route matches the request URI but not the request method. * - * @return callable|Error + * @return callable|ErrorHandler */ public function getNotAllowedHandler() { @@ -364,7 +365,7 @@ public function getErrorHandler($type = null) if (is_callable($handler)) { return $handler; - } else if ($handler instanceof AbstractHandler) { + } else if ($handler instanceof AbstractErrorHandler) { $handler = get_class($handler); return new $handler($displayErrorDetails); } diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index 36908da97..f5d0e9291 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -46,7 +46,7 @@ public function __construct(Exception $exception, $displayErrorDetails = false) public function render() { if ($this->exception instanceof PhpException) { - return $this->renderLanguageExceptionOutput(); + return $this->renderPhpExceptionOutput(); } return $this->renderGenericExceptionOutput(); diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php index c82b2b803..86672db1f 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -18,7 +18,7 @@ */ class HTMLErrorRenderer extends AbstractErrorRenderer { - public function renderLanguageExceptionOutput() + public function renderPhpExceptionOutput() { $title = 'Slim Application Error'; $e = $this->exception; diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php index a2858baa5..9c73c1e17 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -15,7 +15,7 @@ */ class JSONErrorRenderer extends AbstractErrorRenderer { - public function renderLanguageExceptionOutput() + public function renderPhpExceptionOutput() { $e = $this->exception; $error = ['message' => 'Slim Application Error']; diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index cb2716aff..5aea2f92d 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -15,7 +15,7 @@ */ class PlainTextErrorRenderer extends AbstractErrorRenderer { - public function renderLanguageExceptionOutput() + public function renderPhpExceptionOutput() { return $this->exception->getMessage(); } diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php index b984f3550..5ad69bbab 100644 --- a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php @@ -18,7 +18,7 @@ class XMLErrorRenderer extends AbstractErrorRenderer /** * @return string */ - public function renderLanguageExceptionOutput() + public function renderPhpExceptionOutput() { $e = $this->exception; $xml = "\n Slim Application Error\n"; diff --git a/Slim/Interfaces/ErrorRendererInterface.php b/Slim/Interfaces/ErrorRendererInterface.php index 2da2b17c1..f52df6dfd 100644 --- a/Slim/Interfaces/ErrorRendererInterface.php +++ b/Slim/Interfaces/ErrorRendererInterface.php @@ -16,6 +16,6 @@ */ interface ErrorRendererInterface { - public function renderLanguageExceptionOutput(); + public function renderPhpExceptionOutput(); public function renderGenericExceptionOutput(); } From 64ed37ae1cab3d142423e042c4f5a57bec6299bc Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 11:29:09 -0600 Subject: [PATCH 06/36] Fix HttpNotAllowedException allowed methods setting in App invoke --- Slim/App.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index 435306fb7..641ec972a 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -712,14 +712,15 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res return $route->run($request, $response); case Dispatcher::METHOD_NOT_ALLOWED: - $exception = new HttpNotAllowedException(['method' => $routeInfo[1]]); + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods($routeInfo[1]); $exception->setRequest($request); - break; + break; case Dispatcher::NOT_FOUND: $exception = new HttpNotFoundException; $exception->setRequest($request); - break; + break; } return $this->handleException($exception, $request, $response); From 662d5502179dbf65e56d70957ae9900c8f7c9286 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 12:09:24 -0600 Subject: [PATCH 07/36] Refactor AbstractErrorHandler renderer resolver and respond methods --- Slim/Handlers/AbstractErrorHandler.php | 67 +++++++++++++++----------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index d93918cdf..148236682 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -12,7 +12,6 @@ use UnexpectedValueException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; @@ -21,6 +20,7 @@ use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; use Slim\Http\Body; use Slim\Interfaces\ErrorHandlerInterface; +use Slim\Interfaces\ErrorRendererInterface; /** * Default Slim application error handler @@ -66,7 +66,7 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface */ protected $exception; /** - * @var HTMLErrorRenderer|JSONErrorRenderer|XMLErrorRenderer|PlainTextErrorRenderer + * @var ErrorRendererInterface */ protected $renderer = null; /** @@ -134,35 +134,44 @@ protected function resolveContentType() } /** - * @return HTMLErrorRenderer|JSONErrorRenderer|XMLErrorRenderer|PlainTextErrorRenderer - * @throws HttpBadRequestException + * Determine which renderer to use based on content type + * Overloaded $renderer from calling class takes precedence over all + * + * @return ErrorRendererInterface + * @throws UnexpectedValueException */ protected function resolveRenderer() { - if ($this->method === 'OPTIONS') { - $this->statusCode = 200; - $this->contentType = 'text/plain'; - return PlainTextErrorRenderer::class; - } + $renderer = null; if (!is_null($this->renderer)) { - return $this->renderer; + $renderer = $this->renderer; + } else if ($this->method === 'OPTIONS') { + $this->statusCode = 200; + $this->contentType = 'text/plain'; + $renderer = PlainTextErrorRenderer::class; + } else { + switch ($this->contentType) { + case 'application/json': + $renderer = JSONErrorRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XMLErrorRenderer::class; + break; + + case 'text/html': + $renderer = HTMLErrorRenderer::class; + break; + + default: + throw new UnexpectedValueException(sprintf('Cannot render unknown content type: %s', $this->contentType)); + break; + } } - switch ($this->contentType) { - case 'application/json': - return JSONErrorRenderer::class; - - case 'text/xml': - case 'application/xml': - return XMLErrorRenderer::class; - - case 'text/html': - return HTMLErrorRenderer::class; - - default: - throw new UnexpectedValueException(sprintf('Cannot render unknown content type: %s', $this->contentType)); - } + return new $renderer($this->exception, $this->displayErrorDetails); } /** @@ -185,16 +194,16 @@ protected function resolveStatusCode() public function respond() { $e = $this->exception; - $renderer = new $this->renderer($e, $this->displayErrorDetails); - $output = $renderer->render(); + $response = $this->response; + $output = $this->renderer->render(); $body = new Body(fopen('php://temp', 'r+')); $body->write($output); - if ($this->exception instanceof HttpNotAllowedException) { - $this->response->withHeader('Allow', $e->getAllowedMethods()); + if ($e instanceof HttpNotAllowedException) { + $response = $response->withHeader('Allow', $e->getAllowedMethods()); } - return $this->response + return $response ->withStatus($this->statusCode) ->withHeader('Content-type', $this->contentType) ->withBody($body); From 360f685ba656e2a332755dacce1105855648a883 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 13:06:10 -0600 Subject: [PATCH 08/36] Refactor App->getErrorHandler() logic and add custom renderer type checking in AbstractErrorHandler --- Slim/App.php | 32 ++++++++++++++------------ Slim/Handlers/AbstractErrorHandler.php | 25 ++++++++++++++++---- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index 641ec972a..e7f8c447b 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -8,8 +8,6 @@ */ namespace Slim; -use BadMethodCallException; -use Exception; use FastRoute\Dispatcher; use Interop\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; @@ -19,12 +17,14 @@ use Slim\Exception\HttpNotAllowedException; use Slim\Exception\PhpException; use Slim\Handlers\AbstractErrorHandler; -use Slim\Handlers\AbstractHandler; 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 RuntimeException; use Throwable; @@ -275,9 +275,9 @@ public function getRouter() * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string|callable $handler */ - public function setNotFoundHandler(callable $handler) + public function setNotFoundHandler($handler) { $this->setErrorHandler(HttpNotFoundException::class, $handler); } @@ -307,9 +307,9 @@ public function getNotFoundHandler() * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param callable $handler + * @param string|callable $handler */ - public function setNotAllowedHandler(callable $handler) + public function setNotAllowedHandler($handler) { $this->setErrorHandler(HttpNotAllowedException::class, $handler); } @@ -339,9 +339,9 @@ public function getNotAllowedHandler() * \Psr\Http\Message\ResponseInterface. * * @param string $type - * @param callable $handler + * @param string|callable $handler */ - public function setErrorHandler($type, callable $handler) + public function setErrorHandler($type, $handler) { $handlers = $this->getSetting('errorHandlers', []); $handlers[$type] = $handler; @@ -353,7 +353,7 @@ public function setErrorHandler($type, callable $handler) * occurs when processing the current request. * * @param null|string $type - * @return callable|ErrorHandler + * @return callable|ErrorHandlerInterface */ public function getErrorHandler($type = null) { @@ -365,9 +365,11 @@ public function getErrorHandler($type = null) if (is_callable($handler)) { return $handler; - } else if ($handler instanceof AbstractErrorHandler) { - $handler = get_class($handler); + } else if (is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) { return new $handler($displayErrorDetails); + } else if ($handler instanceof AbstractErrorHandler) { + $handler->setDisplayErrorDetails($displayErrorDetails); + return $handler; } } @@ -387,9 +389,9 @@ public function getErrorHandler($type = null) * 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 setPhpErrorHandler($handler) { $this->setErrorHandler(PhpException::class, $handler); } @@ -398,7 +400,7 @@ public function setPhpErrorHandler(callable $handler) * Get callable to handle scenarios where a PHP error * occurs when processing the current request. * - * @return callable|Error + * @return callable|ErrorHandlerInterface */ public function getPhpErrorHandler() { diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 148236682..9d7c608ba 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -8,8 +8,6 @@ */ namespace Slim\Handlers; -use Exception; -use UnexpectedValueException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpException; @@ -21,6 +19,8 @@ use Slim\Http\Body; use Slim\Interfaces\ErrorHandlerInterface; use Slim\Interfaces\ErrorRendererInterface; +use Error; +use Exception; /** * Default Slim application error handler @@ -137,8 +137,8 @@ protected function resolveContentType() * Determine which renderer to use based on content type * Overloaded $renderer from calling class takes precedence over all * + * @throws Error * @return ErrorRendererInterface - * @throws UnexpectedValueException */ protected function resolveRenderer() { @@ -146,6 +146,12 @@ protected function resolveRenderer() if (!is_null($this->renderer)) { $renderer = $this->renderer; + if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { + throw new Error(sprintf( + 'Non compliant error renderer provided (%s). Renderer expected to be a subclass of AbstractErrorRenderer', + $renderer + )); + } } else if ($this->method === 'OPTIONS') { $this->statusCode = 200; $this->contentType = 'text/plain'; @@ -166,7 +172,10 @@ protected function resolveRenderer() break; default: - throw new UnexpectedValueException(sprintf('Cannot render unknown content type: %s', $this->contentType)); + throw new Error(sprintf( + 'Cannot render unknown content type: %s', + $this->contentType + )); break; } } @@ -188,6 +197,14 @@ protected function resolveStatusCode() return $statusCode; } + /** + * @param bool $displayErrorDetails + */ + public function setDisplayErrorDetails($displayErrorDetails) + { + $this->displayErrorDetails = $displayErrorDetails; + } + /** * @return ResponseInterface */ From 2c97dd184b37f3f078a0848ca3fff63e328f58bc Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 13:18:02 -0600 Subject: [PATCH 09/36] Add HttpUnauthorizedException and fix PHP Doc block in AbstractErrorHandler --- Slim/Exception/HttpUnauthorizedException.php | 10 ++++++++++ Slim/Handlers/AbstractErrorHandler.php | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 Slim/Exception/HttpUnauthorizedException.php diff --git a/Slim/Exception/HttpUnauthorizedException.php b/Slim/Exception/HttpUnauthorizedException.php new file mode 100644 index 000000000..2ba2d0132 --- /dev/null +++ b/Slim/Exception/HttpUnauthorizedException.php @@ -0,0 +1,10 @@ + Date: Wed, 10 May 2017 17:43:53 -0600 Subject: [PATCH 10/36] Fix all unit tests to get passing build --- Slim/Exception/PhpException.php | 12 +- Slim/Handlers/AbstractErrorHandler.php | 50 ++++- Slim/Handlers/AbstractErrorRenderer.php | 7 +- .../ErrorRenderers/HTMLErrorRenderer.php | 17 +- .../ErrorRenderers/JSONErrorRenderer.php | 39 +++- .../ErrorRenderers/PlainTextErrorRenderer.php | 35 +++- tests/AppTest.php | 106 ++-------- tests/ContainerTest.php | 16 -- ...rTest.php => AbstractErrorHandlerTest.php} | 22 +-- tests/Handlers/AbstractErrorRendererTest.php | 24 +++ .../{ErrorTest.php => ErrorHandlerTest.php} | 38 ++-- tests/Handlers/NotAllowedTest.php | 84 -------- tests/Handlers/NotFoundTest.php | 75 ------- tests/Handlers/PhpErrorTest.php | 183 ------------------ tests/Http/RequestTest.php | 8 +- tests/Http/UploadedFilesTest.php | 22 ++- 16 files changed, 205 insertions(+), 533 deletions(-) rename tests/Handlers/{AbstractHandlerTest.php => AbstractErrorHandlerTest.php} (74%) create mode 100644 tests/Handlers/AbstractErrorRendererTest.php rename tests/Handlers/{ErrorTest.php => ErrorHandlerTest.php} (70%) delete mode 100644 tests/Handlers/NotAllowedTest.php delete mode 100644 tests/Handlers/NotFoundTest.php delete mode 100644 tests/Handlers/PhpErrorTest.php diff --git a/Slim/Exception/PhpException.php b/Slim/Exception/PhpException.php index ef93bbf49..cdcfc186a 100644 --- a/Slim/Exception/PhpException.php +++ b/Slim/Exception/PhpException.php @@ -5,22 +5,12 @@ class PhpException extends Exception { - protected $exception; - /** * PhpException constructor. * @param $exception */ public function __construct($exception) { - $this->exception = $exception; - } - - /** - * @return mixed - */ - public function getException() - { - return $this->exception; + parent::__construct('PHP Error', 500, $exception); } } \ No newline at end of file diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 50760f37b..12b9d0b00 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -21,6 +21,7 @@ use Slim\Interfaces\ErrorRendererInterface; use Error; use Exception; +use Throwable; /** * Default Slim application error handler @@ -44,7 +45,7 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface /** * @var bool */ - protected $displayErrorDetails = false; + protected $displayErrorDetails; /** * @var string */ @@ -78,7 +79,7 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface * AbstractHandler constructor. * @param bool $displayErrorDetails */ - public function __construct($displayErrorDetails = true) + public function __construct($displayErrorDetails = false) { $this->displayErrorDetails = $displayErrorDetails; } @@ -98,10 +99,14 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $this->response = $response; $this->exception = $exception; $this->method = $request->getMethod(); - $this->contentType = $this->resolveContentType(); + $this->contentType = $this->resolveContentType($request); $this->renderer = $this->resolveRenderer(); $this->statusCode = $this->resolveStatusCode(); + if (!$this->displayErrorDetails) { + $this->writeToErrorLog($exception); + } + return $this->respond(); } @@ -112,11 +117,12 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res * 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 resolveContentType() + protected function resolveContentType(ServerRequestInterface $request) { - $acceptHeader = $this->request->getHeaderLine('Accept'); + $acceptHeader = $request->getHeaderLine('Accept'); $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); if (count($selectedContentTypes)) { @@ -226,4 +232,38 @@ public function respond() ->withHeader('Content-type', $this->contentType) ->withBody($body); } + + /** + * Write to the error log if displayErrorDetails is false + * + * @param Exception|Throwable $throwable + * + * @return void + */ + protected function writeToErrorLog($throwable) + { + if ($this->displayErrorDetails) { + return; + } + $renderer = new PlainTextErrorRenderer($throwable); + $message = 'Slim Application Error:' . PHP_EOL; + $message .= $renderer->render(); + while ($throwable = $throwable->getPrevious()) { + $renderer = new PlainTextErrorRenderer($throwable); + $message .= PHP_EOL . 'Previous Error:' . PHP_EOL; + $message .= $renderer->render(); + } + $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($message); + } + + /** + * 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/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index f5d0e9291..ab7d25fd4 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -8,9 +8,10 @@ */ namespace Slim\Handlers; -use Exception; use Slim\Exception\PhpException; use Slim\Interfaces\ErrorRendererInterface; +use Exception; +use Throwable; /** * Default Slim application error renderer @@ -31,10 +32,10 @@ abstract class AbstractErrorRenderer implements ErrorRendererInterface /** * AbstractErrorRenderer constructor. - * @param Exception $exception + * @param Exception|Throwable $exception * @param bool $displayErrorDetails */ - public function __construct(Exception $exception, $displayErrorDetails = false) + public function __construct($exception, $displayErrorDetails = false) { $this->exception = $exception; $this->displayErrorDetails = $displayErrorDetails; diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php index 86672db1f..ab818efa2 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -20,21 +20,14 @@ class HTMLErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { - $title = 'Slim Application Error'; $e = $this->exception; - $re = $e->getException(); + $title = 'Slim Application Error'; if ($this->displayErrorDetails) { $html = '

The application could not run because of the following error:

'; $html .= '

Details

'; $html .= $this->renderExceptionFragment($e); - $html .= '

Previous exception

'; - $html .= $this->renderExceptionFragment($re); - while ($e = $e->getPrevious()) { - $html .= '

Previous exception

'; - $html .= $this->renderExceptionFragment($e); - } } else { $html = '

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

'; } @@ -55,13 +48,17 @@ public function renderPhpExceptionOutput() public function renderGenericExceptionOutput() { $e = $this->exception; + $title = ''; + $description = ''; + + if ($this->displayErrorDetails) { + $description = $e->getMessage(); + } - $title = $e->getMessage(); if (method_exists($e, 'getTitle')) { $title = $e->getTitle(); } - $description = ''; if (method_exists($e, 'getDescription')) { $description = $e->getDescription(); } diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php index 9c73c1e17..b441ea1cd 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -9,6 +9,8 @@ namespace Slim\Handlers\ErrorRenderers; use Slim\Handlers\AbstractErrorRenderer; +use Exception; +use Throwable; /** * Default Slim application JSON Error Renderer @@ -22,16 +24,8 @@ public function renderPhpExceptionOutput() if ($this->displayErrorDetails) { $error['exception'] = []; - do { - $error['exception'][] = [ - 'type' => get_class($e), - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => explode("\n", $e->getTraceAsString()), - ]; + $error['exception'][] = $this->renderExceptionFragment($e); } while ($e = $e->getPrevious()); } @@ -40,7 +34,32 @@ public function renderPhpExceptionOutput() public function renderGenericExceptionOutput() { - $error = ['message' => $this->exception->getMessage()]; + $e = $this->exception; + $error = ['message' => $e->getMessage()]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->renderExceptionFragment($e); + } while ($e = $e->getPrevious()); + } + return json_encode($error, JSON_PRETTY_PRINT); } + + /** + * @param Exception|Throwable $e + * @return array + */ + public function renderExceptionFragment($e) + { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => explode("\n", $e->getTraceAsString()), + ]; + } } \ No newline at end of file diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index 5aea2f92d..8723343d3 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -17,11 +17,42 @@ class PlainTextErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { - return $this->exception->getMessage(); + $e = $this->exception; + return $this->renderExceptionFragment($e); } public function renderGenericExceptionOutput() { - return $this->exception->getMessage(); + $e = $this->exception; + + if (!$this->displayErrorDetails) { + return $this->renderExceptionFragment($e); + } + + return $e->getMessage(); + } + + public function renderExceptionFragment() + { + $e = $this->exception; + $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; } } \ No newline at end of file diff --git a/tests/AppTest.php b/tests/AppTest.php index ab7772bf9..ed9b530e0 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -12,8 +12,9 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Slim\App; -use Slim\Exception\MethodNotAllowedException; -use Slim\Exception\NotFoundException; +use Slim\Exception\HttpNotAllowedException; +use Slim\Exception\HttpNotFoundException; +use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; use Slim\Handlers\Strategies\RequestResponseArgs; use Slim\Http\Body; use Slim\Http\Environment; @@ -88,6 +89,12 @@ public function testAddSetting() $this->assertAttributeContains('foo', 'settings', $app); } + public function testGetErrorHandler() + { + $app = new App(); + $this->assertInstanceOf('\Slim\Handlers\ErrorHandler', $app->getErrorHandler()); + } + /******************************************************************************** * Router proxy methods *******************************************************************************/ @@ -1016,6 +1023,11 @@ public function testInvokeReturnMethodNotAllowed() $req = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); $res = new Response(); + // Create Html Renderer and Assert Output + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods(['GET']); + $renderer = new HTMLErrorRenderer($exception, false); + // Invoke app $resOut = $app($req, $res); @@ -1023,7 +1035,7 @@ public function testInvokeReturnMethodNotAllowed() $this->assertEquals(405, (string)$resOut->getStatusCode()); $this->assertEquals(['GET'], $resOut->getHeader('Allow')); $this->assertContains( - '

Method not allowed. Must be one of: GET

', + $renderer->render(), (string)$resOut->getBody() ); @@ -1784,28 +1796,6 @@ public function appFactory() return $app; } - /** - * throws \Exception - * throws \Slim\Exception\MethodNotAllowedException - * throws \Slim\Exception\NotFoundException - * expectedException \Exception - */ -// public function testRunExceptionNoHandler() -// { -// $app = $this->appFactory(); -// -// $container = $app->getContainer(); -// unset($container['errorHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new \Exception(); -// }); -// $res = $app->run(true); -// } - /** * @requires PHP 7.0 */ @@ -1834,64 +1824,28 @@ public function testRunNotFound() $app->get('/foo', function ($req, $res, $args) { return $res; }); - $app->add(function ($req, $res, $args) { - throw new NotFoundException($req, $res); + $app->add(function () { + throw new HttpNotFoundException; }); $res = $app->run(true); $this->assertEquals(404, $res->getStatusCode()); } - /** - * expectedException \Slim\Exception\NotFoundException - */ -// public function testRunNotFoundWithoutHandler() -// { -// $app = $this->appFactory(); -// $container = $app->getContainer(); -// unset($container['notFoundHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new NotFoundException($req, $res); -// }); -// $res = $app->run(true); -// } - public function testRunNotAllowed() { $app = $this->appFactory(); $app->get('/foo', function ($req, $res, $args) { return $res; }); - $app->add(function ($req, $res, $args) { - throw new MethodNotAllowedException($req, $res, ['POST']); + $app->add(function () { + throw new HttpNotAllowedException; }); $res = $app->run(true); $this->assertEquals(405, $res->getStatusCode()); } - /** - * expectedException \Slim\Exception\MethodNotAllowedException - */ -// public function testRunNotAllowedWithoutHandler() -// { -// $app = $this->appFactory(); -// $container = $app->getContainer(); -// unset($container['notAllowedHandler']); -// -// $app->get('/foo', function ($req, $res, $args) { -// return $res; -// }); -// $app->add(function ($req, $res, $args) { -// throw new MethodNotAllowedException($req, $res, ['POST']); -// }); -// $res = $app->run(true); -// } - public function testAppRunWithdetermineRouteBeforeAppMiddleware() { $app = $this->appFactory(); @@ -1985,8 +1939,9 @@ public function testCallingAContainerCallable() $request = new Request('GET', Uri::createFromString(''), $headers, [], [], $body); $response = new Response(); + $exception = new HttpNotFoundException; $notFoundHandler = $app->getNotFoundHandler(); - $response = $notFoundHandler($request, $response); + $response = $notFoundHandler($request, $response, $exception); $this->assertSame(404, $response->getStatusCode()); } @@ -2147,25 +2102,6 @@ public function testIsEmptyResponseWithoutEmptyMethod() $this->assertTrue($result); } - public function testHandlePhpError() - { - $this->skipIfPhp70(); - $method = new \ReflectionMethod('Slim\App', 'handlePhpError'); - $method->setAccessible(true); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $res = new Response(); - - $res = $method->invoke(new App(), $throwable, $req, $res); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame('text/html', $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), '')); - } - protected function skipIfPhp70() { if (version_compare(PHP_VERSION, '7.0', '>=')) { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 32981858c..a9e8bfdfe 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -124,22 +124,6 @@ public function testGetRouter() $this->assertInstanceOf('\Slim\Router', $this->container['router']); } - /** - * Test container has error handler - */ - public function testGetErrorHandler() - { - $this->assertInstanceOf('\Slim\Handlers\Error', $this->container['errorHandler']); - } - - /** - * Test container has error handler - */ - public function testGetNotAllowedHandler() - { - $this->assertInstanceOf('\Slim\Handlers\NotAllowed', $this->container['notAllowedHandler']); - } - /** * Test settings can be edited */ diff --git a/tests/Handlers/AbstractHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php similarity index 74% rename from tests/Handlers/AbstractHandlerTest.php rename to tests/Handlers/AbstractErrorHandlerTest.php index afd27afdf..8f0e54132 100644 --- a/tests/Handlers/AbstractHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -9,9 +9,9 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; -use Slim\Handlers\AbstractHandler; +use Slim\Handlers\AbstractErrorHandler; -class AbstractHandlerTest extends TestCase +class AbstractErrorHandlerTest extends TestCase { public function testHalfValidContentType() { @@ -19,7 +19,7 @@ public function testHalfValidContentType() $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue('unknown/+json')); - $abstractHandler = $this->getMockForAbstractClass(AbstractHandler::class); + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); $newTypes = [ 'application/xml', @@ -27,13 +27,13 @@ public function testHalfValidContentType() 'text/html', ]; - $class = new \ReflectionClass(AbstractHandler::class); + $class = new \ReflectionClass(AbstractErrorHandler::class); $reflectionProperty = $class->getProperty('knownContentTypes'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($abstractHandler, $newTypes); - $method = $class->getMethod('determineContentType'); + $method = $class->getMethod('resolveContentType'); $method->setAccessible(true); $return = $method->invoke($abstractHandler, $req); @@ -55,15 +55,15 @@ public function testAcceptableMediaTypeIsNotFirstInList() ->method('getHeaderLine') ->willReturn('text/plain,text/html'); - // provide access to the determineContentType() as it's a protected method - $class = new \ReflectionClass(AbstractHandler::class); - $method = $class->getMethod('determineContentType'); + // provide access to the resolveContentType() as it's a protected method + $class = new \ReflectionClass(AbstractErrorHandler::class); + $method = $class->getMethod('resolveContentType'); $method->setAccessible(true); - // use a mock object here as AbstractHandler cannot be directly instantiated - $abstractHandler = $this->getMockForAbstractClass(AbstractHandler::class); + // use a mock object here as AbstractErrorHandler cannot be directly instantiated + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); - // call determineContentType() + // call resolveContentType() $return = $method->invoke($abstractHandler, $request); $this->assertEquals('text/html', $return); diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php new file mode 100644 index 000000000..da4869413 --- /dev/null +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -0,0 +1,24 @@ +assertEquals('Oops..', $renderer->render()); + } +} \ No newline at end of file diff --git a/tests/Handlers/ErrorTest.php b/tests/Handlers/ErrorHandlerTest.php similarity index 70% rename from tests/Handlers/ErrorTest.php rename to tests/Handlers/ErrorHandlerTest.php index a513acc42..00dc1fe0a 100644 --- a/tests/Handlers/ErrorTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -9,7 +9,7 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; -use Slim\Handlers\Error; +use Slim\Handlers\ErrorHandler; use Slim\Http\Response; class ErrorTest extends TestCase @@ -31,13 +31,13 @@ public function errorProvider() * * @dataProvider errorProvider */ - public function testError($acceptHeader, $contentType, $startOfBody) + public function testErrorHandler($acceptHeader, $contentType, $startOfBody) { - $error = new Error(); + $errorHandler = new ErrorHandler(); $e = new \Exception("Oops", 1, new \Exception('Previous oops')); /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); @@ -49,13 +49,13 @@ public function testError($acceptHeader, $contentType, $startOfBody) * * @dataProvider errorProvider */ - public function testErrorDisplayDetails($acceptHeader, $contentType, $startOfBody) + public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $startOfBody) { - $error = new Error(true); + $errorHandler = new ErrorHandler(true); $e = new \Exception('Oops', 1, new \Exception('Opps before')); /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); @@ -63,12 +63,12 @@ public function testErrorDisplayDetails($acceptHeader, $contentType, $startOfBod } /** - * @expectedException \UnexpectedValueException + * @expectedException \Error */ public function testNotFoundContentType() { - $errorMock = $this->getMockBuilder(Error::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') + $errorMock = $this->getMockBuilder(ErrorHandler::class)->setMethods(['resolveContentType'])->getMock(); + $errorMock->method('resolveContentType') ->will($this->returnValue('unknown/type')); $e = new \Exception("Oops"); @@ -84,7 +84,7 @@ public function testNotFoundContentType() */ public function testPreviousException() { - $error = $this->getMockBuilder('\Slim\Handlers\Error')->setMethods(['logError'])->getMock(); + $error = $this->getMockBuilder('\Slim\Handlers\ErrorHandler')->setMethods(['logError'])->getMock(); $error->expects($this->once())->method('logError')->with( $this->logicalAnd( $this->stringContains("Type: Exception" . PHP_EOL . "Message: Second Oops"), @@ -98,22 +98,6 @@ public function testPreviousException() $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second); } - /** - * If someone extends the Error handler and calls renderHtmlExceptionOrError with - * a parameter that isn't an Exception or Error, then we thrown an Exception. - * - * @expectedException \RuntimeException - */ - public function testRenderHtmlExceptionorErrorTypeChecksParameter() - { - $class = new \ReflectionClass(Error::class); - $renderHtmlExceptionorError = $class->getMethod('renderHtmlExceptionOrError'); - $renderHtmlExceptionorError->setAccessible(true); - - $error = new Error(); - $renderHtmlExceptionorError->invokeArgs($error, ['foo']); - } - /** * @param string $method * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request diff --git a/tests/Handlers/NotAllowedTest.php b/tests/Handlers/NotAllowedTest.php deleted file mode 100644 index fa26d5523..000000000 --- a/tests/Handlers/NotAllowedTest.php +++ /dev/null @@ -1,84 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider invalidMethodProvider - */ - public function testInvalidMethod($acceptHeader, $contentType, $startOfBody) - { - $notAllowed = new NotAllowed(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('GET', $acceptHeader), new Response(), ['POST', 'PUT']); - - $this->assertSame(405, $res->getStatusCode()); - $this->assertTrue($res->hasHeader('Allow')); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - public function testOptions() - { - $notAllowed = new NotAllowed(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('OPTIONS'), new Response(), ['POST', 'PUT']); - - $this->assertSame(200, $res->getStatusCode()); - $this->assertTrue($res->hasHeader('Allow')); - $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); - } - - /** - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(NotAllowed::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $errorMock->__invoke($this->getRequest('GET', 'unknown/type'), new Response(), ['POST']); - } - - /** - * @param string $method - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $contentType = 'text/html') - { - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getMethod')->will($this->returnValue($method)); - $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue($contentType)); - - return $req; - } -} diff --git a/tests/Handlers/NotFoundTest.php b/tests/Handlers/NotFoundTest.php deleted file mode 100644 index 5500c8b5c..000000000 --- a/tests/Handlers/NotFoundTest.php +++ /dev/null @@ -1,75 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider notFoundProvider - */ - public function testNotFound($acceptHeader, $contentType, $startOfBody) - { - $notAllowed = new NotFound(); - - /** @var Response $res */ - $res = $notAllowed->__invoke($this->getRequest('GET', $acceptHeader), new Response(), ['POST', 'PUT']); - - $this->assertSame(404, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(NotFound::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), ['POST']); - } - - /** - * @param string $method - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $contentType = 'text/html') - { - $uri = new Uri('http', 'example.com', 80, '/notfound'); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getHeaderLine')->will($this->returnValue($contentType)); - $req->expects($this->any())->method('getUri')->will($this->returnValue($uri)); - - return $req; - } -} diff --git a/tests/Handlers/PhpErrorTest.php b/tests/Handlers/PhpErrorTest.php deleted file mode 100644 index 5e5e3188d..000000000 --- a/tests/Handlers/PhpErrorTest.php +++ /dev/null @@ -1,183 +0,0 @@ -'], - ['application/hal+xml', 'application/xml', ''], - ['text/xml', 'text/xml', ''], - ['text/html', 'text/html', ''], - ]; - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 7.0 - * @dataProvider phpErrorProvider - */ - public function testPhpError($acceptHeader, $contentType, $startOfBody) - { - $error = new PhpError(); - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), new \Exception()); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 7.0 - * @dataProvider phpErrorProvider - */ - public function testPhpErrorDisplayDetails($acceptHeader, $contentType, $startOfBody) - { - $error = new PhpError(true); - - $exception = new \Exception('Oops', 1, new \Exception('Opps before')); - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $exception); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @requires PHP 7.0 - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(PhpError::class)->setMethods(['determineContentType'])->getMock(); - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), new \Exception()); - } - - /** - * Test invalid method returns the correct code and content type - * - * @requires PHP 5.0 - * @dataProvider phpErrorProvider - */ - public function testPhpError5($acceptHeader, $contentType, $startOfBody) - { - $this->skipIfPhp70(); - $error = new PhpError(); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - /** @var \Throwable $throwable */ - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $throwable); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - - - /** - * Test invalid method returns the correct code and content type - * - * @dataProvider phpErrorProvider - */ - public function testPhpErrorDisplayDetails5($acceptHeader, $contentType, $startOfBody) - { - $this->skipIfPhp70(); - - $error = new PhpError(true); - - $throwable = $this->getMockBuilder('\Throwable') - ->setMethods(['getCode', 'getMessage', 'getFile', 'getLine', 'getTraceAsString', 'getPrevious'])->getMock(); - - $throwablePrev = clone $throwable; - - $throwable->method('getCode')->will($this->returnValue(1)); - $throwable->method('getMessage')->will($this->returnValue('Oops')); - $throwable->method('getFile')->will($this->returnValue('test.php')); - $throwable->method('getLine')->will($this->returnValue('1')); - $throwable->method('getTraceAsString')->will($this->returnValue('This is error')); - $throwable->method('getPrevious')->will($this->returnValue($throwablePrev)); - - /** @var \Throwable $throwable */ - - /** @var Response $res */ - $res = $error->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $throwable); - - $this->assertSame(500, $res->getStatusCode()); - $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); - $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); - } - - /** - * @requires PHP 5.0 - * @expectedException \UnexpectedValueException - */ - public function testNotFoundContentType5() - { - $this->skipIfPhp70(); - $errorMock = $this->getMockBuilder(PhpError::class)->setMethods(['determineContentType'])->getMock(); - - $errorMock->method('determineContentType') - ->will($this->returnValue('unknown/type')); - - $throwable = $this->getMockBuilder('\Throwable')->getMock(); - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), $throwable); - } - - /** - * @param string $method - * - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request - */ - protected function getRequest($method, $acceptHeader) - { - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - $req->expects($this->once())->method('getHeaderLine')->will($this->returnValue($acceptHeader)); - - return $req; - } - - /** - * @return mixed - */ - protected function skipIfPhp70() - { - if (version_compare(PHP_VERSION, '7.0', '>=')) { - $this->markTestSkipped(); - } - } -} diff --git a/tests/Http/RequestTest.php b/tests/Http/RequestTest.php index 6d0bdbc75..ab068c23e 100644 --- a/tests/Http/RequestTest.php +++ b/tests/Http/RequestTest.php @@ -10,14 +10,16 @@ namespace Slim\Tests\Http; use PHPUnit\Framework\TestCase; -use ReflectionProperty; use Slim\Collection; +use Slim\Exception\HttpNotImplementedException; use Slim\Http\Environment; use Slim\Http\Headers; use Slim\Http\Request; use Slim\Http\RequestBody; use Slim\Http\UploadedFile; use Slim\Http\Uri; +use ReflectionProperty; +use InvalidArgumentException; class RequestTest extends TestCase { @@ -84,7 +86,7 @@ public function testWithAllAllowedCharactersMethod() } /** - * @expectedException \InvalidArgumentException + * @expectedException \Slim\Exception\HttpNotImplementedException */ public function testWithMethodInvalid() { @@ -208,7 +210,7 @@ public function testGetMethodOverrideParameterFromBodyArray() } /** - * @expectedException \InvalidArgumentException + * @expectedException \Slim\Exception\HttpNotImplementedException */ public function testCreateRequestWithInvalidMethodString() { diff --git a/tests/Http/UploadedFilesTest.php b/tests/Http/UploadedFilesTest.php index e8ca325ea..e45d77c43 100644 --- a/tests/Http/UploadedFilesTest.php +++ b/tests/Http/UploadedFilesTest.php @@ -153,11 +153,11 @@ public function testMoveToNotWritable(UploadedFile $uploadedFile) $uploadedFile->moveTo($path); } - /** + /* * @depends testConstructor * @param UploadedFile $uploadedFile * @return UploadedFile - */ + * public function testMoveTo(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); @@ -170,12 +170,13 @@ public function testMoveTo(UploadedFile $uploadedFile) return $uploadedFile; } + */ - /** + /* * @depends testMoveTo * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - */ + * public function testMoveToCannotBeDoneTwice(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); @@ -186,38 +187,43 @@ public function testMoveToCannotBeDoneTwice(UploadedFile $uploadedFile) $uploadedFile->moveTo($path); } + */ - /** + /* * This test must run after testMoveTo * * @depends testConstructor * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - */ + * public function testMoveToAgain(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $tempName; $uploadedFile->moveTo($path); } + */ - /** + /* * This test must run after testMoveTo * * @depends testConstructor * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - */ + * public function testMovedStream($uploadedFile) { $uploadedFile->getStream(); } + */ + /* public function testMoveToStream() { $uploadedFile = $this->generateNewTmpFile(); $uploadedFile->moveTo('php://temp'); } + */ public function providerCreateFromEnvironment() { From a6d1a3e7222c5607a3e7efd52278befb84420652 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 17:50:49 -0600 Subject: [PATCH 11/36] Fix all coding standard violations with phpcbf --- Slim/App.php | 8 ++++---- Slim/Exception/HttpBadRequestException.php | 2 +- Slim/Exception/HttpException.php | 4 ++-- Slim/Exception/HttpInternalServerErrorException.php | 2 +- Slim/Exception/HttpNotAllowedException.php | 4 ++-- Slim/Exception/HttpNotFoundException.php | 2 +- Slim/Exception/HttpNotImplementedException.php | 2 +- Slim/Exception/HttpUnauthorizedException.php | 2 +- Slim/Exception/PhpException.php | 2 +- Slim/Handlers/AbstractErrorHandler.php | 8 ++++---- Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php | 3 +-- Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php | 2 +- Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php | 2 +- Slim/Http/Request.php | 1 - tests/Handlers/AbstractErrorRendererTest.php | 2 +- 16 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index e7f8c447b..eb25179e7 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -365,9 +365,9 @@ public function getErrorHandler($type = null) if (is_callable($handler)) { return $handler; - } else if (is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) { + } elseif (is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) { return new $handler($displayErrorDetails); - } else if ($handler instanceof AbstractErrorHandler) { + } elseif ($handler instanceof AbstractErrorHandler) { $handler->setDisplayErrorDetails($displayErrorDetails); return $handler; } @@ -717,12 +717,12 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $exception = new HttpNotAllowedException; $exception->setAllowedMethods($routeInfo[1]); $exception->setRequest($request); - break; + break; case Dispatcher::NOT_FOUND: $exception = new HttpNotFoundException; $exception->setRequest($request); - break; + break; } return $this->handleException($exception, $request, $response); diff --git a/Slim/Exception/HttpBadRequestException.php b/Slim/Exception/HttpBadRequestException.php index 08e18b77b..5f503fc64 100644 --- a/Slim/Exception/HttpBadRequestException.php +++ b/Slim/Exception/HttpBadRequestException.php @@ -7,4 +7,4 @@ class HttpBadRequestException extends HttpException protected $message = 'Bad request.'; protected $title = '400 Bad Request'; protected $description = 'The server cannot or will not process the request due to an apparent client error.'; -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpException.php b/Slim/Exception/HttpException.php index e35b89f7b..59bb4b2f0 100644 --- a/Slim/Exception/HttpException.php +++ b/Slim/Exception/HttpException.php @@ -40,7 +40,7 @@ public function __construct($details = null) { if (is_string($details)) { parent::__construct($details); - } else if (!is_null($details)) { + } elseif (!is_null($details)) { $this->details = $details; } } @@ -137,4 +137,4 @@ public function isRecoverable() { return $this->recoverable; } -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpInternalServerErrorException.php b/Slim/Exception/HttpInternalServerErrorException.php index 528f3c492..1cd19f28b 100644 --- a/Slim/Exception/HttpInternalServerErrorException.php +++ b/Slim/Exception/HttpInternalServerErrorException.php @@ -7,4 +7,4 @@ class HttpInternalServerErrorException extends HttpException protected $message = 'Internal server error.'; protected $title = '500 Internal Server Error'; protected $description = 'A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.'; -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpNotAllowedException.php b/Slim/Exception/HttpNotAllowedException.php index 2ea066ddf..9c942c3d6 100644 --- a/Slim/Exception/HttpNotAllowedException.php +++ b/Slim/Exception/HttpNotAllowedException.php @@ -20,7 +20,7 @@ public function getAllowedMethods() if (is_array($allowedMethods)) { return implode(',', $allowedMethods); - } else if (is_string($allowedMethods)) { + } elseif (is_string($allowedMethods)) { return $allowedMethods; } @@ -39,4 +39,4 @@ public function setAllowedMethods($methods) $this->details['allowedMethods'] = $methods; } -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php index 912e27699..3646ebc37 100644 --- a/Slim/Exception/HttpNotFoundException.php +++ b/Slim/Exception/HttpNotFoundException.php @@ -7,4 +7,4 @@ class HttpNotFoundException extends HttpException protected $message = 'Not found.'; protected $title = '404 Not Found'; protected $description = 'The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.'; -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpNotImplementedException.php b/Slim/Exception/HttpNotImplementedException.php index 3144957da..d254da743 100644 --- a/Slim/Exception/HttpNotImplementedException.php +++ b/Slim/Exception/HttpNotImplementedException.php @@ -7,4 +7,4 @@ class HttpNotImplementedException extends HttpException protected $message = 'Not implemented.'; protected $title = '501 Not Implemented'; protected $description = 'The server either does not recognize the request method, or it lacks the ability to fulfil the request.'; -} \ No newline at end of file +} diff --git a/Slim/Exception/HttpUnauthorizedException.php b/Slim/Exception/HttpUnauthorizedException.php index 2ba2d0132..71ce87b68 100644 --- a/Slim/Exception/HttpUnauthorizedException.php +++ b/Slim/Exception/HttpUnauthorizedException.php @@ -7,4 +7,4 @@ class HttpUnauthorizedException extends HttpException protected $message = 'Unauthorized.'; protected $title = '401 Unauthorized'; protected $description = 'The request has not been applied because it lacks valid authentication credentials for the target resource.'; -} \ No newline at end of file +} diff --git a/Slim/Exception/PhpException.php b/Slim/Exception/PhpException.php index cdcfc186a..578626563 100644 --- a/Slim/Exception/PhpException.php +++ b/Slim/Exception/PhpException.php @@ -13,4 +13,4 @@ public function __construct($exception) { parent::__construct('PHP Error', 500, $exception); } -} \ No newline at end of file +} diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 12b9d0b00..a79ffd246 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -159,7 +159,7 @@ protected function resolveRenderer() $renderer )); } - } else if ($this->method === 'OPTIONS') { + } elseif ($this->method === 'OPTIONS') { $this->statusCode = 200; $this->contentType = 'text/plain'; $renderer = PlainTextErrorRenderer::class; @@ -167,16 +167,16 @@ protected function resolveRenderer() switch ($this->contentType) { case 'application/json': $renderer = JSONErrorRenderer::class; - break; + break; case 'text/xml': case 'application/xml': $renderer = XMLErrorRenderer::class; - break; + break; case 'text/html': $renderer = HTMLErrorRenderer::class; - break; + break; default: throw new Error(sprintf( diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php index ab818efa2..e66b7e62c 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -27,7 +27,6 @@ public function renderPhpExceptionOutput() $html = '

The application could not run because of the following error:

'; $html .= '

Details

'; $html .= $this->renderExceptionFragment($e); - } else { $html = '

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

'; } @@ -113,4 +112,4 @@ public function renderExceptionFragment($exception) return $html; } -} \ No newline at end of file +} diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php index b441ea1cd..a17b2f42a 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -62,4 +62,4 @@ public function renderExceptionFragment($e) 'trace' => explode("\n", $e->getTraceAsString()), ]; } -} \ No newline at end of file +} diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index 8723343d3..406730fa2 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -55,4 +55,4 @@ public function renderExceptionFragment() return $text; } -} \ No newline at end of file +} diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php index 5ad69bbab..18af6e01c 100644 --- a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php @@ -57,4 +57,4 @@ private function createCdataSection($content) { return sprintf('', str_replace(']]>', ']]]]>', $content)); } -} \ No newline at end of file +} diff --git a/Slim/Http/Request.php b/Slim/Http/Request.php index 31e0ac9bd..c90fb5482 100644 --- a/Slim/Http/Request.php +++ b/Slim/Http/Request.php @@ -337,7 +337,6 @@ protected function filterMethod($method) $e->setRequest($this); $e->setDetails(['method' => $method]); throw $e; - } return $method; } diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index da4869413..1cd26f903 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -21,4 +21,4 @@ public function testPlainTextErrorRenderDoesNotDisplayErrorDetails() $this->assertEquals('Oops..', $renderer->render()); } -} \ No newline at end of file +} From 94ab3b43510c5ad54697693bce472c7c5b8ef634 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 18:02:50 -0600 Subject: [PATCH 12/36] Change throwable from Error to RuntimeException for backwards compat --- Slim/Handlers/AbstractErrorHandler.php | 10 ++++++---- tests/Handlers/ErrorHandlerTest.php | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index a79ffd246..1a58ca729 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -10,6 +10,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; @@ -19,8 +20,8 @@ use Slim\Http\Body; use Slim\Interfaces\ErrorHandlerInterface; use Slim\Interfaces\ErrorRendererInterface; -use Error; use Exception; +use RuntimeException; use Throwable; /** @@ -145,7 +146,8 @@ protected function resolveContentType(ServerRequestInterface $request) * * @return ErrorRendererInterface * - * @throws Error + * @throws HttpBadRequestException + * @throws RuntimeException */ protected function resolveRenderer() { @@ -154,7 +156,7 @@ protected function resolveRenderer() if (!is_null($this->renderer)) { $renderer = $this->renderer; if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { - throw new Error(sprintf( + throw new RuntimeException(sprintf( 'Non compliant error renderer provided (%s). Renderer expected to be a subclass of AbstractErrorRenderer', $renderer )); @@ -179,7 +181,7 @@ protected function resolveRenderer() break; default: - throw new Error(sprintf( + throw new HttpBadRequestException(sprintf( 'Cannot render unknown content type: %s', $this->contentType )); diff --git a/tests/Handlers/ErrorHandlerTest.php b/tests/Handlers/ErrorHandlerTest.php index 00dc1fe0a..c4daeedb6 100644 --- a/tests/Handlers/ErrorHandlerTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -63,7 +63,7 @@ public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $sta } /** - * @expectedException \Error + * @expectedException \Slim\Exception\HttpBadRequestException */ public function testNotFoundContentType() { From dfa2a4792f5e65bc07bca35f4cc3d15ee74320d0 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 18:03:13 -0600 Subject: [PATCH 13/36] Remove error handler methods on container --- Slim/DefaultServicesProvider.php | 79 -------------------------------- 1 file changed, 79 deletions(-) diff --git a/Slim/DefaultServicesProvider.php b/Slim/DefaultServicesProvider.php index 007205aa4..65fb7356b 100644 --- a/Slim/DefaultServicesProvider.php +++ b/Slim/DefaultServicesProvider.php @@ -117,85 +117,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 From 0446badea85b7bc8b6743aab4d74e47e1e21c64a Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 18:19:19 -0600 Subject: [PATCH 14/36] Fix length of lines to get passing build on PHP 5.6 --- Slim/Exception/HttpInternalServerErrorException.php | 2 +- Slim/Exception/HttpNotFoundException.php | 2 +- Slim/Exception/HttpNotImplementedException.php | 2 +- Slim/Exception/HttpUnauthorizedException.php | 2 +- Slim/Handlers/AbstractErrorHandler.php | 3 ++- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Slim/Exception/HttpInternalServerErrorException.php b/Slim/Exception/HttpInternalServerErrorException.php index 1cd19f28b..44ac1ed89 100644 --- a/Slim/Exception/HttpInternalServerErrorException.php +++ b/Slim/Exception/HttpInternalServerErrorException.php @@ -6,5 +6,5 @@ class HttpInternalServerErrorException extends HttpException protected $code = 500; protected $message = 'Internal server error.'; protected $title = '500 Internal Server Error'; - protected $description = 'A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.'; + protected $description = 'Unexpected condition encountered preventing server from fulfilling request.'; } diff --git a/Slim/Exception/HttpNotFoundException.php b/Slim/Exception/HttpNotFoundException.php index 3646ebc37..614c001ce 100644 --- a/Slim/Exception/HttpNotFoundException.php +++ b/Slim/Exception/HttpNotFoundException.php @@ -6,5 +6,5 @@ class HttpNotFoundException extends HttpException protected $code = 404; protected $message = 'Not found.'; protected $title = '404 Not Found'; - protected $description = 'The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.'; + protected $description = 'The requested resource could not be found. Please verify URI and try again.'; } diff --git a/Slim/Exception/HttpNotImplementedException.php b/Slim/Exception/HttpNotImplementedException.php index d254da743..4b0e2f665 100644 --- a/Slim/Exception/HttpNotImplementedException.php +++ b/Slim/Exception/HttpNotImplementedException.php @@ -6,5 +6,5 @@ class HttpNotImplementedException extends HttpException protected $code = 501; protected $message = 'Not implemented.'; protected $title = '501 Not Implemented'; - protected $description = 'The server either does not recognize the request method, or it lacks the ability to fulfil the request.'; + protected $description = 'The server does not support the functionality required to fulfill the request.'; } diff --git a/Slim/Exception/HttpUnauthorizedException.php b/Slim/Exception/HttpUnauthorizedException.php index 71ce87b68..c0bcfc7fb 100644 --- a/Slim/Exception/HttpUnauthorizedException.php +++ b/Slim/Exception/HttpUnauthorizedException.php @@ -6,5 +6,5 @@ class HttpUnauthorizedException extends HttpException protected $code = 401; protected $message = 'Unauthorized.'; protected $title = '401 Unauthorized'; - protected $description = 'The request has not been applied because it lacks valid authentication credentials for the target resource.'; + protected $description = 'The request requires valid user authentication.'; } diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 1a58ca729..5e3e4ca99 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -157,7 +157,8 @@ protected function resolveRenderer() $renderer = $this->renderer; if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { throw new RuntimeException(sprintf( - 'Non compliant error renderer provided (%s). Renderer expected to be a subclass of AbstractErrorRenderer', + 'Non compliant error renderer provided (%s). ' . + 'Renderer expected to be a subclass of AbstractErrorRenderer', $renderer )); } From 686bb43164ff15433b0d91bd1ce4c783730a67b2 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 10 May 2017 18:40:38 -0600 Subject: [PATCH 15/36] Remove unused test for ErrorHandler --- Slim/Handlers/AbstractErrorHandler.php | 27 ++++++++++++-------------- tests/Handlers/ErrorHandlerTest.php | 16 --------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 5e3e4ca99..5b1148c3d 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -10,7 +10,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Slim\Exception\HttpBadRequestException; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; @@ -41,7 +40,7 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface 'application/json', 'application/xml', 'text/xml', - 'text/html', + 'text/html' ]; /** * @var bool @@ -50,7 +49,7 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface /** * @var string */ - protected $contentType = 'text/plain'; + protected $contentType; /** * @var string */ @@ -146,13 +145,17 @@ protected function resolveContentType(ServerRequestInterface $request) * * @return ErrorRendererInterface * - * @throws HttpBadRequestException * @throws RuntimeException */ protected function resolveRenderer() { $renderer = null; + if ($this->method === 'OPTIONS') { + $this->statusCode = 200; + $this->contentType = 'text/plain'; + } + if (!is_null($this->renderer)) { $renderer = $this->renderer; if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { @@ -162,10 +165,6 @@ protected function resolveRenderer() $renderer )); } - } elseif ($this->method === 'OPTIONS') { - $this->statusCode = 200; - $this->contentType = 'text/plain'; - $renderer = PlainTextErrorRenderer::class; } else { switch ($this->contentType) { case 'application/json': @@ -177,16 +176,14 @@ protected function resolveRenderer() $renderer = XMLErrorRenderer::class; break; - case 'text/html': - $renderer = HTMLErrorRenderer::class; + case 'text/plain': + $renderer = PlainTextErrorRenderer::class; break; default: - throw new HttpBadRequestException(sprintf( - 'Cannot render unknown content type: %s', - $this->contentType - )); - break; + case 'text/html': + $renderer = HTMLErrorRenderer::class; + break; } } diff --git a/tests/Handlers/ErrorHandlerTest.php b/tests/Handlers/ErrorHandlerTest.php index c4daeedb6..e91667bdf 100644 --- a/tests/Handlers/ErrorHandlerTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -62,22 +62,6 @@ public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $sta $this->assertEquals(0, strpos((string)$res->getBody(), $startOfBody)); } - /** - * @expectedException \Slim\Exception\HttpBadRequestException - */ - public function testNotFoundContentType() - { - $errorMock = $this->getMockBuilder(ErrorHandler::class)->setMethods(['resolveContentType'])->getMock(); - $errorMock->method('resolveContentType') - ->will($this->returnValue('unknown/type')); - - $e = new \Exception("Oops"); - - $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); - - $errorMock->__invoke($req, new Response(), $e); - } - /** * Test that an exception with a previous exception provides correct output * to the error log From c2f455a1c829e44b35a6376d1c3c21b75022494a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 01:15:56 -0600 Subject: [PATCH 16/36] Write tests for new code blocks in App, AbstractErrorHandler and AbstractErrorRenderes --- Slim/App.php | 15 +++- Slim/Exception/HttpNotAllowedException.php | 2 +- Slim/Handlers/AbstractErrorHandler.php | 10 ++- .../ErrorRenderers/JSONErrorRenderer.php | 26 +++---- .../ErrorRenderers/PlainTextErrorRenderer.php | 5 +- tests/AppTest.php | 68 +++++++++++++++++-- tests/Handlers/AbstractErrorHandlerTest.php | 27 ++++++++ tests/Handlers/AbstractErrorRendererTest.php | 54 +++++++++++++++ tests/Http/UploadedFilesTest.php | 23 +++---- tests/Mocks/MockCustomException.php | 18 +++++ tests/Mocks/MockErrorHandler.php | 18 +++++ 11 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 tests/Mocks/MockCustomException.php create mode 100644 tests/Mocks/MockErrorHandler.php diff --git a/Slim/App.php b/Slim/App.php index eb25179e7..a3bbe8d2d 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -343,6 +343,18 @@ public function getNotAllowedHandler() */ public function setErrorHandler($type, $handler) { + if (!(is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) + && !is_callable($handler) + ) { + throw new RuntimeException(sprintf( + 'Invalid parameter %s of type %s passed to setErrorHandler method. ' . + 'Please provide a valid subclass name of AbstractErrorHandler ' . + 'or an invokable callback function.', + $handler, + gettype($handler) + )); + } + $handlers = $this->getSetting('errorHandlers', []); $handlers[$type] = $handler; $this->addSetting('errorHandlers', $handlers); @@ -367,9 +379,6 @@ public function getErrorHandler($type = null) return $handler; } elseif (is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) { return new $handler($displayErrorDetails); - } elseif ($handler instanceof AbstractErrorHandler) { - $handler->setDisplayErrorDetails($displayErrorDetails); - return $handler; } } diff --git a/Slim/Exception/HttpNotAllowedException.php b/Slim/Exception/HttpNotAllowedException.php index 9c942c3d6..caf81d34a 100644 --- a/Slim/Exception/HttpNotAllowedException.php +++ b/Slim/Exception/HttpNotAllowedException.php @@ -19,7 +19,7 @@ public function getAllowedMethods() $allowedMethods = $details['allowedMethods']; if (is_array($allowedMethods)) { - return implode(',', $allowedMethods); + return implode(', ', $allowedMethods); } elseif (is_string($allowedMethods)) { return $allowedMethods; } diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 5b1148c3d..e21eef7f9 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -99,9 +99,9 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $this->response = $response; $this->exception = $exception; $this->method = $request->getMethod(); + $this->statusCode = $this->resolveStatusCode(); $this->contentType = $this->resolveContentType($request); $this->renderer = $this->resolveRenderer(); - $this->statusCode = $this->resolveStatusCode(); if (!$this->displayErrorDetails) { $this->writeToErrorLog($exception); @@ -212,6 +212,14 @@ public function setDisplayErrorDetails($displayErrorDetails) $this->displayErrorDetails = $displayErrorDetails; } + /** + * @return bool + */ + public function getDisplayErrorDetails() + { + return $this->displayErrorDetails; + } + /** * @return ResponseInterface */ diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php index a17b2f42a..42a54bc0a 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php @@ -19,23 +19,24 @@ class JSONErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { - $e = $this->exception; - $error = ['message' => 'Slim Application Error']; - - if ($this->displayErrorDetails) { - $error['exception'] = []; - do { - $error['exception'][] = $this->renderExceptionFragment($e); - } while ($e = $e->getPrevious()); - } - - return json_encode($error, JSON_PRETTY_PRINT); + $message = 'Slim Application Error'; + return $this->renderExceptionBody($message); } public function renderGenericExceptionOutput() + { + $message = $this->exception->getMessage(); + return $this->renderExceptionBody($message); + } + + /** + * @param $message + * @return string + */ + public function renderExceptionBody($message) { $e = $this->exception; - $error = ['message' => $e->getMessage()]; + $error = ['message' => $message]; if ($this->displayErrorDetails) { $error['exception'] = []; @@ -59,7 +60,6 @@ public function renderExceptionFragment($e) 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), - 'trace' => explode("\n", $e->getTraceAsString()), ]; } } diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index 406730fa2..43f991dd1 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -17,8 +17,7 @@ class PlainTextErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { - $e = $this->exception; - return $this->renderExceptionFragment($e); + return $this->renderExceptionFragment(); } public function renderGenericExceptionOutput() @@ -26,7 +25,7 @@ public function renderGenericExceptionOutput() $e = $this->exception; if (!$this->displayErrorDetails) { - return $this->renderExceptionFragment($e); + return $this->renderExceptionFragment(); } return $e->getMessage(); diff --git a/tests/AppTest.php b/tests/AppTest.php index ed9b530e0..903fce729 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -14,6 +14,8 @@ use Slim\App; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\HttpNotFoundException; +use Slim\Exception\PhpException; +use Slim\Handlers\ErrorHandler; use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; use Slim\Handlers\Strategies\RequestResponseArgs; use Slim\Http\Body; @@ -25,6 +27,8 @@ use Slim\Http\Uri; use Slim\Router; use Slim\Tests\Mocks\MockAction; +use Slim\Tests\Mocks\MockCustomException; +use Slim\Tests\Mocks\MockErrorHandler; class AppTest extends TestCase { @@ -42,7 +46,6 @@ public static function tearDownAfterClass() /******************************************************************************** * Settings management methods *******************************************************************************/ - public function testHasSetting() { $app = new App(); @@ -98,7 +101,6 @@ public function testGetErrorHandler() /******************************************************************************** * Router proxy methods *******************************************************************************/ - public function testGetRoute() { $path = '/foo'; @@ -789,7 +791,6 @@ public function testEmptyGroupWithEmptyNestedGroupAndSegmentRouteWithoutLeadingS /******************************************************************************** * Middleware *******************************************************************************/ - public function testBottomMiddlewareIsApp() { $app = new App(); @@ -995,11 +996,69 @@ public function testAddMiddlewareOnRouteAndOnTwoRouteGroup() $this->assertEquals('In1In2In3CenterOut3Out2Out1', (string)$res->getBody()); } + /******************************************************************************** + * Error Handlers + *******************************************************************************/ + public function testSetErrorHandler() + { + $app = $this->appFactory(); + $app->get('/foo', function ($req, $res, $args) { + return $res; + }); + $app->add(function () { + throw new HttpNotFoundException; + }); + $exception = HttpNotFoundException::class; + $handler = function ($req, $res) { + return $res->withJson(['Oops..']); + }; + $app->setErrorHandler($exception, $handler); + $res = $app->run(true); + $expectedOutput = json_encode(['Oops..']); + + $this->assertEquals($res->getBody(), $expectedOutput); + } + + /** + * @expectedException \RuntimeException + */ + public function testSetErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() + { + $app = new App(); + $app->setErrorHandler('RandomExceptionClassName', 'NonExistantClassname'); + } + + public function testGetErrorHandlerInstantiatesHandlerFromString() + { + $app = new App(); + $exception = HttpNotFoundException::class; + $app->setErrorHandler($exception, MockErrorHandler::class); + $handler = $app->getErrorHandler($exception); + + $this->assertInstanceOf(MockErrorHandler::class, $handler); + } + + public function testGetErrorHandlerWillReturnDefaultErrorHandlerForPhpExceptions() + { + $app = new App(); + $exception = PhpException::class; + $handler = $app->getErrorHandler($exception); + + $this->assertInstanceOf(ErrorHandler::class, $handler); + } + + public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExceptions() + { + $app = new App(); + $exception = MockCustomException::class; + $handler = $app->getErrorHandler($exception); + + $this->assertInstanceOf(ErrorHandler::class, $handler); + } /******************************************************************************** * Runner *******************************************************************************/ - public function testInvokeReturnMethodNotAllowed() { $app = new App(); @@ -1480,7 +1539,6 @@ public function testRun() $this->assertEquals('bar', (string)$resOut); } - public function testRespond() { $app = new App(); diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php index 8f0e54132..a296dade6 100644 --- a/tests/Handlers/AbstractErrorHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -9,7 +9,10 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; +use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\AbstractErrorHandler; +use Slim\Handlers\ErrorHandler; +use Slim\Http\Response; class AbstractErrorHandlerTest extends TestCase { @@ -68,4 +71,28 @@ public function testAcceptableMediaTypeIsNotFirstInList() $this->assertEquals('text/html', $return); } + + public function testOptions() + { + $handler = new ErrorHandler; + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods(['POST', 'PUT']); + /** @var Response $res */ + $res = $handler->__invoke($this->getRequest('OPTIONS'), new Response(), $exception); + $this->assertSame(200, $res->getStatusCode()); + $this->assertTrue($res->hasHeader('Allow')); + $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); + } + + /** + * @param string $method + * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Request + */ + protected function getRequest($method, $contentType = 'text/html') + { + $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); + $req->expects($this->once())->method('getMethod')->will($this->returnValue($method)); + $req->expects($this->any())->method('getHeaderLine')->will($this->returnValue($contentType)); + return $req; + } } diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 1cd26f903..558017697 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -9,8 +9,12 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; +use Slim\Exception\PhpException; +use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; +use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; use Exception; +use RuntimeException; class AbstractErrorRendererTest extends TestCase { @@ -21,4 +25,54 @@ public function testPlainTextErrorRenderDoesNotDisplayErrorDetails() $this->assertEquals('Oops..', $renderer->render()); } + + public function testHTMLErrorRendererOutputIsDifferentForPhpExceptions() + { + $exception = new Exception('Oops..'); + $genericRenderer = new HTMLErrorRenderer($exception); + $genericOutput = $genericRenderer->render(); + + $phpException = new PhpException(new RuntimeException('Oops..')); + $phpExceptionRenderer = new HTMLErrorRenderer($phpException); + $phpExceptionOutput = $phpExceptionRenderer->render(); + + $this->assertNotEquals($genericOutput, $phpExceptionOutput); + } + + public function testJSONErrorRendererDisplaysErrorDetails() + { + $exception = new Exception('Oops..'); + $renderer = new JSONErrorRenderer($exception, true); + $fragment = $renderer->renderExceptionFragment($exception); + $output = json_encode(json_decode($renderer->render())); + $expectedString = json_encode(['message' => 'Oops..', 'exception' => [$fragment]]); + + $this->assertEquals($output, $expectedString); + } + + public function testJSONErrorRendererDoesNotDisplayErrorDetails() + { + $exception = new Exception('Oops..'); + $renderer = new JSONErrorRenderer($exception); + $output = json_encode(json_decode($renderer->render())); + + $this->assertEquals($output, json_encode(['message' => 'Oops..'])); + } + + public function testJSONErrorRendererDisplaysPreviousError() + { + $previousException = new Exception('Oh no!'); + $exception = new Exception('Oops..', 0, $previousException); + $renderer = new JSONErrorRenderer($exception, true); + $output = json_encode(json_decode($renderer->render())); + + $fragments = [ + $renderer->renderExceptionFragment($exception), + $renderer->renderExceptionFragment($previousException), + ]; + + $expectedString = json_encode(['message' => 'Oops..', 'exception' => $fragments]); + + $this->assertEquals($output, $expectedString); + } } diff --git a/tests/Http/UploadedFilesTest.php b/tests/Http/UploadedFilesTest.php index e45d77c43..7c2035960 100644 --- a/tests/Http/UploadedFilesTest.php +++ b/tests/Http/UploadedFilesTest.php @@ -153,11 +153,11 @@ public function testMoveToNotWritable(UploadedFile $uploadedFile) $uploadedFile->moveTo($path); } - /* + /** * @depends testConstructor * @param UploadedFile $uploadedFile * @return UploadedFile - * + */ public function testMoveTo(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); @@ -170,13 +170,12 @@ public function testMoveTo(UploadedFile $uploadedFile) return $uploadedFile; } - */ - /* + /** * @depends testMoveTo * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - * + */ public function testMoveToCannotBeDoneTwice(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); @@ -187,43 +186,39 @@ public function testMoveToCannotBeDoneTwice(UploadedFile $uploadedFile) $uploadedFile->moveTo($path); } - */ - /* + /** * This test must run after testMoveTo * * @depends testConstructor * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - * + */ public function testMoveToAgain(UploadedFile $uploadedFile) { $tempName = uniqid('file-'); $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $tempName; $uploadedFile->moveTo($path); } - */ - /* + + /** * This test must run after testMoveTo * * @depends testConstructor * @param UploadedFile $uploadedFile * @expectedException \RuntimeException - * + */ public function testMovedStream($uploadedFile) { $uploadedFile->getStream(); } - */ - /* public function testMoveToStream() { $uploadedFile = $this->generateNewTmpFile(); $uploadedFile->moveTo('php://temp'); } - */ public function providerCreateFromEnvironment() { diff --git a/tests/Mocks/MockCustomException.php b/tests/Mocks/MockCustomException.php new file mode 100644 index 000000000..307215093 --- /dev/null +++ b/tests/Mocks/MockCustomException.php @@ -0,0 +1,18 @@ + Date: Thu, 11 May 2017 01:51:28 -0600 Subject: [PATCH 17/36] Add test for XMLErrorRenderer to increase test coverage --- Slim/Handlers/AbstractErrorHandler.php | 16 ---------------- .../Handlers/ErrorRenderers/XMLErrorRenderer.php | 1 - tests/Handlers/AbstractErrorRendererTest.php | 13 +++++++++++++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index e21eef7f9..f45ba738d 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -204,22 +204,6 @@ protected function resolveStatusCode() return $statusCode; } - /** - * @param bool $displayErrorDetails - */ - public function setDisplayErrorDetails($displayErrorDetails) - { - $this->displayErrorDetails = $displayErrorDetails; - } - - /** - * @return bool - */ - public function getDisplayErrorDetails() - { - return $this->displayErrorDetails; - } - /** * @return ResponseInterface */ diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php index 18af6e01c..674f9b72e 100644 --- a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php @@ -30,7 +30,6 @@ public function renderPhpExceptionOutput() $xml .= " " . $this->createCdataSection($e->getMessage()) . "\n"; $xml .= " " . $e->getFile() . "\n"; $xml .= " " . $e->getLine() . "\n"; - $xml .= " " . $this->createCdataSection($e->getTraceAsString()) . "\n"; $xml .= " \n"; } while ($e = $e->getPrevious()); } diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 558017697..67cedd3b5 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -13,6 +13,7 @@ use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; +use Slim\Handlers\ErrorRenderers\XMLErrorRenderer; use Exception; use RuntimeException; @@ -75,4 +76,16 @@ public function testJSONErrorRendererDisplaysPreviousError() $this->assertEquals($output, $expectedString); } + + public function testXMLErrorRendererDisplaysErrorDetails() + { + $previousException = new RuntimeException('Oops..'); + $exception = new PhpException($previousException); + $renderer = new XMLErrorRenderer($exception, true); + $output = simplexml_load_string($renderer->render()); + + $this->assertEquals($output->message[0], 'Slim Application Error'); + $this->assertEquals((string)$output->exception[0]->type, 'Slim\Exception\PhpException'); + $this->assertEquals((string)$output->exception[1]->type, 'RuntimeException'); + } } From 44e5a5182eb1c12f6c0a70aae04803175495f6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 02:13:01 -0600 Subject: [PATCH 18/36] Write tests to increase test coverage for HTMLErrorRender --- .../ErrorRenderers/HTMLErrorRenderer.php | 4 ---- tests/Handlers/AbstractErrorRendererTest.php | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php index e66b7e62c..0baa0661b 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php @@ -83,10 +83,6 @@ public function renderGenericExceptionOutput() */ public function renderExceptionFragment($exception) { - if (!$exception instanceof Exception && !$exception instanceof Error) { - throw new RuntimeException('Unexpected type. Expected Exception or Error.'); - } - $html = sprintf('
Type: %s
', get_class($exception)); if (($code = $exception->getCode())) { diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 67cedd3b5..3a8430563 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -40,6 +40,28 @@ public function testHTMLErrorRendererOutputIsDifferentForPhpExceptions() $this->assertNotEquals($genericOutput, $phpExceptionOutput); } + public function testHTMLErrorRendererDisplaysErrorDetails() + { + $exception = new PhpException(new RuntimeException('Oops..')); + $renderer = new HTMLErrorRenderer($exception, true); + $output = $renderer->render(); + + $this->assertRegExp('/.*The application could not run because of the following error:.*/', $output); + } + + public function testHTMLErrorRendererRenderFragmentMethod() + { + $exception = new Exception('Oops..', 500); + $renderer = new HTMLErrorRenderer($exception, true); + $output = $renderer->renderExceptionFragment($exception); + + $this->assertRegExp('/.*Type:*/', $output); + $this->assertRegExp('/.*Code:*/', $output); + $this->assertRegExp('/.*Message*/', $output); + $this->assertRegExp('/.*File*/', $output); + $this->assertRegExp('/.*Line*/', $output); + } + public function testJSONErrorRendererDisplaysErrorDetails() { $exception = new Exception('Oops..'); From 3454b3a8f07ce906b2d81fb804931f8e3dd09c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=83=3F=C3=82=C2=A9rub=C3=83=3F=C3=82=C2=A9?= Date: Thu, 11 May 2017 02:30:06 -0600 Subject: [PATCH 19/36] Add unit test for HttpException to increase test coverage --- tests/Exception/HttpExceptionTest.php | 85 +++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/Exception/HttpExceptionTest.php diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php new file mode 100644 index 000000000..d845e3739 --- /dev/null +++ b/tests/Exception/HttpExceptionTest.php @@ -0,0 +1,85 @@ +assertEquals($exceptionWithMessage->getMessage(), 'Oops..'); + + $details = ['allowedMethods' => 'POST']; + $exceptionWithDetails = new HttpNotAllowedException($details); + $this->assertEquals($exceptionWithDetails->getDetails(), $details); + } + + public function testHttpExceptionRequestReponseGetterSetters() + { + // Prepare request and response objects + $env = Environment::mock([ + 'SCRIPT_NAME' => '/index.php', + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + ]); + $uri = Uri::createFromEnvironment($env); + $headers = Headers::createFromEnvironment($env); + $cookies = []; + $serverParams = $env->all(); + $body = new RequestBody(); + $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body); + $response = new Response(); + $exception = new HttpNotFoundException; + $exception->setRequest($request); + $exception->setResponse($response); + + $this->assertInstanceOf(Request::class, $exception->getRequest()); + $this->assertInstanceOf(Response::class, $exception->getResponse()); + } + + public function testHttpExceptionAttributeGettersSetters() + { + $exception = new HttpNotFoundException; + $exception->setTitle('Title'); + $exception->setDescription('Description'); + $exception->setDetails(['Details']); + + $this->assertEquals('Title', $exception->getTitle()); + $this->assertEquals('Description', $exception->getDescription()); + $this->assertEquals(['Details'], $exception->getDetails()); + } + + public function testHttpExceptionRecoverableGetterSetter() + { + $exception = new HttpNotFoundException; + $exception->notRecoverable(); + + $this->assertEquals(false, $exception->isRecoverable()); + } + + public function testHttpNotAllowedExceptionGetAllowedMethods() + { + $exception = new HttpNotAllowedException; + $exception->setAllowedMethods('GET'); + $this->assertEquals('GET', $exception->getAllowedMethods()); + + $exception = new HttpNotAllowedException; + $this->assertEquals('', $exception->getAllowedMethods()); + } +} From 52140aa939cb67e336dd7b6dab458b2c8cd181eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 02:44:59 -0600 Subject: [PATCH 20/36] Update HTMLErrorRenderer test to increase test coverage --- tests/Handlers/AbstractErrorRendererTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 3a8430563..4ea94e394 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -38,6 +38,7 @@ public function testHTMLErrorRendererOutputIsDifferentForPhpExceptions() $phpExceptionOutput = $phpExceptionRenderer->render(); $this->assertNotEquals($genericOutput, $phpExceptionOutput); + $this->assertRegExp('/.*Slim Application Error.*/', $phpExceptionOutput); } public function testHTMLErrorRendererDisplaysErrorDetails() From 4e5fe99f290ab227b3026ba484a5a71f48e76ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 02:55:34 -0600 Subject: [PATCH 21/36] Add unit test for JSONErrorRenderer to increase test coverage and refactor error renderers names --- Slim/Handlers/AbstractErrorHandler.php | 12 +++---- ...rrorRenderer.php => HtmlErrorRenderer.php} | 2 +- ...rrorRenderer.php => JsonErrorRenderer.php} | 2 +- ...ErrorRenderer.php => XmlErrorRenderer.php} | 2 +- tests/AppTest.php | 4 +-- tests/Handlers/AbstractErrorRendererTest.php | 32 ++++++++++++------- 6 files changed, 31 insertions(+), 23 deletions(-) rename Slim/Handlers/ErrorRenderers/{HTMLErrorRenderer.php => HtmlErrorRenderer.php} (98%) rename Slim/Handlers/ErrorRenderers/{JSONErrorRenderer.php => JsonErrorRenderer.php} (96%) rename Slim/Handlers/ErrorRenderers/{XMLErrorRenderer.php => XmlErrorRenderer.php} (96%) diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index f45ba738d..d33c3471a 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -13,9 +13,9 @@ use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; -use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; -use Slim\Handlers\ErrorRenderers\XMLErrorRenderer; -use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; +use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; +use Slim\Handlers\ErrorRenderers\XmlErrorRenderer; +use Slim\Handlers\ErrorRenderers\JsonErrorRenderer; use Slim\Http\Body; use Slim\Interfaces\ErrorHandlerInterface; use Slim\Interfaces\ErrorRendererInterface; @@ -168,12 +168,12 @@ protected function resolveRenderer() } else { switch ($this->contentType) { case 'application/json': - $renderer = JSONErrorRenderer::class; + $renderer = JsonErrorRenderer::class; break; case 'text/xml': case 'application/xml': - $renderer = XMLErrorRenderer::class; + $renderer = XmlErrorRenderer::class; break; case 'text/plain': @@ -182,7 +182,7 @@ protected function resolveRenderer() default: case 'text/html': - $renderer = HTMLErrorRenderer::class; + $renderer = HtmlErrorRenderer::class; break; } } diff --git a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php similarity index 98% rename from Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php rename to Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php index 0baa0661b..e9dcf1ed6 100644 --- a/Slim/Handlers/ErrorRenderers/HTMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php @@ -16,7 +16,7 @@ /** * Default Slim application HTML Error Renderer */ -class HTMLErrorRenderer extends AbstractErrorRenderer +class HtmlErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { diff --git a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php similarity index 96% rename from Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php rename to Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php index 42a54bc0a..0af1904bd 100644 --- a/Slim/Handlers/ErrorRenderers/JSONErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -15,7 +15,7 @@ /** * Default Slim application JSON Error Renderer */ -class JSONErrorRenderer extends AbstractErrorRenderer +class JsonErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { diff --git a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php similarity index 96% rename from Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php rename to Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php index 674f9b72e..6a05c928b 100644 --- a/Slim/Handlers/ErrorRenderers/XMLErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php @@ -13,7 +13,7 @@ /** * Default Slim application XML Error Renderer */ -class XMLErrorRenderer extends AbstractErrorRenderer +class XmlErrorRenderer extends AbstractErrorRenderer { /** * @return string diff --git a/tests/AppTest.php b/tests/AppTest.php index 903fce729..29d131eda 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -16,7 +16,7 @@ use Slim\Exception\HttpNotFoundException; use Slim\Exception\PhpException; use Slim\Handlers\ErrorHandler; -use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; +use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; use Slim\Handlers\Strategies\RequestResponseArgs; use Slim\Http\Body; use Slim\Http\Environment; @@ -1085,7 +1085,7 @@ public function testInvokeReturnMethodNotAllowed() // Create Html Renderer and Assert Output $exception = new HttpNotAllowedException; $exception->setAllowedMethods(['GET']); - $renderer = new HTMLErrorRenderer($exception, false); + $renderer = new HtmlErrorRenderer($exception, false); // Invoke app $resOut = $app($req, $res); diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 4ea94e394..96442422b 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -10,10 +10,10 @@ use PHPUnit\Framework\TestCase; use Slim\Exception\PhpException; -use Slim\Handlers\ErrorRenderers\HTMLErrorRenderer; -use Slim\Handlers\ErrorRenderers\JSONErrorRenderer; +use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; +use Slim\Handlers\ErrorRenderers\JsonErrorRenderer; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; -use Slim\Handlers\ErrorRenderers\XMLErrorRenderer; +use Slim\Handlers\ErrorRenderers\XmlErrorRenderer; use Exception; use RuntimeException; @@ -27,14 +27,14 @@ public function testPlainTextErrorRenderDoesNotDisplayErrorDetails() $this->assertEquals('Oops..', $renderer->render()); } - public function testHTMLErrorRendererOutputIsDifferentForPhpExceptions() + public function testHTMLErrorRendererOutputForPhpExceptions() { $exception = new Exception('Oops..'); - $genericRenderer = new HTMLErrorRenderer($exception); + $genericRenderer = new HtmlErrorRenderer($exception); $genericOutput = $genericRenderer->render(); $phpException = new PhpException(new RuntimeException('Oops..')); - $phpExceptionRenderer = new HTMLErrorRenderer($phpException); + $phpExceptionRenderer = new HtmlErrorRenderer($phpException); $phpExceptionOutput = $phpExceptionRenderer->render(); $this->assertNotEquals($genericOutput, $phpExceptionOutput); @@ -44,7 +44,7 @@ public function testHTMLErrorRendererOutputIsDifferentForPhpExceptions() public function testHTMLErrorRendererDisplaysErrorDetails() { $exception = new PhpException(new RuntimeException('Oops..')); - $renderer = new HTMLErrorRenderer($exception, true); + $renderer = new HtmlErrorRenderer($exception, true); $output = $renderer->render(); $this->assertRegExp('/.*The application could not run because of the following error:.*/', $output); @@ -53,7 +53,7 @@ public function testHTMLErrorRendererDisplaysErrorDetails() public function testHTMLErrorRendererRenderFragmentMethod() { $exception = new Exception('Oops..', 500); - $renderer = new HTMLErrorRenderer($exception, true); + $renderer = new HtmlErrorRenderer($exception, true); $output = $renderer->renderExceptionFragment($exception); $this->assertRegExp('/.*Type:*/', $output); @@ -63,10 +63,18 @@ public function testHTMLErrorRendererRenderFragmentMethod() $this->assertRegExp('/.*Line*/', $output); } + public function testJSONErrorRendererPhpOutputForPhpExceptions() + { + $exception = new PhpException(new RuntimeException('Oops..')); + $renderer = new JsonErrorRenderer($exception); + $output = $renderer->renderPhpExceptionOutput(); + $this->assertRegExp('/.*Slim Application Error.*/', $output); + } + public function testJSONErrorRendererDisplaysErrorDetails() { $exception = new Exception('Oops..'); - $renderer = new JSONErrorRenderer($exception, true); + $renderer = new JsonErrorRenderer($exception, true); $fragment = $renderer->renderExceptionFragment($exception); $output = json_encode(json_decode($renderer->render())); $expectedString = json_encode(['message' => 'Oops..', 'exception' => [$fragment]]); @@ -77,7 +85,7 @@ public function testJSONErrorRendererDisplaysErrorDetails() public function testJSONErrorRendererDoesNotDisplayErrorDetails() { $exception = new Exception('Oops..'); - $renderer = new JSONErrorRenderer($exception); + $renderer = new JsonErrorRenderer($exception); $output = json_encode(json_decode($renderer->render())); $this->assertEquals($output, json_encode(['message' => 'Oops..'])); @@ -87,7 +95,7 @@ public function testJSONErrorRendererDisplaysPreviousError() { $previousException = new Exception('Oh no!'); $exception = new Exception('Oops..', 0, $previousException); - $renderer = new JSONErrorRenderer($exception, true); + $renderer = new JsonErrorRenderer($exception, true); $output = json_encode(json_decode($renderer->render())); $fragments = [ @@ -104,7 +112,7 @@ public function testXMLErrorRendererDisplaysErrorDetails() { $previousException = new RuntimeException('Oops..'); $exception = new PhpException($previousException); - $renderer = new XMLErrorRenderer($exception, true); + $renderer = new XmlErrorRenderer($exception, true); $output = simplexml_load_string($renderer->render()); $this->assertEquals($output->message[0], 'Slim Application Error'); From 145335a904bb546e6bfe04ce91a666e25575054e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 03:01:22 -0600 Subject: [PATCH 22/36] Add unit test for App error handler shortcuts to increase test coverage --- tests/AppTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/AppTest.php b/tests/AppTest.php index 29d131eda..41d391605 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1028,6 +1028,19 @@ public function testSetErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() $app->setErrorHandler('RandomExceptionClassName', 'NonExistantClassname'); } + public function testErrorHandlerShortcuts() + { + $app = new App(); + $handler = MockErrorHandler::class; + $app->setNotAllowedHandler($handler); + $app->setNotFoundHandler($handler); + $app->setPhpErrorHandler($handler); + + $this->assertInstanceOf($handler, $app->getErrorHandler(HttpNotAllowedException::class)); + $this->assertInstanceOf($handler, $app->getErrorHandler(HttpNotFoundException::class)); + $this->assertInstanceOf($handler, $app->getErrorHandler(PhpException::class)); + } + public function testGetErrorHandlerInstantiatesHandlerFromString() { $app = new App(); From db17f1feba7ca99071a3944e3b311c1e8af6db9c Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Thu, 11 May 2017 11:28:05 -0600 Subject: [PATCH 23/36] Refactor AbstractErrorHandler writeToLog logic and PlainTextRenderer output --- Slim/Exception/PhpException.php | 3 +- Slim/Handlers/AbstractErrorHandler.php | 33 +++++++------------ Slim/Handlers/ErrorHandler.php | 9 ++--- .../ErrorRenderers/JsonErrorRenderer.php | 10 +++--- .../ErrorRenderers/PlainTextErrorRenderer.php | 30 +++++++++++++---- tests/Handlers/AbstractErrorRendererTest.php | 8 ++--- tests/Handlers/ErrorHandlerTest.php | 9 ++--- 7 files changed, 53 insertions(+), 49 deletions(-) diff --git a/Slim/Exception/PhpException.php b/Slim/Exception/PhpException.php index 578626563..e63497957 100644 --- a/Slim/Exception/PhpException.php +++ b/Slim/Exception/PhpException.php @@ -2,12 +2,13 @@ namespace Slim\Exception; use Exception; +use Throwable; class PhpException extends Exception { /** * PhpException constructor. - * @param $exception + * @param Exception|Throwable $exception */ public function __construct($exception) { diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index d33c3471a..b87d3f8c8 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -12,6 +12,7 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; +use Slim\Exception\PhpException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; use Slim\Handlers\ErrorRenderers\XmlErrorRenderer; @@ -21,7 +22,6 @@ use Slim\Interfaces\ErrorRendererInterface; use Exception; use RuntimeException; -use Throwable; /** * Default Slim application error handler @@ -89,7 +89,7 @@ public function __construct($displayErrorDetails = false) * * @param ServerRequestInterface $request The most recent Request object * @param ResponseInterface $response The most recent Response object - * @param Exception $exception The caught Exception object + * @param Exception $exception The caught Exception object * * @return ResponseInterface */ @@ -104,7 +104,7 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res $this->renderer = $this->resolveRenderer(); if (!$this->displayErrorDetails) { - $this->writeToErrorLog($exception); + $this->writeToErrorLog(); } return $this->respond(); @@ -228,34 +228,23 @@ public function respond() /** * Write to the error log if displayErrorDetails is false * - * @param Exception|Throwable $throwable - * * @return void */ - protected function writeToErrorLog($throwable) + protected function writeToErrorLog() { - if ($this->displayErrorDetails) { - return; - } - $renderer = new PlainTextErrorRenderer($throwable); - $message = 'Slim Application Error:' . PHP_EOL; - $message .= $renderer->render(); - while ($throwable = $throwable->getPrevious()) { - $renderer = new PlainTextErrorRenderer($throwable); - $message .= PHP_EOL . 'Previous Error:' . PHP_EOL; - $message .= $renderer->render(); - } - $message .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; - $this->logError($message); + $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 $message + * @param string $error */ - protected function logError($message) + protected function logError($error) { - error_log($message); + error_log($error); } } diff --git a/Slim/Handlers/ErrorHandler.php b/Slim/Handlers/ErrorHandler.php index e4d96fdb3..47836b216 100644 --- a/Slim/Handlers/ErrorHandler.php +++ b/Slim/Handlers/ErrorHandler.php @@ -8,16 +8,11 @@ */ namespace Slim\Handlers; -use Psr\Http\Message\ResponseInterface; -use Slim\Exception\HttpNotAllowedException; -use Slim\Exception\PhpException; -use Slim\Http\Body; - /** * Default Slim application error handler * - * It outputs the error message and diagnostic information in either JSON, XML, - * or HTML based on the Accept header. + * It outputs the error message and diagnostic information in either + * JSON, XML or HTML based on the Accept header. */ class ErrorHandler extends AbstractErrorHandler { diff --git a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php index 0af1904bd..33905da3b 100644 --- a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -20,20 +20,20 @@ class JsonErrorRenderer extends AbstractErrorRenderer public function renderPhpExceptionOutput() { $message = 'Slim Application Error'; - return $this->renderExceptionBody($message); + return $this->formatExceptionPayload($message); } public function renderGenericExceptionOutput() { $message = $this->exception->getMessage(); - return $this->renderExceptionBody($message); + return $this->formatExceptionPayload($message); } /** * @param $message * @return string */ - public function renderExceptionBody($message) + public function formatExceptionPayload($message) { $e = $this->exception; $error = ['message' => $message]; @@ -41,7 +41,7 @@ public function renderExceptionBody($message) if ($this->displayErrorDetails) { $error['exception'] = []; do { - $error['exception'][] = $this->renderExceptionFragment($e); + $error['exception'][] = $this->formatExceptionFragment($e); } while ($e = $e->getPrevious()); } @@ -52,7 +52,7 @@ public function renderExceptionBody($message) * @param Exception|Throwable $e * @return array */ - public function renderExceptionFragment($e) + public function formatExceptionFragment($e) { return [ 'type' => get_class($e), diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index 43f991dd1..b1ff3f1e1 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -9,6 +9,8 @@ namespace Slim\Handlers\ErrorRenderers; use Slim\Handlers\AbstractErrorRenderer; +use Exception; +use Throwable; /** * Default Slim application Plain Text Error Renderer @@ -17,23 +19,39 @@ class PlainTextErrorRenderer extends AbstractErrorRenderer { public function renderPhpExceptionOutput() { - return $this->renderExceptionFragment(); + return $this->formatExceptionBody(); } public function renderGenericExceptionOutput() + { + if ($this->displayErrorDetails) { + return $this->formatExceptionBody(); + } + + return $this->exception->getMessage(); + } + + public function formatExceptionBody() { $e = $this->exception; - if (!$this->displayErrorDetails) { - return $this->renderExceptionFragment(); + $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 $e->getMessage(); + return $text; } - public function renderExceptionFragment() + /** + * @param Exception|Throwable $e + * @return string + */ + public function formatExceptionFragment($e) { - $e = $this->exception; $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); if ($code = $e->getCode()) { diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index 96442422b..ce65220e3 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -22,7 +22,7 @@ class AbstractErrorRendererTest extends TestCase public function testPlainTextErrorRenderDoesNotDisplayErrorDetails() { $exception = new Exception('Oops..'); - $renderer = new PlainTextErrorRenderer($exception, true); + $renderer = new PlainTextErrorRenderer($exception); $this->assertEquals('Oops..', $renderer->render()); } @@ -75,7 +75,7 @@ public function testJSONErrorRendererDisplaysErrorDetails() { $exception = new Exception('Oops..'); $renderer = new JsonErrorRenderer($exception, true); - $fragment = $renderer->renderExceptionFragment($exception); + $fragment = $renderer->formatExceptionFragment($exception); $output = json_encode(json_decode($renderer->render())); $expectedString = json_encode(['message' => 'Oops..', 'exception' => [$fragment]]); @@ -99,8 +99,8 @@ public function testJSONErrorRendererDisplaysPreviousError() $output = json_encode(json_decode($renderer->render())); $fragments = [ - $renderer->renderExceptionFragment($exception), - $renderer->renderExceptionFragment($previousException), + $renderer->formatExceptionFragment($exception), + $renderer->formatExceptionFragment($previousException), ]; $expectedString = json_encode(['message' => 'Oops..', 'exception' => $fragments]); diff --git a/tests/Handlers/ErrorHandlerTest.php b/tests/Handlers/ErrorHandlerTest.php index e91667bdf..b165e8a81 100644 --- a/tests/Handlers/ErrorHandlerTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase; use Slim\Handlers\ErrorHandler; use Slim\Http\Response; +use Exception; class ErrorTest extends TestCase { @@ -34,7 +35,7 @@ public function errorProvider() public function testErrorHandler($acceptHeader, $contentType, $startOfBody) { $errorHandler = new ErrorHandler(); - $e = new \Exception("Oops", 1, new \Exception('Previous oops')); + $e = new Exception("Oops", 1, new Exception('Previous oops')); /** @var Response $res */ $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); @@ -52,7 +53,7 @@ public function testErrorHandler($acceptHeader, $contentType, $startOfBody) public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $startOfBody) { $errorHandler = new ErrorHandler(true); - $e = new \Exception('Oops', 1, new \Exception('Opps before')); + $e = new Exception('Oops', 1, new Exception('Oops before')); /** @var Response $res */ $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); @@ -76,8 +77,8 @@ public function testPreviousException() ) ); - $first = new \Exception("First Oops"); - $second = new \Exception("Second Oops", 0, $first); + $first = new Exception("First Oops"); + $second = new Exception("Second Oops", 0, $first); $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second); } From 025be5df780befc7daa091db329ebd12b6fc6fe1 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Thu, 11 May 2017 14:35:52 -0600 Subject: [PATCH 24/36] Add setDefaultErrorHandler method to App and supporting unit tests --- Slim/App.php | 176 ++++++++++++-------- Slim/Handlers/AbstractErrorHandler.php | 39 ++--- Slim/Interfaces/ErrorHandlerInterface.php | 13 +- Slim/Interfaces/ErrorRendererInterface.php | 1 + tests/AppTest.php | 52 ++++-- tests/Handlers/AbstractErrorHandlerTest.php | 8 +- 6 files changed, 180 insertions(+), 109 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index a3bbe8d2d..3b78f66cc 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -67,6 +67,7 @@ class App 'displayErrorDetails' => false, 'addContentLengthHeader' => true, 'routerCacheFile' => false, + 'defaultErrorHandler' => null, 'errorHandlers' => [] ]; @@ -263,142 +264,186 @@ 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 string|callable $handler + * @param string $type + * @param callable $handler */ - public function setNotFoundHandler($handler) + public function setErrorHandler($type, $handler) { - $this->setErrorHandler(HttpNotFoundException::class, $handler); + if (!is_callable($handler)) { + throw new RuntimeException(sprintf( + 'Invalid parameter %s of type %s passed to the setErrorHandler method. ' . + 'Please provide a callable function.', + $handler, + gettype($handler) + )); + } + + $handlers = $this->getSetting('errorHandlers', []); + $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. * - * @return callable|Error + * @param string $type + * @return callable */ - public function getNotFoundHandler() + public function getErrorHandler($type) { - return $this->getErrorHandler(HttpNotFoundException::class); + $handlers = $this->getSetting('errorHandlers'); + + if (isset($handlers[$type])) { + $handler = $handlers[$type]; + + if (is_callable($handler)) { + return $handler; + } + } + + return $this->getDefaultErrorHandler(); } /** - * Set callable to handle scenarios where a suitable - * route matches the request URI but not the request method. + * 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 string|callable $handler + * @param callable $handler */ - public function setNotAllowedHandler($handler) + public function setDefaultErrorHandler($handler) { - $this->setErrorHandler(HttpNotAllowedException::class, $handler); + if (!is_callable($handler)) { + throw new RuntimeException(sprintf( + 'Invalid parameter %s of type %s passed to the setDefaultErrorHandler method. ' . + 'Please provide a callable function.', + $handler, + gettype($handler) + )); + } + + $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|ErrorHandler */ - public function getNotAllowedHandler() + public function getDefaultErrorHandler() { - return $this->getErrorHandler(HttpNotAllowedException::class); + return $this->getSetting('defaultErrorHandler', 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 string $type - * @param string|callable $handler + * @param callable $handler */ - public function setErrorHandler($type, $handler) + public function setNotFoundHandler($handler) { - if (!(is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) - && !is_callable($handler) - ) { - throw new RuntimeException(sprintf( - 'Invalid parameter %s of type %s passed to setErrorHandler method. ' . - 'Please provide a valid subclass name of AbstractErrorHandler ' . - 'or an invokable callback function.', - $handler, - gettype($handler) - )); - } - - $handlers = $this->getSetting('errorHandlers', []); - $handlers[$type] = $handler; - $this->addSetting('errorHandlers', $handlers); + $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. * - * @param null|string $type * @return callable|ErrorHandlerInterface */ - public function getErrorHandler($type = null) + public function getNotFoundHandler() { - $handlers = $this->getSetting('errorHandlers'); - $displayErrorDetails = $this->getSetting('displayErrorDetails'); - - if (!is_null($type) && isset($handlers[$type])) { - $handler = $handlers[$type]; + return $this->getErrorHandler(HttpNotFoundException::class); + } - if (is_callable($handler)) { - return $handler; - } elseif (is_string($handler) && is_subclass_of($handler, AbstractErrorHandler::class)) { - return new $handler($displayErrorDetails); - } - } + /** + * 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 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 string|callable $handler + */ + public function setNotAllowedHandler($handler) + { + $this->setErrorHandler(HttpNotAllowedException::class, $handler); + } - return new ErrorHandler($displayErrorDetails); + /** + * Get callable to handle scenarios where a suitable + * route matches the request URI but not the request method. + * + * @return callable + */ + public function getNotAllowedHandler() + { + return $this->getErrorHandler(HttpNotAllowedException::class); } /** * Set callable to handle scenarios where a PHP error * occurs when processing 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) + * + * Or a subclass name of AbstractErrorHandler + * eg. CustomErrorHandler::class * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * - * @param string|callable $handler + * @param callable $handler */ public function setPhpErrorHandler($handler) { @@ -409,7 +454,7 @@ public function setPhpErrorHandler($handler) * Get callable to handle scenarios where a PHP error * occurs when processing the current request. * - * @return callable|ErrorHandlerInterface + * @return callable */ public function getPhpErrorHandler() { @@ -777,6 +822,7 @@ public function handleException(Exception $exception, ServerRequestInterface $re { $exceptionType = get_class($exception); $handler = $this->getErrorHandler($exceptionType); + $displayErrorDetails = $this->getSetting('displayErrorDetails'); /** * Retrieve request object from exception @@ -800,7 +846,7 @@ public function handleException(Exception $exception, ServerRequestInterface $re } return $recoverable - ? call_user_func_array($handler, [$request, $response, $exception]) + ? call_user_func_array($handler, [$request, $response, $exception, $displayErrorDetails]) : $response; } diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index b87d3f8c8..da4fab633 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -12,7 +12,6 @@ use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; -use Slim\Exception\PhpException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; use Slim\Handlers\ErrorRenderers\XmlErrorRenderer; @@ -75,39 +74,36 @@ abstract class AbstractErrorHandler implements ErrorHandlerInterface */ protected $statusCode; - /** - * AbstractHandler constructor. - * @param bool $displayErrorDetails - */ - public function __construct($displayErrorDetails = false) - { - $this->displayErrorDetails = $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 + * @param bool $displayErrorDetails Whether or not to display the error details * * @return ResponseInterface */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Exception $exception) - { + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + Exception $exception, + $displayErrorDetails = false + ) { + $this->displayErrorDetails = $displayErrorDetails; $this->request = $request; $this->response = $response; $this->exception = $exception; $this->method = $request->getMethod(); - $this->statusCode = $this->resolveStatusCode(); - $this->contentType = $this->resolveContentType($request); - $this->renderer = $this->resolveRenderer(); + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request); + $this->renderer = $this->determineRenderer(); if (!$this->displayErrorDetails) { $this->writeToErrorLog(); } - return $this->respond(); + return $this->formatResponse(); } /** @@ -120,7 +116,7 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res * @param ServerRequestInterface $request * @return string */ - protected function resolveContentType(ServerRequestInterface $request) + protected function determineContentType(ServerRequestInterface $request) { $acceptHeader = $request->getHeaderLine('Accept'); $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); @@ -147,7 +143,7 @@ protected function resolveContentType(ServerRequestInterface $request) * * @throws RuntimeException */ - protected function resolveRenderer() + protected function determineRenderer() { $renderer = null; @@ -193,7 +189,7 @@ protected function resolveRenderer() /** * @return int */ - protected function resolveStatusCode() + protected function determineStatusCode() { $statusCode = 500; @@ -207,13 +203,12 @@ protected function resolveStatusCode() /** * @return ResponseInterface */ - public function respond() + protected function formatResponse() { $e = $this->exception; $response = $this->response; - $output = $this->renderer->render(); $body = new Body(fopen('php://temp', 'r+')); - $body->write($output); + $body->write($this->renderer->render()); if ($e instanceof HttpNotAllowedException) { $response = $response->withHeader('Allow', $e->getAllowedMethods()); diff --git a/Slim/Interfaces/ErrorHandlerInterface.php b/Slim/Interfaces/ErrorHandlerInterface.php index 6ab296ae0..10a2ac18b 100644 --- a/Slim/Interfaces/ErrorHandlerInterface.php +++ b/Slim/Interfaces/ErrorHandlerInterface.php @@ -9,6 +9,8 @@ namespace Slim\Interfaces; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Exception; /** * ErrorHandlerInterface @@ -19,7 +21,16 @@ interface ErrorHandlerInterface { /** + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param Exception $exception + * @param $displayErrorDetails * @return ResponseInterface */ - public function respond(); + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + Exception $exception, + $displayErrorDetails + ); } diff --git a/Slim/Interfaces/ErrorRendererInterface.php b/Slim/Interfaces/ErrorRendererInterface.php index f52df6dfd..cebc88496 100644 --- a/Slim/Interfaces/ErrorRendererInterface.php +++ b/Slim/Interfaces/ErrorRendererInterface.php @@ -16,6 +16,7 @@ */ interface ErrorRendererInterface { + public function render(); public function renderPhpExceptionOutput(); public function renderGenericExceptionOutput(); } diff --git a/tests/AppTest.php b/tests/AppTest.php index 41d391605..0cade363b 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -92,10 +92,10 @@ public function testAddSetting() $this->assertAttributeContains('foo', 'settings', $app); } - public function testGetErrorHandler() + public function testGetDefaultErrorHandler() { $app = new App(); - $this->assertInstanceOf('\Slim\Handlers\ErrorHandler', $app->getErrorHandler()); + $this->assertInstanceOf('\Slim\Handlers\ErrorHandler', $app->getDefaultErrorHandler()); } /******************************************************************************** @@ -1019,36 +1019,54 @@ public function testSetErrorHandler() $this->assertEquals($res->getBody(), $expectedOutput); } + public function testSetDefaultErrorHandler() + { + $app = $this->appFactory(); + $app->get('/foo', function ($req, $res, $args) { + return $res; + }); + $app->add(function () { + throw new HttpNotFoundException; + }); + $handler = function ($req, $res) { + return $res->withJson(['Oops..']); + }; + $app->setDefaultErrorHandler($handler); + $res = $app->run(true); + $expectedOutput = json_encode(['Oops..']); + + $this->assertEquals($res->getBody(), $expectedOutput); + } + /** * @expectedException \RuntimeException */ public function testSetErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() { $app = new App(); - $app->setErrorHandler('RandomExceptionClassName', 'NonExistantClassname'); + $app->setErrorHandler('RandomExceptionClassName', 'InvalidParameter'); } - public function testErrorHandlerShortcuts() + /** + * @expectedException \RuntimeException + */ + public function testSetDefaultErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() { $app = new App(); - $handler = MockErrorHandler::class; - $app->setNotAllowedHandler($handler); - $app->setNotFoundHandler($handler); - $app->setPhpErrorHandler($handler); - - $this->assertInstanceOf($handler, $app->getErrorHandler(HttpNotAllowedException::class)); - $this->assertInstanceOf($handler, $app->getErrorHandler(HttpNotFoundException::class)); - $this->assertInstanceOf($handler, $app->getErrorHandler(PhpException::class)); + $app->setDefaultErrorHandler('RandomExceptionClassName'); } - public function testGetErrorHandlerInstantiatesHandlerFromString() + public function testErrorHandlerShortcuts() { $app = new App(); - $exception = HttpNotFoundException::class; - $app->setErrorHandler($exception, MockErrorHandler::class); - $handler = $app->getErrorHandler($exception); + $handler = new MockErrorHandler; + $app->setNotAllowedHandler($handler); + $app->setNotFoundHandler($handler); + $app->setPhpErrorHandler($handler); - $this->assertInstanceOf(MockErrorHandler::class, $handler); + $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotAllowedException::class)); + $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotFoundException::class)); + $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(PhpException::class)); } public function testGetErrorHandlerWillReturnDefaultErrorHandlerForPhpExceptions() diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php index a296dade6..2265971c5 100644 --- a/tests/Handlers/AbstractErrorHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -36,7 +36,7 @@ public function testHalfValidContentType() $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($abstractHandler, $newTypes); - $method = $class->getMethod('resolveContentType'); + $method = $class->getMethod('determineContentType'); $method->setAccessible(true); $return = $method->invoke($abstractHandler, $req); @@ -58,15 +58,15 @@ public function testAcceptableMediaTypeIsNotFirstInList() ->method('getHeaderLine') ->willReturn('text/plain,text/html'); - // provide access to the resolveContentType() as it's a protected method + // provide access to the determineContentType() as it's a protected method $class = new \ReflectionClass(AbstractErrorHandler::class); - $method = $class->getMethod('resolveContentType'); + $method = $class->getMethod('determineContentType'); $method->setAccessible(true); // use a mock object here as AbstractErrorHandler cannot be directly instantiated $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); - // call resolveContentType() + // call determineContentType() $return = $method->invoke($abstractHandler, $request); $this->assertEquals('text/html', $return); From 506b1ea05bb0368965349fc8c5acaec0bf96911a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 23:25:13 -0600 Subject: [PATCH 25/36] Refactor error handler setters to implement CallableResolver --- Slim/App.php | 52 +++++++----------- Slim/Exception/HttpException.php | 3 +- Slim/Exception/PhpException.php | 7 +-- Slim/Handlers/AbstractErrorHandler.php | 23 +++++--- Slim/Handlers/AbstractErrorRenderer.php | 6 +-- .../ErrorRenderers/HtmlErrorRenderer.php | 54 +++++++++---------- .../ErrorRenderers/JsonErrorRenderer.php | 4 +- .../ErrorRenderers/PlainTextErrorRenderer.php | 4 +- tests/AppTest.php | 28 +++++----- tests/Handlers/AbstractErrorHandlerTest.php | 9 ++-- 10 files changed, 90 insertions(+), 100 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index 3b78f66cc..6bf9331ad 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -16,7 +16,6 @@ use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\PhpException; -use Slim\Handlers\AbstractErrorHandler; use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\ErrorHandlerInterface; @@ -25,7 +24,6 @@ use Slim\Interfaces\RouterInterface; use BadMethodCallException; use Exception; -use RuntimeException; use Throwable; /** @@ -272,26 +270,21 @@ public function getRouter() * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of Exception + * 3. Instance of \Exception * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of * \Psr\Http\Message\ResponseInterface. * * @param string $type - * @param callable $handler + * @param callable $callable + * + * @throws \RuntimeException */ - public function setErrorHandler($type, $handler) + public function setErrorHandler($type, $callable) { - if (!is_callable($handler)) { - throw new RuntimeException(sprintf( - 'Invalid parameter %s of type %s passed to the setErrorHandler method. ' . - 'Please provide a callable function.', - $handler, - gettype($handler) - )); - } - + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); $handlers = $this->getSetting('errorHandlers', []); $handlers[$type] = $handler; $this->addSetting('errorHandlers', $handlers); @@ -327,25 +320,20 @@ public function getErrorHandler($type) * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of Exception + * 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 setDefaultErrorHandler($handler) + public function setDefaultErrorHandler($callable) { - if (!is_callable($handler)) { - throw new RuntimeException(sprintf( - 'Invalid parameter %s of type %s passed to the setDefaultErrorHandler method. ' . - 'Please provide a callable function.', - $handler, - gettype($handler) - )); - } - + $resolver = $this->getCallableResolver(); + $handler = $resolver->resolve($callable); $this->addSetting('defaultErrorHandler', $handler); } @@ -356,7 +344,7 @@ public function setDefaultErrorHandler($handler) */ public function getDefaultErrorHandler() { - return $this->getSetting('defaultErrorHandler', new ErrorHandler); + return $this->getSetting('defaultErrorHandler', new ErrorHandler()); } /** @@ -368,7 +356,7 @@ public function getDefaultErrorHandler() * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of Exception + * 3. Instance of \Exception * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of @@ -401,7 +389,7 @@ public function getNotFoundHandler() * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of Exception + * 3. Instance of \Exception * 4. Boolean displayErrorDetails (optional) * * The callable MUST return an instance of @@ -434,7 +422,7 @@ public function getNotAllowedHandler() * * 1. Instance of \Psr\Http\Message\ServerRequestInterface * 2. Instance of \Psr\Http\Message\ResponseInterface - * 3. Instance of Exception + * 3. Instance of \Exception * 4. Boolean displayErrorDetails (optional) * * Or a subclass name of AbstractErrorHandler @@ -856,7 +844,7 @@ public function handleException(Exception $exception, ServerRequestInterface $re * @param ResponseInterface $response * @return ResponseInterface * - * @throws RuntimeException + * @throws \RuntimeException */ protected function finalize(ResponseInterface $response) { @@ -870,7 +858,7 @@ protected function finalize(ResponseInterface $response) // Add Content-Length header if `addContentLengthHeader` setting is set if ($this->getSetting('addContentLengthHeader') == true) { if (ob_get_length() > 0) { - throw new RuntimeException("Unexpected data in output buffer. " . + throw new \RuntimeException("Unexpected data in output buffer. " . "Maybe you have characters before an opening getBody()->getSize(); diff --git a/Slim/Exception/HttpException.php b/Slim/Exception/HttpException.php index 59bb4b2f0..4a444b314 100644 --- a/Slim/Exception/HttpException.php +++ b/Slim/Exception/HttpException.php @@ -1,11 +1,10 @@ getHeaderLine('Accept'); $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + $count = count($selectedContentTypes); - if (count($selectedContentTypes)) { - return current($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)) { @@ -141,7 +152,7 @@ protected function determineContentType(ServerRequestInterface $request) * * @return ErrorRendererInterface * - * @throws RuntimeException + * @throws \RuntimeException */ protected function determineRenderer() { @@ -155,7 +166,7 @@ protected function determineRenderer() if (!is_null($this->renderer)) { $renderer = $this->renderer; if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { - throw new RuntimeException(sprintf( + throw new \RuntimeException(sprintf( 'Non compliant error renderer provided (%s). ' . 'Renderer expected to be a subclass of AbstractErrorRenderer', $renderer diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index ab7d25fd4..e5116d3c1 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -10,8 +10,6 @@ use Slim\Exception\PhpException; use Slim\Interfaces\ErrorRendererInterface; -use Exception; -use Throwable; /** * Default Slim application error renderer @@ -22,7 +20,7 @@ abstract class AbstractErrorRenderer implements ErrorRendererInterface { /** - * @var Exception + * @var \Exception */ protected $exception; /** @@ -32,7 +30,7 @@ abstract class AbstractErrorRenderer implements ErrorRendererInterface /** * AbstractErrorRenderer constructor. - * @param Exception|Throwable $exception + * @param \Exception|\Throwable $exception * @param bool $displayErrorDetails */ public function __construct($exception, $displayErrorDetails = false) diff --git a/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php index e9dcf1ed6..b84316a2c 100644 --- a/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php @@ -8,9 +8,7 @@ */ namespace Slim\Handlers\ErrorRenderers; -use Error; use Exception; -use RuntimeException; use Slim\Handlers\AbstractErrorRenderer; /** @@ -31,17 +29,7 @@ public function renderPhpExceptionOutput() $html = '

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

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

%s

%s", - $title, - $title, - $html - ); - - return $output; + return $this->renderHtmlBody($title, $html); } public function renderGenericExceptionOutput() @@ -50,31 +38,43 @@ public function renderGenericExceptionOutput() $title = ''; $description = ''; - if ($this->displayErrorDetails) { - $description = $e->getMessage(); - } - if (method_exists($e, 'getTitle')) { $title = $e->getTitle(); } if (method_exists($e, 'getDescription')) { $description = $e->getDescription(); + } elseif ($this->displayErrorDetails) { + $description = $e->getMessage(); } + $html = "

${description}

"; + + return $this->renderHtmlBody($title, $html); + } - $output = sprintf( - "%s

%s

%s

" . - "Go Back", + public function renderHtmlBody($title = '', $html = '') + { + return sprintf( + "" . + " " . + " " . + " %s" . + " " . + " " . + " " . + "

%s

" . + "
%s
" . + " Go Back" . + " " . + "", $title, $title, - $description + $html ); - - return $output; } /** diff --git a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php index 33905da3b..1f231c246 100644 --- a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -9,8 +9,6 @@ namespace Slim\Handlers\ErrorRenderers; use Slim\Handlers\AbstractErrorRenderer; -use Exception; -use Throwable; /** * Default Slim application JSON Error Renderer @@ -49,7 +47,7 @@ public function formatExceptionPayload($message) } /** - * @param Exception|Throwable $e + * @param \Exception|\Throwable $e * @return array */ public function formatExceptionFragment($e) diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index b1ff3f1e1..2f7e33116 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -9,8 +9,6 @@ namespace Slim\Handlers\ErrorRenderers; use Slim\Handlers\AbstractErrorRenderer; -use Exception; -use Throwable; /** * Default Slim application Plain Text Error Renderer @@ -47,7 +45,7 @@ public function formatExceptionBody() } /** - * @param Exception|Throwable $e + * @param \Exception|\Throwable $e * @return string */ public function formatExceptionFragment($e) diff --git a/tests/AppTest.php b/tests/AppTest.php index 0cade363b..5d74c0994 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1006,7 +1006,7 @@ public function testSetErrorHandler() return $res; }); $app->add(function () { - throw new HttpNotFoundException; + throw new HttpNotFoundException(); }); $exception = HttpNotFoundException::class; $handler = function ($req, $res) { @@ -1019,6 +1019,15 @@ public function testSetErrorHandler() $this->assertEquals($res->getBody(), $expectedOutput); } + /** + * @expectedException \RuntimeException + */ + public function testSetErrorHandlerThrowsExceptionWhenInvalidCallableIsPassed() + { + $app = new App(); + $app->setErrorHandler(HttpNotFoundException::class, 'UnresolvableCallable'); + } + public function testSetDefaultErrorHandler() { $app = $this->appFactory(); @@ -1026,7 +1035,7 @@ public function testSetDefaultErrorHandler() return $res; }); $app->add(function () { - throw new HttpNotFoundException; + throw new HttpNotFoundException(); }); $handler = function ($req, $res) { return $res->withJson(['Oops..']); @@ -1041,25 +1050,16 @@ public function testSetDefaultErrorHandler() /** * @expectedException \RuntimeException */ - public function testSetErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() - { - $app = new App(); - $app->setErrorHandler('RandomExceptionClassName', 'InvalidParameter'); - } - - /** - * @expectedException \RuntimeException - */ - public function testSetDefaultErrorHandlerThrowsExceptionWhenInvalidArgumentPassed() + public function testSetDefaultErrorHandlerThrowsExceptionWhenInvalidCallableIsPassed() { $app = new App(); - $app->setDefaultErrorHandler('RandomExceptionClassName'); + $app->setDefaultErrorHandler(HttpNotFoundException::class, 'UnresolvableCallable'); } public function testErrorHandlerShortcuts() { $app = new App(); - $handler = new MockErrorHandler; + $handler = new MockErrorHandler(); $app->setNotAllowedHandler($handler); $app->setNotFoundHandler($handler); $app->setPhpErrorHandler($handler); diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php index 2265971c5..69ac46ddb 100644 --- a/tests/Handlers/AbstractErrorHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -13,6 +13,7 @@ use Slim\Handlers\AbstractErrorHandler; use Slim\Handlers\ErrorHandler; use Slim\Http\Response; +use ReflectionClass; class AbstractErrorHandlerTest extends TestCase { @@ -30,7 +31,7 @@ public function testHalfValidContentType() 'text/html', ]; - $class = new \ReflectionClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); $reflectionProperty = $class->getProperty('knownContentTypes'); $reflectionProperty->setAccessible(true); @@ -59,7 +60,7 @@ public function testAcceptableMediaTypeIsNotFirstInList() ->willReturn('text/plain,text/html'); // provide access to the determineContentType() as it's a protected method - $class = new \ReflectionClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); $method = $class->getMethod('determineContentType'); $method->setAccessible(true); @@ -74,8 +75,8 @@ public function testAcceptableMediaTypeIsNotFirstInList() public function testOptions() { - $handler = new ErrorHandler; - $exception = new HttpNotAllowedException; + $handler = new ErrorHandler(); + $exception = new HttpNotAllowedException(); $exception->setAllowedMethods(['POST', 'PUT']); /** @var Response $res */ $res = $handler->__invoke($this->getRequest('OPTIONS'), new Response(), $exception); From e448e173434ad43c19bed8771d79ddcd7482b02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Thu, 11 May 2017 23:29:48 -0600 Subject: [PATCH 26/36] Add HttpForbiddenException --- Slim/Exception/HttpForbiddenException.php | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Slim/Exception/HttpForbiddenException.php diff --git a/Slim/Exception/HttpForbiddenException.php b/Slim/Exception/HttpForbiddenException.php new file mode 100644 index 000000000..28e8e6cf7 --- /dev/null +++ b/Slim/Exception/HttpForbiddenException.php @@ -0,0 +1,10 @@ + Date: Thu, 11 May 2017 23:54:17 -0600 Subject: [PATCH 27/36] Add internal getErrorHandlers method to App to assert setting is of valid type --- Slim/App.php | 25 +++++++++++++++++++++++-- tests/AppTest.php | 11 +++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index 6bf9331ad..c1bec9fea 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -285,7 +285,7 @@ public function setErrorHandler($type, $callable) { $resolver = $this->getCallableResolver(); $handler = $resolver->resolve($callable); - $handlers = $this->getSetting('errorHandlers', []); + $handlers = $this->getErrorHandlers(); $handlers[$type] = $handler; $this->addSetting('errorHandlers', $handlers); } @@ -296,10 +296,12 @@ public function setErrorHandler($type, $callable) * * @param string $type * @return callable + * + * @throws \RuntimeException */ public function getErrorHandler($type) { - $handlers = $this->getSetting('errorHandlers'); + $handlers = $this->getErrorHandlers(); if (isset($handlers[$type])) { $handler = $handlers[$type]; @@ -312,6 +314,25 @@ public function getErrorHandler($type) return $this->getDefaultErrorHandler(); } + /** + * 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. * diff --git a/tests/AppTest.php b/tests/AppTest.php index 5d74c0994..c6caee132 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1087,6 +1087,17 @@ public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExce $this->assertInstanceOf(ErrorHandler::class, $handler); } + /** + * @expectedException \RuntimeException + */ + public function testGetErrorHandlersThrowsExceptionWhenErrorHandlersArgumentSettingIsNotArray() + { + $settings = ['errorHandlers' => 'ShouldBeArray']; + $app = new App($settings); + $handler = new MockErrorHandler(); + $app->setErrorHandler(HttpNotFoundException::class, $handler); + } + /******************************************************************************** * Runner *******************************************************************************/ From 44bcaded3dbbbbf2db7ae4c4b08488545f166f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=83=C2=A9rub=C3=83=C2=A9?= Date: Sat, 13 May 2017 00:48:00 -0600 Subject: [PATCH 28/36] Add unit test for AbstractErrorHandler class and refactor misc. code blocks --- Slim/App.php | 9 ++++- Slim/Handlers/AbstractErrorHandler.php | 41 +++++++++------------ Slim/Handlers/AbstractErrorRenderer.php | 17 +++++++-- Slim/Interfaces/ErrorRendererInterface.php | 22 +++++++++++ tests/AppTest.php | 3 +- tests/Handlers/AbstractErrorHandlerTest.php | 34 +++++++++++++++++ tests/Mocks/MockErrorRenderer.php | 27 ++++++++++++++ 7 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 tests/Mocks/MockErrorRenderer.php diff --git a/Slim/App.php b/Slim/App.php index c1bec9fea..c66c8e0cf 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -854,8 +854,15 @@ public function handleException(Exception $exception, ServerRequestInterface $re $recoverable = $exception->isRecoverable(); } + $params = [ + $request, + $response, + $exception, + $displayErrorDetails + ]; + return $recoverable - ? call_user_func_array($handler, [$request, $response, $exception, $displayErrorDetails]) + ? call_user_func_array($handler, $params) : $response; } diff --git a/Slim/Handlers/AbstractErrorHandler.php b/Slim/Handlers/AbstractErrorHandler.php index 7811e0c7e..507c1a34b 100644 --- a/Slim/Handlers/AbstractErrorHandler.php +++ b/Slim/Handlers/AbstractErrorHandler.php @@ -10,6 +10,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Slim\CallableResolver; use Slim\Exception\HttpException; use Slim\Exception\HttpNotAllowedException; use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; @@ -156,23 +157,19 @@ protected function determineContentType(ServerRequestInterface $request) */ protected function determineRenderer() { - $renderer = null; - - if ($this->method === 'OPTIONS') { - $this->statusCode = 200; - $this->contentType = 'text/plain'; + $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($this->renderer)) { - $renderer = $this->renderer; - if (!is_subclass_of($renderer, AbstractErrorRenderer::class)) { - throw new \RuntimeException(sprintf( - 'Non compliant error renderer provided (%s). ' . - 'Renderer expected to be a subclass of AbstractErrorRenderer', - $renderer - )); - } - } else { + if (is_null($renderer)) { switch ($this->contentType) { case 'application/json': $renderer = JsonErrorRenderer::class; @@ -202,13 +199,12 @@ protected function determineRenderer() */ protected function determineStatusCode() { - $statusCode = 500; - - if ($this->exception instanceof HttpException) { - $statusCode = $this->exception->getCode(); + if ($this->method === 'OPTIONS') { + return 200; + } elseif ($this->exception instanceof HttpException) { + return $this->exception->getCode(); } - - return $statusCode; + return 500; } /** @@ -218,8 +214,7 @@ protected function formatResponse() { $e = $this->exception; $response = $this->response; - $body = new Body(fopen('php://temp', 'r+')); - $body->write($this->renderer->render()); + $body = $this->renderer->renderWithBody(); if ($e instanceof HttpNotAllowedException) { $response = $response->withHeader('Allow', $e->getAllowedMethods()); diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index e5116d3c1..e55cc81ed 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -9,6 +9,7 @@ namespace Slim\Handlers; use Slim\Exception\PhpException; +use Slim\Http\Body; use Slim\Interfaces\ErrorRendererInterface; /** @@ -44,10 +45,18 @@ public function __construct($exception, $displayErrorDetails = false) */ public function render() { - if ($this->exception instanceof PhpException) { - return $this->renderPhpExceptionOutput(); - } + return $this->exception instanceof PhpException + ? $this->renderPhpExceptionOutput() + : $this->renderGenericExceptionOutput(); + } - return $this->renderGenericExceptionOutput(); + /** + * @return Body + */ + public function renderWithBody() + { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; } } diff --git a/Slim/Interfaces/ErrorRendererInterface.php b/Slim/Interfaces/ErrorRendererInterface.php index cebc88496..b5643b6f6 100644 --- a/Slim/Interfaces/ErrorRendererInterface.php +++ b/Slim/Interfaces/ErrorRendererInterface.php @@ -16,7 +16,29 @@ */ interface ErrorRendererInterface { + /** + * @param \Exception|\Throwable $exception + * @param bool $displayErrorDetails + */ + public function __construct($exception, $displayErrorDetails); + + /** + * @return string + */ public function render(); + + /** + * @return \Slim\Http\Body + */ + public function renderWithBody(); + + /** + * @return string + */ public function renderPhpExceptionOutput(); + + /** + * @return string + */ public function renderGenericExceptionOutput(); } diff --git a/tests/AppTest.php b/tests/AppTest.php index c6caee132..96f9a3fb4 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -2041,7 +2041,8 @@ public function testCallingAContainerCallable() $exception = new HttpNotFoundException; $notFoundHandler = $app->getNotFoundHandler(); - $response = $notFoundHandler($request, $response, $exception); + $displayErrorDetails = $app->getSetting('displayErrorDetails'); + $response = $notFoundHandler($request, $response, $exception, $displayErrorDetails); $this->assertSame(404, $response->getStatusCode()); } diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php index 69ac46ddb..7d3dd253e 100644 --- a/tests/Handlers/AbstractErrorHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -13,10 +13,44 @@ use Slim\Handlers\AbstractErrorHandler; use Slim\Handlers\ErrorHandler; use Slim\Http\Response; +use Slim\Tests\Mocks\MockErrorRenderer; use ReflectionClass; class AbstractErrorHandlerTest extends TestCase { + public function testDetermineContentTypeMethodDoesNotThrowExceptionWhenPassedValidRenderer() + { + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); + + $reflectionProperty = $class->getProperty('renderer'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($abstractHandler, MockErrorRenderer::class); + + $method = $class->getMethod('determineRenderer'); + $method->setAccessible(true); + $method->invoke($abstractHandler); + + $this->addToAssertionCount(1); + } + + /** + * @expectedException \RuntimeException + */ + public function testDetermineContentTypeMethodThrowsExceptionWhenPassedAnInvalidRenderer() + { + $abstractHandler = $this->getMockForAbstractClass(AbstractErrorHandler::class); + $class = new ReflectionClass(AbstractErrorHandler::class); + + $reflectionProperty = $class->getProperty('renderer'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($abstractHandler, 'NonExistentRenderer::class'); + + $method = $class->getMethod('determineRenderer'); + $method->setAccessible(true); + $method->invoke($abstractHandler); + } + public function testHalfValidContentType() { $req = $this->getMockBuilder('Slim\Http\Request')->disableOriginalConstructor()->getMock(); diff --git a/tests/Mocks/MockErrorRenderer.php b/tests/Mocks/MockErrorRenderer.php new file mode 100644 index 000000000..5e53b8ec9 --- /dev/null +++ b/tests/Mocks/MockErrorRenderer.php @@ -0,0 +1,27 @@ + Date: Sat, 13 May 2017 14:02:19 -0600 Subject: [PATCH 29/36] Implement CallableResolver for errorHandlers App settings --- Slim/App.php | 6 ++---- tests/AppTest.php | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index c66c8e0cf..ff00827eb 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -305,10 +305,8 @@ public function getErrorHandler($type) if (isset($handlers[$type])) { $handler = $handlers[$type]; - - if (is_callable($handler)) { - return $handler; - } + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); } return $this->getDefaultErrorHandler(); diff --git a/tests/AppTest.php b/tests/AppTest.php index 96f9a3fb4..17c812523 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Slim\App; +use Slim\Container; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\HttpNotFoundException; use Slim\Exception\PhpException; @@ -1087,6 +1088,20 @@ public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExce $this->assertInstanceOf(ErrorHandler::class, $handler); } + public function testGetErrorHandlerResolvesContainerCallableWhenHandlerPassedIntoSettings() + { + $app = new App(); + $container = new Container(); + $container[MockErrorHandler::class] = function ($c) { + return new MockErrorHandler(); + }; + $app->setContainer($container); + $app->setNotAllowedHandler(MockErrorHandler::class); + $handler = $app->getErrorHandler(HttpNotAllowedException::class); + + $this->assertEquals([new MockErrorHandler(), '__invoke'], $handler); + } + /** * @expectedException \RuntimeException */ From a3377933079d20774b5a26a54156a639675db076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Sat, 13 May 2017 15:13:52 -0600 Subject: [PATCH 30/36] Implement CallableResolver for defaultErrorHandler and add supporting unit tests --- Slim/App.php | 9 ++++++++- tests/AppTest.php | 14 +++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Slim/App.php b/Slim/App.php index ff00827eb..937f57cb9 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -363,7 +363,14 @@ public function setDefaultErrorHandler($callable) */ public function getDefaultErrorHandler() { - return $this->getSetting('defaultErrorHandler', new ErrorHandler()); + $handler = $this->getSetting('defaultErrorHandler', null); + + if (!is_null($handler)) { + $resolver = $this->getCallableResolver(); + return $resolver->resolve($handler); + } + + return new ErrorHandler(); } /** diff --git a/tests/AppTest.php b/tests/AppTest.php index 17c812523..cc9c02fe5 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -1092,9 +1092,6 @@ public function testGetErrorHandlerResolvesContainerCallableWhenHandlerPassedInt { $app = new App(); $container = new Container(); - $container[MockErrorHandler::class] = function ($c) { - return new MockErrorHandler(); - }; $app->setContainer($container); $app->setNotAllowedHandler(MockErrorHandler::class); $handler = $app->getErrorHandler(HttpNotAllowedException::class); @@ -1102,6 +1099,17 @@ public function testGetErrorHandlerResolvesContainerCallableWhenHandlerPassedInt $this->assertEquals([new MockErrorHandler(), '__invoke'], $handler); } + public function testGetDefaultHandlerResolvesContainerCallableWhenHandlerPassedIntoSettings() + { + $app = new App(); + $container = new Container(); + $app->setContainer($container); + $app->setDefaultErrorHandler(MockErrorHandler::class); + $handler = $app->getDefaultErrorHandler(); + + $this->assertEquals([new MockErrorHandler(), '__invoke'], $handler); + } + /** * @expectedException \RuntimeException */ From 6fc9102191856afc123a6dcc58e3ebbfcd3b04a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20B=C3=A9rub=C3=A9?= Date: Sat, 13 May 2017 15:35:11 -0600 Subject: [PATCH 31/36] Add HttpBadGatewayException and fix spelling error in HttpForbiddenException --- Slim/Exception/HttpBadGatewayException.php | 10 ++++++++++ Slim/Exception/HttpForbiddenException.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Slim/Exception/HttpBadGatewayException.php 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 @@ + Date: Wed, 17 May 2017 12:11:56 -0600 Subject: [PATCH 32/36] Fix conflicts for merge #2221 --- Slim/Http/Body.php | 22 - Slim/Http/Cookies.php | 195 ----- Slim/Http/Environment.php | 52 -- Slim/Http/Headers.php | 222 ------ Slim/Http/Message.php | 304 -------- Slim/Http/Request.php | 1200 ------------------------------ Slim/Http/RequestBody.php | 27 - Slim/Http/Response.php | 480 ------------ Slim/Http/Stream.php | 450 ----------- Slim/Http/UploadedFile.php | 327 -------- Slim/Http/Uri.php | 762 ------------------- tests/Http/BodyTest.php | 416 ----------- tests/Http/CookiesTest.php | 235 ------ tests/Http/EnvironmentTest.php | 59 -- tests/Http/HeadersTest.php | 228 ------ tests/Http/MessageTest.php | 214 ------ tests/Http/RequestBodyTest.php | 334 --------- tests/Http/RequestTest.php | 1170 ----------------------------- tests/Http/ResponseTest.php | 341 --------- tests/Http/StreamTest.php | 159 ---- tests/Http/UploadedFilesTest.php | 486 ------------ tests/Http/UriTest.php | 609 --------------- 22 files changed, 8292 deletions(-) delete mode 100644 Slim/Http/Body.php delete mode 100644 Slim/Http/Cookies.php delete mode 100644 Slim/Http/Environment.php delete mode 100644 Slim/Http/Headers.php delete mode 100644 Slim/Http/Message.php delete mode 100644 Slim/Http/Request.php delete mode 100644 Slim/Http/RequestBody.php delete mode 100644 Slim/Http/Response.php delete mode 100644 Slim/Http/Stream.php delete mode 100644 Slim/Http/UploadedFile.php delete mode 100644 Slim/Http/Uri.php delete mode 100644 tests/Http/BodyTest.php delete mode 100644 tests/Http/CookiesTest.php delete mode 100644 tests/Http/EnvironmentTest.php delete mode 100644 tests/Http/HeadersTest.php delete mode 100644 tests/Http/MessageTest.php delete mode 100644 tests/Http/RequestBodyTest.php delete mode 100644 tests/Http/RequestTest.php delete mode 100755 tests/Http/ResponseTest.php delete mode 100644 tests/Http/StreamTest.php delete mode 100644 tests/Http/UploadedFilesTest.php delete mode 100644 tests/Http/UriTest.php diff --git a/Slim/Http/Body.php b/Slim/Http/Body.php deleted file mode 100644 index 7a7b4df81..000000000 --- a/Slim/Http/Body.php +++ /dev/null @@ -1,22 +0,0 @@ - '', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => false, - 'httponly' => false - ]; - - /** - * Create new cookies helper - * - * @param array $cookies - */ - public function __construct(array $cookies = []) - { - $this->requestCookies = $cookies; - } - - /** - * Set default cookie properties - * - * @param array $settings - */ - public function setDefaults(array $settings) - { - $this->defaults = array_replace($this->defaults, $settings); - } - - /** - * Get request cookie - * - * @param string $name Cookie name - * @param mixed $default Cookie default value - * - * @return mixed Cookie value if present, else default - */ - public function get($name, $default = null) - { - return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; - } - - /** - * Set response cookie - * - * @param string $name Cookie name - * @param string|array $value Cookie value, or cookie properties - */ - public function set($name, $value) - { - if (!is_array($value)) { - $value = ['value' => (string)$value]; - } - $this->responseCookies[$name] = array_replace($this->defaults, $value); - } - - /** - * Convert to `Set-Cookie` headers - * - * @return string[] - */ - public function toHeaders() - { - $headers = []; - foreach ($this->responseCookies as $name => $properties) { - $headers[] = $this->toHeader($name, $properties); - } - - return $headers; - } - - /** - * Convert to `Set-Cookie` header - * - * @param string $name Cookie name - * @param array $properties Cookie properties - * - * @return string - */ - protected function toHeader($name, array $properties) - { - $result = urlencode($name) . '=' . urlencode($properties['value']); - - if (isset($properties['domain'])) { - $result .= '; domain=' . $properties['domain']; - } - - if (isset($properties['path'])) { - $result .= '; path=' . $properties['path']; - } - - if (isset($properties['expires'])) { - if (is_string($properties['expires'])) { - $timestamp = strtotime($properties['expires']); - } else { - $timestamp = (int)$properties['expires']; - } - if ($timestamp !== 0) { - $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); - } - } - - if (isset($properties['secure']) && $properties['secure']) { - $result .= '; secure'; - } - - if (isset($properties['hostonly']) && $properties['hostonly']) { - $result .= '; HostOnly'; - } - - if (isset($properties['httponly']) && $properties['httponly']) { - $result .= '; HttpOnly'; - } - - return $result; - } - - /** - * Parse HTTP request `Cookie:` header and extract - * into a PHP associative array. - * - * @param string $header The raw HTTP request `Cookie:` header - * - * @return array Associative array of cookie names and values - * - * @throws InvalidArgumentException if the cookie data cannot be parsed - */ - public static function parseHeader($header) - { - if (is_array($header) === true) { - $header = isset($header[0]) ? $header[0] : ''; - } - - if (is_string($header) === false) { - throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); - } - - $header = rtrim($header, "\r\n"); - $pieces = preg_split('@[;]\s*@', $header); - $cookies = []; - - foreach ($pieces as $cookie) { - $cookie = explode('=', $cookie, 2); - - if (count($cookie) === 2) { - $key = urldecode($cookie[0]); - $value = urldecode($cookie[1]); - - if (!isset($cookies[$key])) { - $cookies[$key] = $value; - } - } - } - - return $cookies; - } -} diff --git a/Slim/Http/Environment.php b/Slim/Http/Environment.php deleted file mode 100644 index 786dc0a22..000000000 --- a/Slim/Http/Environment.php +++ /dev/null @@ -1,52 +0,0 @@ - 'HTTP/1.1', - 'REQUEST_METHOD' => 'GET', - 'SCRIPT_NAME' => '', - 'REQUEST_URI' => '', - 'QUERY_STRING' => '', - 'SERVER_NAME' => 'localhost', - 'SERVER_PORT' => 80, - 'HTTP_HOST' => 'localhost', - 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8', - 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', - 'HTTP_USER_AGENT' => 'Slim Framework', - 'REMOTE_ADDR' => '127.0.0.1', - 'REQUEST_TIME' => time(), - 'REQUEST_TIME_FLOAT' => microtime(true), - ], $userData); - - return new static($data); - } -} diff --git a/Slim/Http/Headers.php b/Slim/Http/Headers.php deleted file mode 100644 index f8c4ac140..000000000 --- a/Slim/Http/Headers.php +++ /dev/null @@ -1,222 +0,0 @@ - 1, - 'CONTENT_LENGTH' => 1, - 'PHP_AUTH_USER' => 1, - 'PHP_AUTH_PW' => 1, - 'PHP_AUTH_DIGEST' => 1, - 'AUTH_TYPE' => 1, - ]; - - /** - * Create new headers collection with data extracted from - * the application Environment object - * - * @param Environment $environment The Slim application Environment - * - * @return self - */ - public static function createFromEnvironment(Environment $environment) - { - $data = []; - $environment = self::determineAuthorization($environment); - foreach ($environment as $key => $value) { - $key = strtoupper($key); - if (isset(static::$special[$key]) || strpos($key, 'HTTP_') === 0) { - if ($key !== 'HTTP_CONTENT_LENGTH') { - $data[$key] = $value; - } - } - } - - return new static($data); - } - - /** - * If HTTP_AUTHORIZATION does not exist tries to get it from - * getallheaders() when available. - * - * @param Environment $environment The Slim application Environment - * - * @return Environment - */ - - public static function determineAuthorization(Environment $environment) - { - $authorization = $environment->get('HTTP_AUTHORIZATION'); - - if (null === $authorization && is_callable('getallheaders')) { - $headers = getallheaders(); - $headers = array_change_key_case($headers, CASE_LOWER); - if (isset($headers['authorization'])) { - $environment->set('HTTP_AUTHORIZATION', $headers['authorization']); - } - } - - return $environment; - } - - /** - * Return array of HTTP header names and values. - * This method returns the _original_ header name - * as specified by the end user. - * - * @return array - */ - public function all() - { - $all = parent::all(); - $out = []; - foreach ($all as $key => $props) { - $out[$props['originalKey']] = $props['value']; - } - - return $out; - } - - /** - * Set HTTP header value - * - * This method sets a header value. It replaces - * any values that may already exist for the header name. - * - * @param string $key The case-insensitive header name - * @param string $value The header value - */ - public function set($key, $value) - { - if (!is_array($value)) { - $value = [$value]; - } - parent::set($this->normalizeKey($key), [ - 'value' => $value, - 'originalKey' => $key - ]); - } - - /** - * Get HTTP header value - * - * @param string $key The case-insensitive header name - * @param mixed $default The default value if key does not exist - * - * @return string[] - */ - public function get($key, $default = null) - { - if ($this->has($key)) { - return parent::get($this->normalizeKey($key))['value']; - } - - return $default; - } - - /** - * Get HTTP header key as originally specified - * - * @param string $key The case-insensitive header name - * @param mixed $default The default value if key does not exist - * - * @return string - */ - public function getOriginalKey($key, $default = null) - { - if ($this->has($key)) { - return parent::get($this->normalizeKey($key))['originalKey']; - } - - return $default; - } - - /** - * Add HTTP header value - * - * This method appends a header value. Unlike the set() method, - * this method _appends_ this new value to any values - * that already exist for this header name. - * - * @param string $key The case-insensitive header name - * @param array|string $value The new header value(s) - */ - public function add($key, $value) - { - $oldValues = $this->get($key, []); - $newValues = is_array($value) ? $value : [$value]; - $this->set($key, array_merge($oldValues, array_values($newValues))); - } - - /** - * Does this collection have a given header? - * - * @param string $key The case-insensitive header name - * - * @return bool - */ - public function has($key) - { - return parent::has($this->normalizeKey($key)); - } - - /** - * Remove header from collection - * - * @param string $key The case-insensitive header name - */ - public function remove($key) - { - parent::remove($this->normalizeKey($key)); - } - - /** - * Normalize header name - * - * This method transforms header names into a - * normalized form. This is how we enable case-insensitive - * header names in the other methods in this class. - * - * @param string $key The case-insensitive header name - * - * @return string Normalized header name - */ - public function normalizeKey($key) - { - $key = strtr(strtolower($key), '_', '-'); - if (strpos($key, 'http-') === 0) { - $key = substr($key, 5); - } - - return $key; - } -} diff --git a/Slim/Http/Message.php b/Slim/Http/Message.php deleted file mode 100644 index d02a43c85..000000000 --- a/Slim/Http/Message.php +++ /dev/null @@ -1,304 +0,0 @@ - true, - '1.1' => true, - '2.0' => true, - ]; - - /** - * Headers - * - * @var \Slim\Interfaces\Http\HeadersInterface - */ - protected $headers; - - /** - * Body object - * - * @var \Psr\Http\Message\StreamInterface - */ - protected $body; - - - /** - * Disable magic setter to ensure immutability - */ - public function __set($name, $value) - { - // Do nothing - } - - /******************************************************************************* - * Protocol - ******************************************************************************/ - - /** - * Retrieves the HTTP protocol version as a string. - * - * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). - * - * @return string HTTP protocol version. - */ - public function getProtocolVersion() - { - return $this->protocolVersion; - } - - /** - * Return an instance with the specified HTTP protocol version. - * - * The version string MUST contain only the HTTP version number (e.g., - * "1.1", "1.0"). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new protocol version. - * - * @param string $version HTTP protocol version - * @return static - * @throws InvalidArgumentException if the http version is an invalid number - */ - public function withProtocolVersion($version) - { - if (!isset(self::$validProtocolVersions[$version])) { - throw new InvalidArgumentException( - 'Invalid HTTP version. Must be one of: ' - . implode(', ', array_keys(self::$validProtocolVersions)) - ); - } - $clone = clone $this; - $clone->protocolVersion = $version; - - return $clone; - } - - /******************************************************************************* - * Headers - ******************************************************************************/ - - /** - * Retrieves all message header values. - * - * The keys represent the header name as it will be sent over the wire, and - * each value is an array of strings associated with the header. - * - * // Represent the headers as a string - * foreach ($message->getHeaders() as $name => $values) { - * echo $name . ": " . implode(", ", $values); - * } - * - * // Emit headers iteratively: - * foreach ($message->getHeaders() as $name => $values) { - * foreach ($values as $value) { - * header(sprintf('%s: %s', $name, $value), false); - * } - * } - * - * While header names are not case-sensitive, getHeaders() will preserve the - * exact case in which headers were originally specified. - * - * @return array Returns an associative array of the message's headers. Each - * key MUST be a header name, and each value MUST be an array of strings - * for that header. - */ - public function getHeaders() - { - return $this->headers->all(); - } - - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $name Case-insensitive header field name. - * @return bool Returns true if any header names match the given header - * name using a case-insensitive string comparison. Returns false if - * no matching header name is found in the message. - */ - public function hasHeader($name) - { - return $this->headers->has($name); - } - - /** - * Retrieves a message header value by the given case-insensitive name. - * - * This method returns an array of all the header values of the given - * case-insensitive header name. - * - * If the header does not appear in the message, this method MUST return an - * empty array. - * - * @param string $name Case-insensitive header field name. - * @return string[] An array of string values as provided for the given - * header. If the header does not appear in the message, this method MUST - * return an empty array. - */ - public function getHeader($name) - { - return $this->headers->get($name, []); - } - - /** - * Retrieves a comma-separated string of the values for a single header. - * - * This method returns all of the header values of the given - * case-insensitive header name as a string concatenated together using - * a comma. - * - * NOTE: Not all header values may be appropriately represented using - * comma concatenation. For such headers, use getHeader() instead - * and supply your own delimiter when concatenating. - * - * If the header does not appear in the message, this method MUST return - * an empty string. - * - * @param string $name Case-insensitive header field name. - * @return string A string of values as provided for the given header - * concatenated together using a comma. If the header does not appear in - * the message, this method MUST return an empty string. - */ - public function getHeaderLine($name) - { - return implode(',', $this->headers->get($name, [])); - } - - /** - * Return an instance with the provided value replacing the specified header. - * - * While header names are case-insensitive, the casing of the header will - * be preserved by this function, and returned from getHeaders(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new and/or updated header and value. - * - * @param string $name Case-insensitive header field name. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withHeader($name, $value) - { - $clone = clone $this; - $clone->headers->set($name, $value); - - return $clone; - } - - /** - * Return an instance with the specified header appended with the given value. - * - * Existing values for the specified header will be maintained. The new - * value(s) will be appended to the existing list. If the header did not - * exist previously, it will be added. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new header and/or value. - * - * @param string $name Case-insensitive header field name to add. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withAddedHeader($name, $value) - { - $clone = clone $this; - $clone->headers->add($name, $value); - - return $clone; - } - - /** - * Return an instance without the specified header. - * - * Header resolution MUST be done without case-sensitivity. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that removes - * the named header. - * - * @param string $name Case-insensitive header field name to remove. - * @return static - */ - public function withoutHeader($name) - { - $clone = clone $this; - $clone->headers->remove($name); - - return $clone; - } - - /******************************************************************************* - * Body - ******************************************************************************/ - - /** - * Gets the body of the message. - * - * @return StreamInterface Returns the body as a stream. - */ - public function getBody() - { - return $this->body; - } - - /** - * Return an instance with the specified message body. - * - * The body MUST be a StreamInterface object. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return a new instance that has the - * new body stream. - * - * @param StreamInterface $body Body. - * @return static - * @throws \InvalidArgumentException When the body is not valid. - */ - public function withBody(StreamInterface $body) - { - // TODO: Test for invalid body? - $clone = clone $this; - $clone->body = $body; - - return $clone; - } -} diff --git a/Slim/Http/Request.php b/Slim/Http/Request.php deleted file mode 100644 index c90fb5482..000000000 --- a/Slim/Http/Request.php +++ /dev/null @@ -1,1200 +0,0 @@ -get('Cookie', [])); - $serverParams = $environment->all(); - $body = new RequestBody(); - $uploadedFiles = UploadedFile::createFromEnvironment($environment); - - $request = new static($method, $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); - - if ($method === 'POST' && - in_array($request->getMediaType(), ['application/x-www-form-urlencoded', 'multipart/form-data']) - ) { - // parsed body must be $_POST - $request = $request->withParsedBody($_POST); - } - return $request; - } - - /** - * Create new HTTP request. - * - * Adds a host header when none was provided and a host is defined in uri. - * - * @param string $method The request method - * @param UriInterface $uri The request URI object - * @param HeadersInterface $headers The request headers collection - * @param array $cookies The request cookies collection - * @param array $serverParams The server environment variables - * @param StreamInterface $body The request body object - * @param array $uploadedFiles The request uploadedFiles collection - * @throws HttpNotImplementedException on invalid HTTP method - */ - public function __construct( - $method, - UriInterface $uri, - HeadersInterface $headers, - array $cookies, - array $serverParams, - StreamInterface $body, - array $uploadedFiles = [] - ) { - try { - $this->originalMethod = $this->filterMethod($method); - } catch (HttpNotImplementedException $e) { - $this->originalMethod = $method; - } - - $this->uri = $uri; - $this->headers = $headers; - $this->cookies = $cookies; - $this->serverParams = $serverParams; - $this->attributes = new Collection(); - $this->body = $body; - $this->uploadedFiles = $uploadedFiles; - - if (isset($serverParams['SERVER_PROTOCOL'])) { - $this->protocolVersion = str_replace('HTTP/', '', $serverParams['SERVER_PROTOCOL']); - } - - if (!$this->headers->has('Host') || $this->uri->getHost() !== '') { - $this->headers->set('Host', $this->uri->getHost()); - } - - $this->registerMediaTypeParser('application/json', function ($input) { - $result = json_decode($input, true); - if (!is_array($result)) { - return null; - } - return $result; - }); - - $this->registerMediaTypeParser('application/xml', function ($input) { - $backup = libxml_disable_entity_loader(true); - $backup_errors = libxml_use_internal_errors(true); - $result = simplexml_load_string($input); - libxml_disable_entity_loader($backup); - libxml_clear_errors(); - libxml_use_internal_errors($backup_errors); - if ($result === false) { - return null; - } - return $result; - }); - - $this->registerMediaTypeParser('text/xml', function ($input) { - $backup = libxml_disable_entity_loader(true); - $backup_errors = libxml_use_internal_errors(true); - $result = simplexml_load_string($input); - libxml_disable_entity_loader($backup); - libxml_clear_errors(); - libxml_use_internal_errors($backup_errors); - if ($result === false) { - return null; - } - return $result; - }); - - $this->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) { - parse_str($input, $data); - return $data; - }); - - // if the request had an invalid method, we can throw it now - if (isset($e) && $e instanceof HttpNotImplementedException) { - throw $e; - } - } - - /** - * This method is applied to the cloned object - * after PHP performs an initial shallow-copy. This - * method completes a deep-copy by creating new objects - * for the cloned object's internal reference pointers. - */ - public function __clone() - { - $this->headers = clone $this->headers; - $this->attributes = clone $this->attributes; - $this->body = clone $this->body; - } - - /******************************************************************************* - * Method - ******************************************************************************/ - - /** - * Retrieves the HTTP method of the request. - * - * @return string Returns the request method. - */ - public function getMethod() - { - if ($this->method === null) { - $this->method = $this->originalMethod; - $customMethod = $this->getHeaderLine('X-Http-Method-Override'); - - if ($customMethod) { - $this->method = $this->filterMethod($customMethod); - } elseif ($this->originalMethod === 'POST') { - $overrideMethod = $this->filterMethod($this->getParsedBodyParam('_METHOD')); - if ($overrideMethod !== null) { - $this->method = $overrideMethod; - } - - if ($this->getBody()->eof()) { - $this->getBody()->rewind(); - } - } - } - - return $this->method; - } - - /** - * Get the original HTTP method (ignore override). - * - * Note: This method is not part of the PSR-7 standard. - * - * @return string - */ - public function getOriginalMethod() - { - return $this->originalMethod; - } - - /** - * Return an instance with the provided HTTP method. - * - * While HTTP method names are typically all uppercase characters, HTTP - * method names are case-sensitive and thus implementations SHOULD NOT - * modify the given string. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * changed request method. - * - * @param string $method Case-sensitive method. - * @return static - * @throws \InvalidArgumentException for invalid HTTP methods. - */ - public function withMethod($method) - { - $method = $this->filterMethod($method); - $clone = clone $this; - $clone->originalMethod = $method; - $clone->method = $method; - - return $clone; - } - - /** - * Validate the HTTP method - * - * @param null|string $method - * @return null|string - * @throws HttpNotImplementedException on invalid HTTP method. - */ - protected function filterMethod($method) - { - if ($method === null) { - return $method; - } - if (!is_string($method)) { - throw new InvalidArgumentException(sprintf( - 'Unsupported HTTP method; must be a string, received %s', - (is_object($method) ? get_class($method) : gettype($method)) - )); - } - $method = strtoupper($method); - if (preg_match("/^[!#$%&'*+.^_`|~0-9a-z-]+$/i", $method) !== 1) { - $e = new HttpNotImplementedException(sprintf( - 'Unsupported HTTP method "%s" provided', - $method - )); - $e->setRequest($this); - $e->setDetails(['method' => $method]); - throw $e; - } - return $method; - } - - /** - * Does this request use a given method? - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $method HTTP method - * @return bool - */ - public function isMethod($method) - { - return $this->getMethod() === $method; - } - - /** - * Is this a GET request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isGet() - { - return $this->isMethod('GET'); - } - - /** - * Is this a POST request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isPost() - { - return $this->isMethod('POST'); - } - - /** - * Is this a PUT request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isPut() - { - return $this->isMethod('PUT'); - } - - /** - * Is this a PATCH request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isPatch() - { - return $this->isMethod('PATCH'); - } - - /** - * Is this a DELETE request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isDelete() - { - return $this->isMethod('DELETE'); - } - - /** - * Is this a HEAD request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isHead() - { - return $this->isMethod('HEAD'); - } - - /** - * Is this a OPTIONS request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isOptions() - { - return $this->isMethod('OPTIONS'); - } - - /** - * Is this an XHR request? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isXhr() - { - return $this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; - } - - /******************************************************************************* - * URI - ******************************************************************************/ - - /** - * Retrieves the message's request target. - * - * Retrieves the message's request-target either as it will appear (for - * clients), as it appeared at request (for servers), or as it was - * specified for the instance (see withRequestTarget()). - * - * In most cases, this will be the origin-form of the composed URI, - * unless a value was provided to the concrete implementation (see - * withRequestTarget() below). - * - * If no URI is available, and no request-target has been specifically - * provided, this method MUST return the string "/". - * - * @return string - */ - public function getRequestTarget() - { - if ($this->requestTarget) { - return $this->requestTarget; - } - - if ($this->uri === null) { - return '/'; - } - - $path = $this->uri->getPath(); - $path = '/' . ltrim($path, '/'); - - $query = $this->uri->getQuery(); - if ($query) { - $path .= '?' . $query; - } - $this->requestTarget = $path; - - return $this->requestTarget; - } - - /** - * Return an instance with the specific request-target. - * - * If the request needs a non-origin-form request-target — e.g., for - * specifying an absolute-form, authority-form, or asterisk-form — - * this method may be used to create an instance with the specified - * request-target, verbatim. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * changed request target. - * - * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various - * request-target forms allowed in request messages) - * @param mixed $requestTarget - * @return static - * @throws InvalidArgumentException if the request target is invalid - */ - public function withRequestTarget($requestTarget) - { - if (preg_match('#\s#', $requestTarget)) { - throw new InvalidArgumentException( - 'Invalid request target provided; must be a string and cannot contain whitespace' - ); - } - $clone = clone $this; - $clone->requestTarget = $requestTarget; - - return $clone; - } - - /** - * Retrieves the URI instance. - * - * This method MUST return a UriInterface instance. - * - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @return UriInterface Returns a UriInterface instance - * representing the URI of the request. - */ - public function getUri() - { - return $this->uri; - } - - /** - * Returns an instance with the provided URI. - * - * This method MUST update the Host header of the returned request by - * default if the URI contains a host component. If the URI does not - * contain a host component, any pre-existing Host header MUST be carried - * over to the returned request. - * - * You can opt-in to preserving the original state of the Host header by - * setting `$preserveHost` to `true`. When `$preserveHost` is set to - * `true`, this method interacts with the Host header in the following ways: - * - * - If the the Host header is missing or empty, and the new URI contains - * a host component, this method MUST update the Host header in the returned - * request. - * - If the Host header is missing or empty, and the new URI does not contain a - * host component, this method MUST NOT update the Host header in the returned - * request. - * - If a Host header is present and non-empty, this method MUST NOT update - * the Host header in the returned request. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new UriInterface instance. - * - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param UriInterface $uri New request URI to use. - * @param bool $preserveHost Preserve the original state of the Host header. - * @return static - */ - public function withUri(UriInterface $uri, $preserveHost = false) - { - $clone = clone $this; - $clone->uri = $uri; - - if (!$preserveHost) { - if ($uri->getHost() !== '') { - $clone->headers->set('Host', $uri->getHost()); - } - } else { - if ($uri->getHost() !== '' && (!$this->hasHeader('Host') || $this->getHeaderLine('Host') === '')) { - $clone->headers->set('Host', $uri->getHost()); - } - } - - return $clone; - } - - /** - * Get request content type. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return string|null The request content type, if known - */ - public function getContentType() - { - $result = $this->getHeader('Content-Type'); - - return $result ? $result[0] : null; - } - - /** - * Get request media type, if known. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return string|null The request media type, minus content-type params - */ - public function getMediaType() - { - $contentType = $this->getContentType(); - if ($contentType) { - $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); - - return strtolower($contentTypeParts[0]); - } - - return null; - } - - /** - * Get request media type params, if known. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return array - */ - public function getMediaTypeParams() - { - $contentType = $this->getContentType(); - $contentTypeParams = []; - if ($contentType) { - $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); - $contentTypePartsLength = count($contentTypeParts); - for ($i = 1; $i < $contentTypePartsLength; $i++) { - $paramParts = explode('=', $contentTypeParts[$i]); - $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; - } - } - - return $contentTypeParams; - } - - /** - * Get request content character set, if known. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return string|null - */ - public function getContentCharset() - { - $mediaTypeParams = $this->getMediaTypeParams(); - if (isset($mediaTypeParams['charset'])) { - return $mediaTypeParams['charset']; - } - - return null; - } - - /** - * Get request content length, if known. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return int|null - */ - public function getContentLength() - { - $result = $this->headers->get('Content-Length'); - - return $result ? (int)$result[0] : null; - } - - /******************************************************************************* - * Cookies - ******************************************************************************/ - - /** - * Retrieve cookies. - * - * Retrieves cookies sent by the client to the server. - * - * The data MUST be compatible with the structure of the $_COOKIE - * superglobal. - * - * @return array - */ - public function getCookieParams() - { - return $this->cookies; - } - - /** - * Fetch cookie value from cookies sent by the client to the server. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $key The attribute name. - * @param mixed $default Default value to return if the attribute does not exist. - * - * @return mixed - */ - public function getCookieParam($key, $default = null) - { - $cookies = $this->getCookieParams(); - $result = $default; - if (isset($cookies[$key])) { - $result = $cookies[$key]; - } - - return $result; - } - - /** - * Return an instance with the specified cookies. - * - * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST - * be compatible with the structure of $_COOKIE. Typically, this data will - * be injected at instantiation. - * - * This method MUST NOT update the related Cookie header of the request - * instance, nor related values in the server params. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated cookie values. - * - * @param array $cookies Array of key/value pairs representing cookies. - * @return static - */ - public function withCookieParams(array $cookies) - { - $clone = clone $this; - $clone->cookies = $cookies; - - return $clone; - } - - /******************************************************************************* - * Query Params - ******************************************************************************/ - - /** - * Retrieve query string arguments. - * - * Retrieves the deserialized query string arguments, if any. - * - * Note: the query params might not be in sync with the URI or server - * params. If you need to ensure you are only getting the original - * values, you may need to parse the query string from `getUri()->getQuery()` - * or from the `QUERY_STRING` server param. - * - * @return array - */ - public function getQueryParams() - { - if (is_array($this->queryParams)) { - return $this->queryParams; - } - - if ($this->uri === null) { - return []; - } - - parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data - - return $this->queryParams; - } - - /** - * Return an instance with the specified query string arguments. - * - * These values SHOULD remain immutable over the course of the incoming - * request. They MAY be injected during instantiation, such as from PHP's - * $_GET superglobal, or MAY be derived from some other value such as the - * URI. In cases where the arguments are parsed from the URI, the data - * MUST be compatible with what PHP's parse_str() would return for - * purposes of how duplicate query parameters are handled, and how nested - * sets are handled. - * - * Setting query string arguments MUST NOT change the URI stored by the - * request, nor the values in the server params. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated query string arguments. - * - * @param array $query Array of query string arguments, typically from - * $_GET. - * @return static - */ - public function withQueryParams(array $query) - { - $clone = clone $this; - $clone->queryParams = $query; - - return $clone; - } - - /******************************************************************************* - * File Params - ******************************************************************************/ - - /** - * Retrieve normalized file upload data. - * - * This method returns upload metadata in a normalized tree, with each leaf - * an instance of Psr\Http\Message\UploadedFileInterface. - * - * These values MAY be prepared from $_FILES or the message body during - * instantiation, or MAY be injected via withUploadedFiles(). - * - * @return array An array tree of UploadedFileInterface instances; an empty - * array MUST be returned if no data is present. - */ - public function getUploadedFiles() - { - return $this->uploadedFiles; - } - - /** - * Create a new instance with the specified uploaded files. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated body parameters. - * - * @param array $uploadedFiles An array tree of UploadedFileInterface instances. - * @return static - * @throws \InvalidArgumentException if an invalid structure is provided. - */ - public function withUploadedFiles(array $uploadedFiles) - { - $clone = clone $this; - $clone->uploadedFiles = $uploadedFiles; - - return $clone; - } - - /******************************************************************************* - * Server Params - ******************************************************************************/ - - /** - * Retrieve server parameters. - * - * Retrieves data related to the incoming request environment, - * typically derived from PHP's $_SERVER superglobal. The data IS NOT - * REQUIRED to originate from $_SERVER. - * - * @return array - */ - public function getServerParams() - { - return $this->serverParams; - } - - /** - * Retrieve a server parameter. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getServerParam($key, $default = null) - { - $serverParams = $this->getServerParams(); - - return isset($serverParams[$key]) ? $serverParams[$key] : $default; - } - - /******************************************************************************* - * Attributes - ******************************************************************************/ - - /** - * Retrieve attributes derived from the request. - * - * The request "attributes" may be used to allow injection of any - * parameters derived from the request: e.g., the results of path - * match operations; the results of decrypting cookies; the results of - * deserializing non-form-encoded message bodies; etc. Attributes - * will be application and request specific, and CAN be mutable. - * - * @return array Attributes derived from the request. - */ - public function getAttributes() - { - return $this->attributes->all(); - } - - /** - * Retrieve a single derived request attribute. - * - * Retrieves a single derived request attribute as described in - * getAttributes(). If the attribute has not been previously set, returns - * the default value as provided. - * - * This method obviates the need for a hasAttribute() method, as it allows - * specifying a default value to return if the attribute is not found. - * - * @see getAttributes() - * @param string $name The attribute name. - * @param mixed $default Default value to return if the attribute does not exist. - * @return mixed - */ - public function getAttribute($name, $default = null) - { - return $this->attributes->get($name, $default); - } - - /** - * Return an instance with the specified derived request attribute. - * - * This method allows setting a single derived request attribute as - * described in getAttributes(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated attribute. - * - * @see getAttributes() - * @param string $name The attribute name. - * @param mixed $value The value of the attribute. - * @return static - */ - public function withAttribute($name, $value) - { - $clone = clone $this; - $clone->attributes->set($name, $value); - - return $clone; - } - - /** - * Create a new instance with the specified derived request attributes. - * - * Note: This method is not part of the PSR-7 standard. - * - * This method allows setting all new derived request attributes as - * described in getAttributes(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return a new instance that has the - * updated attributes. - * - * @param array $attributes New attributes - * @return static - */ - public function withAttributes(array $attributes) - { - $clone = clone $this; - $clone->attributes = new Collection($attributes); - - return $clone; - } - - /** - * Return an instance that removes the specified derived request attribute. - * - * This method allows removing a single derived request attribute as - * described in getAttributes(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that removes - * the attribute. - * - * @see getAttributes() - * @param string $name The attribute name. - * @return static - */ - public function withoutAttribute($name) - { - $clone = clone $this; - $clone->attributes->remove($name); - - return $clone; - } - - /******************************************************************************* - * Body - ******************************************************************************/ - - /** - * Retrieve any parameters provided in the request body. - * - * If the request Content-Type is either application/x-www-form-urlencoded - * or multipart/form-data, and the request method is POST, this method MUST - * return the contents of $_POST. - * - * Otherwise, this method may return any results of deserializing - * the request body content; as parsing returns structured content, the - * potential types MUST be arrays or objects only. A null value indicates - * the absence of body content. - * - * @return null|array|object The deserialized body parameters, if any. - * These will typically be an array or object. - * @throws RuntimeException if the request body media type parser returns an invalid value - */ - public function getParsedBody() - { - if ($this->bodyParsed !== false) { - return $this->bodyParsed; - } - - if (!$this->body) { - return null; - } - - $mediaType = $this->getMediaType(); - - // look for a media type with a structured syntax suffix (RFC 6839) - $parts = explode('+', $mediaType); - if (count($parts) >= 2) { - $mediaType = 'application/' . $parts[count($parts)-1]; - } - - if (isset($this->bodyParsers[$mediaType]) === true) { - $body = (string)$this->getBody(); - $parsed = $this->bodyParsers[$mediaType]($body); - - if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) { - throw new RuntimeException( - 'Request body media type parser return value must be an array, an object, or null' - ); - } - $this->bodyParsed = $parsed; - return $this->bodyParsed; - } - - return null; - } - - /** - * Return an instance with the specified body parameters. - * - * These MAY be injected during instantiation. - * - * If the request Content-Type is either application/x-www-form-urlencoded - * or multipart/form-data, and the request method is POST, use this method - * ONLY to inject the contents of $_POST. - * - * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of - * deserializing the request body content. Deserialization/parsing returns - * structured data, and, as such, this method ONLY accepts arrays or objects, - * or a null value if nothing was available to parse. - * - * As an example, if content negotiation determines that the request data - * is a JSON payload, this method could be used to create a request - * instance with the deserialized parameters. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated body parameters. - * - * @param null|array|object $data The deserialized body data. This will - * typically be in an array or object. - * @return static - * @throws \InvalidArgumentException if an unsupported argument type is - * provided. - */ - public function withParsedBody($data) - { - if (!is_null($data) && !is_object($data) && !is_array($data)) { - throw new InvalidArgumentException('Parsed body value must be an array, an object, or null'); - } - - $clone = clone $this; - $clone->bodyParsed = $data; - - return $clone; - } - - /** - * Force Body to be parsed again. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return $this - */ - public function reparseBody() - { - $this->bodyParsed = false; - - return $this; - } - - /** - * Register media type parser. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $mediaType A HTTP media type (excluding content-type - * params). - * @param callable $callable A callable that returns parsed contents for - * media type. - */ - public function registerMediaTypeParser($mediaType, callable $callable) - { - if ($callable instanceof Closure) { - $callable = $callable->bindTo($this); - } - $this->bodyParsers[(string)$mediaType] = $callable; - } - - /******************************************************************************* - * Parameters (e.g., POST and GET data) - ******************************************************************************/ - - /** - * Fetch request parameter value from body or query string (in that order). - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $key The parameter key. - * @param string $default The default value. - * - * @return mixed The parameter value. - */ - public function getParam($key, $default = null) - { - $postParams = $this->getParsedBody(); - $getParams = $this->getQueryParams(); - $result = $default; - if (is_array($postParams) && isset($postParams[$key])) { - $result = $postParams[$key]; - } elseif (is_object($postParams) && property_exists($postParams, $key)) { - $result = $postParams->$key; - } elseif (isset($getParams[$key])) { - $result = $getParams[$key]; - } - - return $result; - } - - /** - * Fetch parameter value from request body. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $key - * @param mixed $default - * - * @return mixed - */ - public function getParsedBodyParam($key, $default = null) - { - $postParams = $this->getParsedBody(); - $result = $default; - if (is_array($postParams) && isset($postParams[$key])) { - $result = $postParams[$key]; - } elseif (is_object($postParams) && property_exists($postParams, $key)) { - $result = $postParams->$key; - } - - return $result; - } - - /** - * Fetch parameter value from query string. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param string $key - * @param mixed $default - * - * @return mixed - */ - public function getQueryParam($key, $default = null) - { - $getParams = $this->getQueryParams(); - $result = $default; - if (isset($getParams[$key])) { - $result = $getParams[$key]; - } - - return $result; - } - - /** - * Fetch associative array of body and query string parameters. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return array - */ - public function getParams() - { - $params = $this->getQueryParams(); - $postParams = $this->getParsedBody(); - if ($postParams) { - $params = array_merge($params, (array)$postParams); - } - - return $params; - } -} diff --git a/Slim/Http/RequestBody.php b/Slim/Http/RequestBody.php deleted file mode 100644 index 50887fddc..000000000 --- a/Slim/Http/RequestBody.php +++ /dev/null @@ -1,27 +0,0 @@ - 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - //Successful 2xx - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', - 208 => 'Already Reported', - 226 => 'IM Used', - //Redirection 3xx - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 306 => '(Unused)', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', - //Client Error 4xx - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', - 421 => 'Misdirected Request', - 422 => 'Unprocessable Entity', - 423 => 'Locked', - 424 => 'Failed Dependency', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - 444 => 'Connection Closed Without Response', - 451 => 'Unavailable For Legal Reasons', - 499 => 'Client Closed Request', - //Server Error 5xx - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 506 => 'Variant Also Negotiates', - 507 => 'Insufficient Storage', - 508 => 'Loop Detected', - 510 => 'Not Extended', - 511 => 'Network Authentication Required', - 599 => 'Network Connect Timeout Error', - ]; - - /** - * EOL characters used for HTTP response. - * - * @var string - */ - const EOL = "\r\n"; - - /** - * Create new HTTP response. - * - * @param int $status The response status code. - * @param HeadersInterface|null $headers The response headers. - * @param StreamInterface|null $body The response body. - */ - public function __construct($status = 200, HeadersInterface $headers = null, StreamInterface $body = null) - { - $this->status = $this->filterStatus($status); - $this->headers = $headers ? $headers : new Headers(); - $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); - } - - /** - * This method is applied to the cloned object - * after PHP performs an initial shallow-copy. This - * method completes a deep-copy by creating new objects - * for the cloned object's internal reference pointers. - */ - public function __clone() - { - $this->headers = clone $this->headers; - } - - /******************************************************************************* - * Status - ******************************************************************************/ - - /** - * Gets the response status code. - * - * The status code is a 3-digit integer result code of the server's attempt - * to understand and satisfy the request. - * - * @return int Status code. - */ - public function getStatusCode() - { - return $this->status; - } - - /** - * Return an instance with the specified status code and, optionally, reason phrase. - * - * If no reason phrase is specified, implementations MAY choose to default - * to the RFC 7231 or IANA recommended reason phrase for the response's - * status code. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated status and reason phrase. - * - * @link http://tools.ietf.org/html/rfc7231#section-6 - * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - * @param int $code The 3-digit integer result code to set. - * @param string $reasonPhrase The reason phrase to use with the - * provided status code; if none is provided, implementations MAY - * use the defaults as suggested in the HTTP specification. - * @return static - * @throws \InvalidArgumentException For invalid status code arguments. - */ - public function withStatus($code, $reasonPhrase = '') - { - $code = $this->filterStatus($code); - - if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { - throw new InvalidArgumentException('ReasonPhrase must be a string'); - } - - $clone = clone $this; - $clone->status = $code; - if ($reasonPhrase === '' && isset(static::$messages[$code])) { - $reasonPhrase = static::$messages[$code]; - } - - if ($reasonPhrase === '') { - throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); - } - - $clone->reasonPhrase = $reasonPhrase; - - return $clone; - } - - /** - * Filter HTTP status code. - * - * @param int $status HTTP status code. - * @return int - * @throws \InvalidArgumentException If an invalid HTTP status code is provided. - */ - protected function filterStatus($status) - { - if (!is_integer($status) || $status<100 || $status>599) { - throw new InvalidArgumentException('Invalid HTTP status code'); - } - - return $status; - } - - /** - * Gets the response reason phrase associated with the status code. - * - * Because a reason phrase is not a required element in a response - * status line, the reason phrase value MAY be null. Implementations MAY - * choose to return the default RFC 7231 recommended reason phrase (or those - * listed in the IANA HTTP Status Code Registry) for the response's - * status code. - * - * @link http://tools.ietf.org/html/rfc7231#section-6 - * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - * @return string Reason phrase; must return an empty string if none present. - */ - public function getReasonPhrase() - { - if ($this->reasonPhrase) { - return $this->reasonPhrase; - } - if (isset(static::$messages[$this->status])) { - return static::$messages[$this->status]; - } - return ''; - } - - /******************************************************************************* - * Body - ******************************************************************************/ - - /** - * Write data to the response body. - * - * Note: This method is not part of the PSR-7 standard. - * - * Proxies to the underlying stream and writes the provided data to it. - * - * @param string $data - * @return $this - */ - public function write($data) - { - $this->getBody()->write($data); - - return $this; - } - - /******************************************************************************* - * Response Helpers - ******************************************************************************/ - - /** - * Redirect. - * - * Note: This method is not part of the PSR-7 standard. - * - * This method prepares the response object to return an HTTP Redirect - * response to the client. - * - * @param string|UriInterface $url The redirect destination. - * @param int|null $status The redirect HTTP status code. - * @return static - */ - public function withRedirect($url, $status = null) - { - $responseWithRedirect = $this->withHeader('Location', (string)$url); - - if (is_null($status) && $this->getStatusCode() === 200) { - $status = 302; - } - - if (!is_null($status)) { - return $responseWithRedirect->withStatus($status); - } - - return $responseWithRedirect; - } - - /** - * Json. - * - * Note: This method is not part of the PSR-7 standard. - * - * This method prepares the response object to return an HTTP Json - * response to the client. - * - * @param mixed $data The data - * @param int $status The HTTP status code. - * @param int $encodingOptions Json encoding options - * @throws \RuntimeException - * @return static - */ - public function withJson($data, $status = null, $encodingOptions = 0) - { - $response = $this->withBody(new Body(fopen('php://temp', 'r+'))); - $response->body->write($json = json_encode($data, $encodingOptions)); - - // Ensure that the json encoding passed successfully - if ($json === false) { - throw new \RuntimeException(json_last_error_msg(), json_last_error()); - } - - $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8'); - if (isset($status)) { - return $responseWithJson->withStatus($status); - } - return $responseWithJson; - } - - /** - * Is this response empty? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isEmpty() - { - return in_array($this->getStatusCode(), [204, 205, 304]); - } - - /** - * Is this response informational? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isInformational() - { - return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; - } - - /** - * Is this response OK? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isOk() - { - return $this->getStatusCode() === 200; - } - - /** - * Is this response successful? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isSuccessful() - { - return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; - } - - /** - * Is this response a redirect? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isRedirect() - { - return in_array($this->getStatusCode(), [301, 302, 303, 307]); - } - - /** - * Is this response a redirection? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isRedirection() - { - return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; - } - - /** - * Is this response forbidden? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - * @api - */ - public function isForbidden() - { - return $this->getStatusCode() === 403; - } - - /** - * Is this response not Found? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isNotFound() - { - return $this->getStatusCode() === 404; - } - - /** - * Is this response a client error? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isClientError() - { - return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; - } - - /** - * Is this response a server error? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - public function isServerError() - { - return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; - } - - /** - * Convert response to string. - * - * Note: This method is not part of the PSR-7 standard. - * - * @return string - */ - public function __toString() - { - $output = sprintf( - 'HTTP/%s %s %s', - $this->getProtocolVersion(), - $this->getStatusCode(), - $this->getReasonPhrase() - ); - $output .= Response::EOL; - foreach ($this->getHeaders() as $name => $values) { - $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . Response::EOL; - } - $output .= Response::EOL; - $output .= (string)$this->getBody(); - - return $output; - } -} diff --git a/Slim/Http/Stream.php b/Slim/Http/Stream.php deleted file mode 100644 index 27c7a7645..000000000 --- a/Slim/Http/Stream.php +++ /dev/null @@ -1,450 +0,0 @@ - ['r', 'r+', 'w+', 'a+', 'x+', 'c+'], - 'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'], - ]; - - /** - * The underlying stream resource - * - * @var resource - */ - protected $stream; - - /** - * Stream metadata - * - * @var array - */ - protected $meta; - - /** - * Is this stream readable? - * - * @var bool - */ - protected $readable; - - /** - * Is this stream writable? - * - * @var bool - */ - protected $writable; - - /** - * Is this stream seekable? - * - * @var bool - */ - protected $seekable; - - /** - * The size of the stream if known - * - * @var null|int - */ - protected $size; - - /** - * Is this stream a pipe? - * - * @var bool - */ - protected $isPipe; - - /** - * Create a new Stream. - * - * @param resource $stream A PHP resource handle. - * - * @throws InvalidArgumentException If argument is not a resource. - */ - public function __construct($stream) - { - $this->attach($stream); - } - - /** - * Get stream metadata as an associative array or retrieve a specific key. - * - * The keys returned are identical to the keys returned from PHP's - * stream_get_meta_data() function. - * - * @link http://php.net/manual/en/function.stream-get-meta-data.php - * - * @param string $key Specific metadata to retrieve. - * - * @return array|mixed|null Returns an associative array if no key is - * provided. Returns a specific key value if a key is provided and the - * value is found, or null if the key is not found. - */ - public function getMetadata($key = null) - { - $this->meta = stream_get_meta_data($this->stream); - if (is_null($key) === true) { - return $this->meta; - } - - return isset($this->meta[$key]) ? $this->meta[$key] : null; - } - - /** - * Is a resource attached to this stream? - * - * Note: This method is not part of the PSR-7 standard. - * - * @return bool - */ - protected function isAttached() - { - return is_resource($this->stream); - } - - /** - * Attach new resource to this object. - * - * Note: This method is not part of the PSR-7 standard. - * - * @param resource $newStream A PHP resource handle. - * - * @throws InvalidArgumentException If argument is not a valid PHP resource. - */ - protected function attach($newStream) - { - if (is_resource($newStream) === false) { - throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); - } - - if ($this->isAttached() === true) { - $this->detach(); - } - - $this->stream = $newStream; - } - - /** - * Separates any underlying resources from the stream. - * - * After the stream has been detached, the stream is in an unusable state. - * - * @return resource|null Underlying PHP stream, if any - */ - public function detach() - { - $oldResource = $this->stream; - $this->stream = null; - $this->meta = null; - $this->readable = null; - $this->writable = null; - $this->seekable = null; - $this->size = null; - $this->isPipe = null; - - return $oldResource; - } - - /** - * Reads all data from the stream into a string, from the beginning to end. - * - * This method MUST attempt to seek to the beginning of the stream before - * reading data and read the stream until the end is reached. - * - * Warning: This could attempt to load a large amount of data into memory. - * - * This method MUST NOT raise an exception in order to conform with PHP's - * string casting operations. - * - * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring - * @return string - */ - public function __toString() - { - if (!$this->isAttached()) { - return ''; - } - - try { - $this->rewind(); - return $this->getContents(); - } catch (RuntimeException $e) { - return ''; - } - } - - /** - * Closes the stream and any underlying resources. - */ - public function close() - { - if ($this->isAttached() === true) { - if ($this->isPipe()) { - pclose($this->stream); - } else { - fclose($this->stream); - } - } - - $this->detach(); - } - - /** - * Get the size of the stream if known. - * - * @return int|null Returns the size in bytes if known, or null if unknown. - */ - public function getSize() - { - if (!$this->size && $this->isAttached() === true) { - $stats = fstat($this->stream); - $this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null; - } - - return $this->size; - } - - /** - * Returns the current position of the file read/write pointer - * - * @return int Position of the file pointer - * - * @throws RuntimeException on error. - */ - public function tell() - { - if (!$this->isAttached() || ($position = ftell($this->stream)) === false || $this->isPipe()) { - throw new RuntimeException('Could not get the position of the pointer in stream'); - } - - return $position; - } - - /** - * Returns true if the stream is at the end of the stream. - * - * @return bool - */ - public function eof() - { - return $this->isAttached() ? feof($this->stream) : true; - } - - /** - * Returns whether or not the stream is readable. - * - * @return bool - */ - public function isReadable() - { - if ($this->readable === null) { - if ($this->isPipe()) { - $this->readable = true; - } else { - $this->readable = false; - if ($this->isAttached()) { - $meta = $this->getMetadata(); - foreach (self::$modes['readable'] as $mode) { - if (strpos($meta['mode'], $mode) === 0) { - $this->readable = true; - break; - } - } - } - } - } - - return $this->readable; - } - - /** - * Returns whether or not the stream is writable. - * - * @return bool - */ - public function isWritable() - { - if ($this->writable === null) { - $this->writable = false; - if ($this->isAttached()) { - $meta = $this->getMetadata(); - foreach (self::$modes['writable'] as $mode) { - if (strpos($meta['mode'], $mode) === 0) { - $this->writable = true; - break; - } - } - } - } - - return $this->writable; - } - - /** - * Returns whether or not the stream is seekable. - * - * @return bool - */ - public function isSeekable() - { - if ($this->seekable === null) { - $this->seekable = false; - if ($this->isAttached()) { - $meta = $this->getMetadata(); - $this->seekable = !$this->isPipe() && $meta['seekable']; - } - } - - return $this->seekable; - } - - /** - * Seek to a position in the stream. - * - * @link http://www.php.net/manual/en/function.fseek.php - * - * @param int $offset Stream offset - * @param int $whence Specifies how the cursor position will be calculated - * based on the seek offset. Valid values are identical to the built-in - * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to - * offset bytes SEEK_CUR: Set position to current location plus offset - * SEEK_END: Set position to end-of-stream plus offset. - * - * @throws RuntimeException on failure. - */ - public function seek($offset, $whence = SEEK_SET) - { - // Note that fseek returns 0 on success! - if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { - throw new RuntimeException('Could not seek in stream'); - } - } - - /** - * Seek to the beginning of the stream. - * - * If the stream is not seekable, this method will raise an exception; - * otherwise, it will perform a seek(0). - * - * @see seek() - * - * @link http://www.php.net/manual/en/function.fseek.php - * - * @throws RuntimeException on failure. - */ - public function rewind() - { - if (!$this->isSeekable() || rewind($this->stream) === false) { - throw new RuntimeException('Could not rewind stream'); - } - } - - /** - * Read data from the stream. - * - * @param int $length Read up to $length bytes from the object and return - * them. Fewer than $length bytes may be returned if underlying stream - * call returns fewer bytes. - * - * @return string Returns the data read from the stream, or an empty string - * if no bytes are available. - * - * @throws RuntimeException if an error occurs. - */ - public function read($length) - { - if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { - throw new RuntimeException('Could not read from stream'); - } - - return $data; - } - - /** - * Write data to the stream. - * - * @param string $string The string that is to be written. - * - * @return int Returns the number of bytes written to the stream. - * - * @throws RuntimeException on failure. - */ - public function write($string) - { - if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { - throw new RuntimeException('Could not write to stream'); - } - - // reset size so that it will be recalculated on next call to getSize() - $this->size = null; - - return $written; - } - - /** - * Returns the remaining contents in a string - * - * @return string - * - * @throws RuntimeException if unable to read or an error occurs while - * reading. - */ - public function getContents() - { - if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { - throw new RuntimeException('Could not get contents of stream'); - } - - return $contents; - } - - /** - * Returns whether or not the stream is a pipe. - * - * @return bool - */ - public function isPipe() - { - if ($this->isPipe === null) { - $this->isPipe = false; - if ($this->isAttached()) { - $mode = fstat($this->stream)['mode']; - $this->isPipe = ($mode & self::FSTAT_MODE_S_IFIFO) !== 0; - } - } - - return $this->isPipe; - } -} diff --git a/Slim/Http/UploadedFile.php b/Slim/Http/UploadedFile.php deleted file mode 100644 index ae5dfb65e..000000000 --- a/Slim/Http/UploadedFile.php +++ /dev/null @@ -1,327 +0,0 @@ -has('slim.files')) { - return $env['slim.files']; - } elseif (isset($_FILES)) { - return static::parseUploadedFiles($_FILES); - } - - return []; - } - - /** - * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. - * - * @param array $uploadedFiles The non-normalized tree of uploaded file data. - * - * @return array A normalized tree of UploadedFile instances. - */ - private static function parseUploadedFiles(array $uploadedFiles) - { - $parsed = []; - foreach ($uploadedFiles as $field => $uploadedFile) { - if (!isset($uploadedFile['error'])) { - if (is_array($uploadedFile)) { - $parsed[$field] = static::parseUploadedFiles($uploadedFile); - } - continue; - } - - $parsed[$field] = []; - if (!is_array($uploadedFile['error'])) { - $parsed[$field] = new static( - $uploadedFile['tmp_name'], - isset($uploadedFile['name']) ? $uploadedFile['name'] : null, - isset($uploadedFile['type']) ? $uploadedFile['type'] : null, - isset($uploadedFile['size']) ? $uploadedFile['size'] : null, - $uploadedFile['error'], - true - ); - } else { - $subArray = []; - foreach ($uploadedFile['error'] as $fileIdx => $error) { - // normalise subarray and re-parse to move the input's keyname up a level - $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; - $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; - $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; - $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; - $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; - - $parsed[$field] = static::parseUploadedFiles($subArray); - } - } - } - - return $parsed; - } - - /** - * Construct a new UploadedFile instance. - * - * @param string $file The full path to the uploaded file provided by the client. - * @param string|null $name The file name. - * @param string|null $type The file media type. - * @param int|null $size The file size in bytes. - * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. - * @param bool $sapi Indicates if the upload is in a SAPI environment. - */ - public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) - { - $this->file = $file; - $this->name = $name; - $this->type = $type; - $this->size = $size; - $this->error = $error; - $this->sapi = $sapi; - } - - /** - * Retrieve a stream representing the uploaded file. - * - * This method MUST return a StreamInterface instance, representing the - * uploaded file. The purpose of this method is to allow utilizing native PHP - * stream functionality to manipulate the file upload, such as - * stream_copy_to_stream() (though the result will need to be decorated in a - * native PHP stream wrapper to work with such functions). - * - * If the moveTo() method has been called previously, this method MUST raise - * an exception. - * - * @return StreamInterface Stream representation of the uploaded file. - * @throws \RuntimeException in cases when no stream is available or can be - * created. - */ - public function getStream() - { - if ($this->moved) { - throw new \RuntimeException(sprintf('Uploaded file %1s has already been moved', $this->name)); - } - if ($this->stream === null) { - $this->stream = new Stream(fopen($this->file, 'r')); - } - - return $this->stream; - } - - /** - * Move the uploaded file to a new location. - * - * Use this method as an alternative to move_uploaded_file(). This method is - * guaranteed to work in both SAPI and non-SAPI environments. - * Implementations must determine which environment they are in, and use the - * appropriate method (move_uploaded_file(), rename(), or a stream - * operation) to perform the operation. - * - * $targetPath may be an absolute path, or a relative path. If it is a - * relative path, resolution should be the same as used by PHP's rename() - * function. - * - * The original file or stream MUST be removed on completion. - * - * If this method is called more than once, any subsequent calls MUST raise - * an exception. - * - * When used in an SAPI environment where $_FILES is populated, when writing - * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be - * used to ensure permissions and upload status are verified correctly. - * - * If you wish to move to a stream, use getStream(), as SAPI operations - * cannot guarantee writing to stream destinations. - * - * @see http://php.net/is_uploaded_file - * @see http://php.net/move_uploaded_file - * - * @param string $targetPath Path to which to move the uploaded file. - * - * @throws InvalidArgumentException if the $path specified is invalid. - * @throws RuntimeException on any error during the move operation, or on - * the second or subsequent call to the method. - */ - public function moveTo($targetPath) - { - if ($this->moved) { - throw new RuntimeException('Uploaded file already moved'); - } - - $targetIsStream = strpos($targetPath, '://') > 0; - if (!$targetIsStream && !is_writable(dirname($targetPath))) { - throw new InvalidArgumentException('Upload target path is not writable'); - } - - if ($targetIsStream) { - if (!copy($this->file, $targetPath)) { - throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); - } - if (!unlink($this->file)) { - throw new RuntimeException(sprintf('Error removing uploaded file %1s', $this->name)); - } - } elseif ($this->sapi) { - if (!is_uploaded_file($this->file)) { - throw new RuntimeException(sprintf('%1s is not a valid uploaded file', $this->file)); - } - - if (!move_uploaded_file($this->file, $targetPath)) { - throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); - } - } else { - if (!rename($this->file, $targetPath)) { - throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); - } - } - - $this->moved = true; - } - - /** - * Retrieve the error associated with the uploaded file. - * - * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. - * - * If the file was uploaded successfully, this method MUST return - * UPLOAD_ERR_OK. - * - * Implementations SHOULD return the value stored in the "error" key of - * the file in the $_FILES array. - * - * @see http://php.net/manual/en/features.file-upload.errors.php - * - * @return int One of PHP's UPLOAD_ERR_XXX constants. - */ - public function getError() - { - return $this->error; - } - - /** - * Retrieve the filename sent by the client. - * - * Do not trust the value returned by this method. A client could send - * a malicious filename with the intention to corrupt or hack your - * application. - * - * Implementations SHOULD return the value stored in the "name" key of - * the file in the $_FILES array. - * - * @return string|null The filename sent by the client or null if none - * was provided. - */ - public function getClientFilename() - { - return $this->name; - } - - /** - * Retrieve the media type sent by the client. - * - * Do not trust the value returned by this method. A client could send - * a malicious media type with the intention to corrupt or hack your - * application. - * - * Implementations SHOULD return the value stored in the "type" key of - * the file in the $_FILES array. - * - * @return string|null The media type sent by the client or null if none - * was provided. - */ - public function getClientMediaType() - { - return $this->type; - } - - /** - * Retrieve the file size. - * - * Implementations SHOULD return the value stored in the "size" key of - * the file in the $_FILES array if available, as PHP calculates this based - * on the actual size transmitted. - * - * @return int|null The file size in bytes or null if unknown. - */ - public function getSize() - { - return $this->size; - } -} diff --git a/Slim/Http/Uri.php b/Slim/Http/Uri.php deleted file mode 100644 index fb51a49b9..000000000 --- a/Slim/Http/Uri.php +++ /dev/null @@ -1,762 +0,0 @@ -scheme = $this->filterScheme($scheme); - $this->host = $host; - $this->port = $this->filterPort($port); - $this->path = empty($path) ? '/' : $this->filterPath($path); - $this->query = $this->filterQuery($query); - $this->fragment = $this->filterQuery($fragment); - $this->user = $user; - $this->password = $password; - } - - /** - * Create new Uri from string. - * - * @param string $uri Complete Uri string - * (i.e., https://user:pass@host:443/path?query). - * - * @return self - */ - public static function createFromString($uri) - { - if (!is_string($uri) && !method_exists($uri, '__toString')) { - throw new InvalidArgumentException('Uri must be a string'); - } - - $parts = parse_url($uri); - $scheme = isset($parts['scheme']) ? $parts['scheme'] : ''; - $user = isset($parts['user']) ? $parts['user'] : ''; - $pass = isset($parts['pass']) ? $parts['pass'] : ''; - $host = isset($parts['host']) ? $parts['host'] : ''; - $port = isset($parts['port']) ? $parts['port'] : null; - $path = isset($parts['path']) ? $parts['path'] : ''; - $query = isset($parts['query']) ? $parts['query'] : ''; - $fragment = isset($parts['fragment']) ? $parts['fragment'] : ''; - - return new static($scheme, $host, $port, $path, $query, $fragment, $user, $pass); - } - - /** - * Create new Uri from environment. - * - * @param Environment $env - * - * @return self - */ - public static function createFromEnvironment(Environment $env) - { - // Scheme - $isSecure = $env->get('HTTPS'); - $scheme = (empty($isSecure) || $isSecure === 'off') ? 'http' : 'https'; - - // Authority: Username and password - $username = $env->get('PHP_AUTH_USER', ''); - $password = $env->get('PHP_AUTH_PW', ''); - - // Authority: Host - if ($env->has('HTTP_HOST')) { - $host = $env->get('HTTP_HOST'); - } else { - $host = $env->get('SERVER_NAME'); - } - - // Authority: Port - $port = (int)$env->get('SERVER_PORT', 80); - if (preg_match('/^(\[[a-fA-F0-9:.]+\])(:\d+)?\z/', $host, $matches)) { - $host = $matches[1]; - - if ($matches[2]) { - $port = (int) substr($matches[2], 1); - } - } else { - $pos = strpos($host, ':'); - if ($pos !== false) { - $port = (int) substr($host, $pos + 1); - $host = strstr($host, ':', true); - } - } - - // Path - $requestScriptName = parse_url($env->get('SCRIPT_NAME'), PHP_URL_PATH); - $requestScriptDir = dirname($requestScriptName); - - // parse_url() requires a full URL. As we don't extract the domain name or scheme, - // we use a stand-in. - $requestUri = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_PATH); - $requestUri = rawurldecode($requestUri); - - $basePath = ''; - $virtualPath = $requestUri; - if (stripos($requestUri, $requestScriptName) === 0) { - $basePath = $requestScriptName; - } elseif ($requestScriptDir !== '/' && stripos($requestUri, $requestScriptDir) === 0) { - $basePath = $requestScriptDir; - } - - if ($basePath) { - $virtualPath = ltrim(substr($requestUri, strlen($basePath)), '/'); - } - - // Query string - $queryString = $env->get('QUERY_STRING', ''); - if ($queryString === '') { - $queryString = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_QUERY); - } - - // Fragment - $fragment = ''; - - // Build Uri - $uri = new static($scheme, $host, $port, $virtualPath, $queryString, $fragment, $username, $password); - - return $uri; - } - - /******************************************************************************** - * Scheme - *******************************************************************************/ - - /** - * Retrieve the scheme component of the URI. - * - * If no scheme is present, this method MUST return an empty string. - * - * The value returned MUST be normalized to lowercase, per RFC 3986 - * Section 3.1. - * - * The trailing ":" character is not part of the scheme and MUST NOT be - * added. - * - * @see https://tools.ietf.org/html/rfc3986#section-3.1 - * @return string The URI scheme. - */ - public function getScheme() - { - return $this->scheme; - } - - /** - * Return an instance with the specified scheme. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified scheme. - * - * Implementations MUST support the schemes "http" and "https" case - * insensitively, and MAY accommodate other schemes if required. - * - * An empty scheme is equivalent to removing the scheme. - * - * @param string $scheme The scheme to use with the new instance. - * @return self A new instance with the specified scheme. - * @throws \InvalidArgumentException for invalid or unsupported schemes. - */ - public function withScheme($scheme) - { - $scheme = $this->filterScheme($scheme); - $clone = clone $this; - $clone->scheme = $scheme; - - return $clone; - } - - /** - * Filter Uri scheme. - * - * @param string $scheme Raw Uri scheme. - * @return string - * - * @throws InvalidArgumentException If the Uri scheme is not a string. - * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http". - */ - protected function filterScheme($scheme) - { - static $valid = [ - '' => true, - 'https' => true, - 'http' => true, - ]; - - if (!is_string($scheme) && !method_exists($scheme, '__toString')) { - throw new InvalidArgumentException('Uri scheme must be a string'); - } - - $scheme = str_replace('://', '', strtolower((string)$scheme)); - if (!isset($valid[$scheme])) { - throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"'); - } - - return $scheme; - } - - /******************************************************************************** - * Authority - *******************************************************************************/ - - /** - * Retrieve the authority component of the URI. - * - * If no authority information is present, this method MUST return an empty - * string. - * - * The authority syntax of the URI is: - * - *
-     * [user-info@]host[:port]
-     * 
- * - * If the port component is not set or is the standard port for the current - * scheme, it SHOULD NOT be included. - * - * @see https://tools.ietf.org/html/rfc3986#section-3.2 - * @return string The URI authority, in "[user-info@]host[:port]" format. - */ - public function getAuthority() - { - $userInfo = $this->getUserInfo(); - $host = $this->getHost(); - $port = $this->getPort(); - - return ($userInfo ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : ''); - } - - /** - * Retrieve the user information component of the URI. - * - * If no user information is present, this method MUST return an empty - * string. - * - * If a user is present in the URI, this will return that value; - * additionally, if the password is also present, it will be appended to the - * user value, with a colon (":") separating the values. - * - * The trailing "@" character is not part of the user information and MUST - * NOT be added. - * - * @return string The URI user information, in "username[:password]" format. - */ - public function getUserInfo() - { - return $this->user . ($this->password ? ':' . $this->password : ''); - } - - /** - * Return an instance with the specified user information. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified user information. - * - * Password is optional, but the user information MUST include the - * user; an empty string for the user is equivalent to removing user - * information. - * - * @param string $user The user name to use for authority. - * @param null|string $password The password associated with $user. - * @return self A new instance with the specified user information. - */ - public function withUserInfo($user, $password = null) - { - $clone = clone $this; - $clone->user = $user; - $clone->password = $password ? $password : ''; - - return $clone; - } - - /** - * Retrieve the host component of the URI. - * - * If no host is present, this method MUST return an empty string. - * - * The value returned MUST be normalized to lowercase, per RFC 3986 - * Section 3.2.2. - * - * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 - * @return string The URI host. - */ - public function getHost() - { - return $this->host; - } - - /** - * Return an instance with the specified host. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified host. - * - * An empty host value is equivalent to removing the host. - * - * @param string $host The hostname to use with the new instance. - * @return self A new instance with the specified host. - * @throws \InvalidArgumentException for invalid hostnames. - */ - public function withHost($host) - { - $clone = clone $this; - $clone->host = $host; - - return $clone; - } - - /** - * Retrieve the port component of the URI. - * - * If a port is present, and it is non-standard for the current scheme, - * this method MUST return it as an integer. If the port is the standard port - * used with the current scheme, this method SHOULD return null. - * - * If no port is present, and no scheme is present, this method MUST return - * a null value. - * - * If no port is present, but a scheme is present, this method MAY return - * the standard port for that scheme, but SHOULD return null. - * - * @return null|int The URI port. - */ - public function getPort() - { - return $this->port && !$this->hasStandardPort() ? $this->port : null; - } - - /** - * Return an instance with the specified port. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified port. - * - * Implementations MUST raise an exception for ports outside the - * established TCP and UDP port ranges. - * - * A null value provided for the port is equivalent to removing the port - * information. - * - * @param null|int $port The port to use with the new instance; a null value - * removes the port information. - * @return self A new instance with the specified port. - * @throws \InvalidArgumentException for invalid ports. - */ - public function withPort($port) - { - $port = $this->filterPort($port); - $clone = clone $this; - $clone->port = $port; - - return $clone; - } - - /** - * Does this Uri use a standard port? - * - * @return bool - */ - protected function hasStandardPort() - { - return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443); - } - - /** - * Filter Uri port. - * - * @param null|int $port The Uri port number. - * @return null|int - * - * @throws InvalidArgumentException If the port is invalid. - */ - protected function filterPort($port) - { - if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) { - return $port; - } - - throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)'); - } - - /******************************************************************************** - * Path - *******************************************************************************/ - - /** - * Retrieve the path component of the URI. - * - * The path can either be empty or absolute (starting with a slash) or - * rootless (not starting with a slash). Implementations MUST support all - * three syntaxes. - * - * Normally, the empty path "" and absolute path "/" are considered equal as - * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically - * do this normalization because in contexts with a trimmed base path, e.g. - * the front controller, this difference becomes significant. It's the task - * of the user to handle both "" and "/". - * - * The value returned MUST be percent-encoded, but MUST NOT double-encode - * any characters. To determine what characters to encode, please refer to - * RFC 3986, Sections 2 and 3.3. - * - * As an example, if the value should include a slash ("/") not intended as - * delimiter between path segments, that value MUST be passed in encoded - * form (e.g., "%2F") to the instance. - * - * @see https://tools.ietf.org/html/rfc3986#section-2 - * @see https://tools.ietf.org/html/rfc3986#section-3.3 - * @return string The URI path. - */ - public function getPath() - { - return $this->path; - } - - /** - * Return an instance with the specified path. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified path. - * - * The path can either be empty or absolute (starting with a slash) or - * rootless (not starting with a slash). Implementations MUST support all - * three syntaxes. - * - * If the path is intended to be domain-relative rather than path relative then - * it must begin with a slash ("/"). Paths not starting with a slash ("/") - * are assumed to be relative to some base path known to the application or - * consumer. - * - * Users can provide both encoded and decoded path characters. - * Implementations ensure the correct encoding as outlined in getPath(). - * - * @param string $path The path to use with the new instance. - * @return self A new instance with the specified path. - * @throws \InvalidArgumentException for invalid paths. - */ - public function withPath($path) - { - if (!is_string($path)) { - throw new InvalidArgumentException('Uri path must be a string'); - } - - $clone = clone $this; - $clone->path = $this->filterPath($path); - - return $clone; - } - - /** - * Filter Uri path. - * - * This method percent-encodes all reserved - * characters in the provided path string. This method - * will NOT double-encode characters that are already - * percent-encoded. - * - * @param string $path The raw uri path. - * @return string The RFC 3986 percent-encoded uri path. - * @link http://www.faqs.org/rfcs/rfc3986.html - */ - protected function filterPath($path) - { - return preg_replace_callback( - '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/', - function ($match) { - return rawurlencode($match[0]); - }, - $path - ); - } - - /******************************************************************************** - * Query - *******************************************************************************/ - - /** - * Retrieve the query string of the URI. - * - * If no query string is present, this method MUST return an empty string. - * - * The leading "?" character is not part of the query and MUST NOT be - * added. - * - * The value returned MUST be percent-encoded, but MUST NOT double-encode - * any characters. To determine what characters to encode, please refer to - * RFC 3986, Sections 2 and 3.4. - * - * As an example, if a value in a key/value pair of the query string should - * include an ampersand ("&") not intended as a delimiter between values, - * that value MUST be passed in encoded form (e.g., "%26") to the instance. - * - * @see https://tools.ietf.org/html/rfc3986#section-2 - * @see https://tools.ietf.org/html/rfc3986#section-3.4 - * @return string The URI query string. - */ - public function getQuery() - { - return $this->query; - } - - /** - * Return an instance with the specified query string. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified query string. - * - * Users can provide both encoded and decoded query characters. - * Implementations ensure the correct encoding as outlined in getQuery(). - * - * An empty query string value is equivalent to removing the query string. - * - * @param string $query The query string to use with the new instance. - * @return self A new instance with the specified query string. - * @throws \InvalidArgumentException for invalid query strings. - */ - public function withQuery($query) - { - if (!is_string($query) && !method_exists($query, '__toString')) { - throw new InvalidArgumentException('Uri query must be a string'); - } - $query = ltrim((string)$query, '?'); - $clone = clone $this; - $clone->query = $this->filterQuery($query); - - return $clone; - } - - /** - * Filters the query string or fragment of a URI. - * - * @param string $query The raw uri query string. - * @return string The percent-encoded query string. - */ - protected function filterQuery($query) - { - return preg_replace_callback( - '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', - function ($match) { - return rawurlencode($match[0]); - }, - $query - ); - } - - /******************************************************************************** - * Fragment - *******************************************************************************/ - - /** - * Retrieve the fragment component of the URI. - * - * If no fragment is present, this method MUST return an empty string. - * - * The leading "#" character is not part of the fragment and MUST NOT be - * added. - * - * The value returned MUST be percent-encoded, but MUST NOT double-encode - * any characters. To determine what characters to encode, please refer to - * RFC 3986, Sections 2 and 3.5. - * - * @see https://tools.ietf.org/html/rfc3986#section-2 - * @see https://tools.ietf.org/html/rfc3986#section-3.5 - * @return string The URI fragment. - */ - public function getFragment() - { - return $this->fragment; - } - - /** - * Return an instance with the specified URI fragment. - * - * This method MUST retain the state of the current instance, and return - * an instance that contains the specified URI fragment. - * - * Users can provide both encoded and decoded fragment characters. - * Implementations ensure the correct encoding as outlined in getFragment(). - * - * An empty fragment value is equivalent to removing the fragment. - * - * @param string $fragment The fragment to use with the new instance. - * @return self A new instance with the specified fragment. - */ - public function withFragment($fragment) - { - if (!is_string($fragment) && !method_exists($fragment, '__toString')) { - throw new InvalidArgumentException('Uri fragment must be a string'); - } - $fragment = ltrim((string)$fragment, '#'); - $clone = clone $this; - $clone->fragment = $this->filterQuery($fragment); - - return $clone; - } - - /******************************************************************************** - * Helpers - *******************************************************************************/ - - /** - * Return the string representation as a URI reference. - * - * Depending on which components of the URI are present, the resulting - * string is either a full URI or relative reference according to RFC 3986, - * Section 4.1. The method concatenates the various components of the URI, - * using the appropriate delimiters: - * - * - If a scheme is present, it MUST be suffixed by ":". - * - If an authority is present, it MUST be prefixed by "//". - * - The path can be concatenated without delimiters. But there are two - * cases where the path has to be adjusted to make the URI reference - * valid as PHP does not allow to throw an exception in __toString(): - * - If the path is rootless and an authority is present, the path MUST - * be prefixed by "/". - * - If the path is starting with more than one "/" and no authority is - * present, the starting slashes MUST be reduced to one. - * - If a query is present, it MUST be prefixed by "?". - * - If a fragment is present, it MUST be prefixed by "#". - * - * @see http://tools.ietf.org/html/rfc3986#section-4.1 - * @return string - */ - public function __toString() - { - $scheme = $this->getScheme(); - $authority = $this->getAuthority(); - $path = $this->getPath(); - $query = $this->getQuery(); - $fragment = $this->getFragment(); - - $path = '/' . ltrim($path, '/'); - - return ($scheme ? $scheme . ':' : '') - . ($authority ? '//' . $authority : '') - . $path - . ($query ? '?' . $query : '') - . ($fragment ? '#' . $fragment : ''); - } - - /** - * Return the fully qualified base URL. - * - * Note that this method never includes a trailing / - * - * This method is not part of PSR-7. - * - * @return string - */ - public function getBaseUrl() - { - $scheme = $this->getScheme(); - $authority = $this->getAuthority(); - - return ($scheme ? $scheme . ':' : '') - . ($authority ? '//' . $authority : ''); - } -} diff --git a/tests/Http/BodyTest.php b/tests/Http/BodyTest.php deleted file mode 100644 index 22bd29b0f..000000000 --- a/tests/Http/BodyTest.php +++ /dev/null @@ -1,416 +0,0 @@ -stream) === true) { - fclose($this->stream); - } - } - - /** - * This method creates a new resource, and it seeds - * the resource with lorem ipsum text. The returned - * resource is readable, writable, and seekable. - * - * @param string $mode - * - * @return resource - */ - public function resourceFactory($mode = 'r+') - { - $stream = fopen('php://temp', $mode); - fwrite($stream, $this->text); - rewind($stream); - - return $stream; - } - - public function testConstructorAttachesStream() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - - $this->assertSame($this->stream, $bodyStream->getValue($body)); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testConstructorInvalidStream() - { - $this->stream = 'foo'; - $body = new Body($this->stream); - } - - public function testGetMetadata() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertTrue(is_array($body->getMetadata())); - } - - public function testGetMetadataKey() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertEquals('php://temp', $body->getMetadata('uri')); - } - - public function testGetMetadataKeyNotFound() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertNull($body->getMetadata('foo')); - } - - public function testDetach() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - - $bodyMetadata = new ReflectionProperty($body, 'meta'); - $bodyMetadata->setAccessible(true); - - $bodyReadable = new ReflectionProperty($body, 'readable'); - $bodyReadable->setAccessible(true); - - $bodyWritable = new ReflectionProperty($body, 'writable'); - $bodyWritable->setAccessible(true); - - $bodySeekable = new ReflectionProperty($body, 'seekable'); - $bodySeekable->setAccessible(true); - - $result = $body->detach(); - - $this->assertSame($this->stream, $result); - $this->assertNull($bodyStream->getValue($body)); - $this->assertNull($bodyMetadata->getValue($body)); - $this->assertNull($bodyReadable->getValue($body)); - $this->assertNull($bodyWritable->getValue($body)); - $this->assertNull($bodySeekable->getValue($body)); - } - - public function testToStringAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertEquals($this->text, (string)$body); - } - - public function testToStringAttachedRewindsFirst() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertEquals($this->text, (string)$body); - $this->assertEquals($this->text, (string)$body); - $this->assertEquals($this->text, (string)$body); - } - - public function testToStringDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($body, null); - - $this->assertEquals('', (string)$body); - } - - public function testClose() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->close(); - - $this->assertAttributeEquals(null, 'stream', $body); - //$this->assertFalse($body->isAttached()); #1269 - } - - public function testGetSizeAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertEquals(mb_strlen($this->text), $body->getSize()); - } - - public function testGetSizeDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($body, null); - - $this->assertNull($body->getSize()); - } - - public function testTellAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - fseek($this->stream, 10); - - $this->assertEquals(10, $body->tell()); - } - - /** - * @expectedException \RuntimeException - */ - public function testTellDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($body, null); - - $body->tell(); - } - - public function testEofAttachedFalse() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - fseek($this->stream, 10); - - $this->assertFalse($body->eof()); - } - - public function testEofAttachedTrue() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - while (feof($this->stream) === false) { - fread($this->stream, 1024); - } - - $this->assertTrue($body->eof()); - } - - public function testEofDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $bodyStream = new ReflectionProperty($body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($body, null); - - $this->assertTrue($body->eof()); - } - - public function isReadableAttachedTrue() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertTrue($body->isReadable()); - } - - public function isReadableAttachedFalse() - { - $stream = fopen('php://temp', 'w'); - $body = new Body($this->stream); - - $this->assertFalse($body->isReadable()); - fclose($stream); - } - - public function testIsReadableDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $this->assertFalse($body->isReadable()); - } - - public function isWritableAttachedTrue() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertTrue($body->isWritable()); - } - - public function isWritableAttachedFalse() - { - $stream = fopen('php://temp', 'r'); - $body = new Body($this->stream); - - $this->assertFalse($body->isWritable()); - fclose($stream); - } - - public function testIsWritableDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $this->assertFalse($body->isWritable()); - } - - public function isSeekableAttachedTrue() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertTrue($body->isSeekable()); - } - - // TODO: Is seekable is false when attached... how? - - public function testIsSeekableDetached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $this->assertFalse($body->isSeekable()); - } - - public function testSeekAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->seek(10); - - $this->assertEquals(10, ftell($this->stream)); - } - - /** - * @expectedException \RuntimeException - */ - public function testSeekDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $body->seek(10); - } - - public function testRewindAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - fseek($this->stream, 10); - $body->rewind(); - - $this->assertEquals(0, ftell($this->stream)); - } - - /** - * @expectedException \RuntimeException - */ - public function testRewindDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $body->rewind(); - } - - public function testReadAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - - $this->assertEquals(substr($this->text, 0, 10), $body->read(10)); - } - - /** - * @expectedException \RuntimeException - */ - public function testReadDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $body->read(10); - } - - public function testWriteAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - while (feof($this->stream) === false) { - fread($this->stream, 1024); - } - $body->write('foo'); - - $this->assertEquals($this->text . 'foo', (string)$body); - } - - /** - * @expectedException \RuntimeException - */ - public function testWriteDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $body->write('foo'); - } - - public function testGetContentsAttached() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - fseek($this->stream, 10); - - $this->assertEquals(substr($this->text, 10), $body->getContents()); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetContentsDetachedThrowsRuntimeException() - { - $this->stream = $this->resourceFactory(); - $body = new Body($this->stream); - $body->detach(); - - $body->getContents(); - } -} diff --git a/tests/Http/CookiesTest.php b/tests/Http/CookiesTest.php deleted file mode 100644 index 8315d80d4..000000000 --- a/tests/Http/CookiesTest.php +++ /dev/null @@ -1,235 +0,0 @@ - 'Works', - ]); - $prop = new ReflectionProperty($cookies, 'requestCookies'); - $prop->setAccessible(true); - $this->assertNotEmpty($prop->getValue($cookies)['test']); - $this->assertEquals('Works', $prop->getValue($cookies)['test']); - } - - public function testSetDefaults() - { - $defaults = [ - 'value' => 'toast', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => true, - 'httponly' => true - ]; - - $cookies = new Cookies; - - $prop = new ReflectionProperty($cookies, 'defaults'); - $prop->setAccessible(true); - - $origDefaults = $prop->getValue($cookies); - - $cookies->setDefaults($defaults); - - $this->assertEquals($defaults, $prop->getValue($cookies)); - $this->assertNotEquals($origDefaults, $prop->getValue($cookies)); - } - - public function testSetCookieValues() - { - $cookies = new Cookies; - $cookies->set('foo', 'bar'); - - $prop = new ReflectionProperty($cookies, 'responseCookies'); - $prop->setAccessible(true); - - //we expect all of these values with null/false defaults - $expectedValue = [ - 'foo' => [ - 'value' => 'bar', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => false, - 'httponly' => false - ] - ]; - - $this->assertEquals($expectedValue, $prop->getValue($cookies)); - } - - public function testSetCookieValuesContainDefaults() - { - $cookies = new Cookies; - $defaults = [ - 'value' => 'toast', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => true, - 'httponly' => true - ]; - - $cookies->setDefaults($defaults); - $cookies->set('foo', 'bar'); - - $prop = new ReflectionProperty($cookies, 'responseCookies'); - $prop->setAccessible(true); - - //we expect to have secure and httponly from defaults - $expectedValue = [ - 'foo' => [ - 'value' => 'bar', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => true, - 'httponly' => true - ] - ]; - - $this->assertEquals($expectedValue, $prop->getValue($cookies)); - } - - public function testSetCookieValuesCanOverrideDefaults() - { - $cookies = new Cookies; - $defaults = [ - 'value' => 'toast', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => true, - 'httponly' => true - ]; - - $cookies->setDefaults($defaults); - - //default has secure true, lets override it to false - $cookies->set('foo', ['value' => 'bar', 'secure' => false]); - - $prop = new ReflectionProperty($cookies, 'responseCookies'); - $prop->setAccessible(true); - - $expectedValue = [ - 'foo' => [ - 'value' => 'bar', - 'domain' => null, - 'hostonly' => null, - 'path' => null, - 'expires' => null, - 'secure' => false, - 'httponly' => true - ] - ]; - - $this->assertEquals($expectedValue, $prop->getValue($cookies)); - } - - public function testGet() - { - $cookies = new Cookies(['foo' => 'bar']); - $this->assertEquals('bar', $cookies->get('foo')); - $this->assertNull($cookies->get('missing')); - $this->assertEquals('defaultValue', $cookies->get('missing', 'defaultValue')); - } - - public function testParseHeader() - { - $cookies = Cookies::parseHeader('foo=bar; name=Josh'); - $this->assertEquals('bar', $cookies['foo']); - $this->assertEquals('Josh', $cookies['name']); - } - - public function testParseHeaderWithJsonArray() - { - $cookies = Cookies::parseHeader('foo=bar; testarray=["someVar1","someVar2","someVar3"]'); - $this->assertEquals('bar', $cookies['foo']); - $this->assertContains('someVar3', json_decode($cookies['testarray'])); - } - - public function testToHeaders() - { - $cookies = new Cookies; - $cookies->set('test', 'Works'); - $cookies->set('test_array', ['value' => 'bar', 'domain' => 'example.com']); - $this->assertEquals('test=Works', $cookies->toHeaders()[0]); - $this->assertEquals('test_array=bar; domain=example.com', $cookies->toHeaders()[1]); - } - - public function testToHeader() - { - $cookies = new Cookies(); - $class = new ReflectionClass($cookies); - $method = $class->getMethod('toHeader'); - $method->setAccessible(true); - $properties = [ - 'name' => 'test', - 'properties' => [ - 'value' => 'Works' - ] - ]; - $time = time(); - $formattedDate = gmdate('D, d-M-Y H:i:s e', $time); - $propertiesComplex = [ - 'name' => 'test_complex', - 'properties' => [ - 'value' => 'Works', - 'domain' => 'example.com', - 'expires' => $time, - 'path' => '/', - 'secure' => true, - 'hostonly' => true, - 'httponly' => true - ] - ]; - $stringDate = '2016-01-01 12:00:00'; - $formattedStringDate = gmdate('D, d-M-Y H:i:s e', strtotime($stringDate)); - $propertiesStringDate = [ - 'name' => 'test_date', - 'properties' => [ - 'value' => 'Works', - 'expires' => $stringDate, - ] - ]; - $cookie = $method->invokeArgs($cookies, $properties); - $cookieComplex = $method->invokeArgs($cookies, $propertiesComplex); - $cookieStringDate = $method->invokeArgs($cookies, $propertiesStringDate); - $this->assertEquals('test=Works', $cookie); - $this->assertEquals( - 'test_complex=Works; domain=example.com; path=/; expires=' - . $formattedDate . '; secure; HostOnly; HttpOnly', - $cookieComplex - ); - $this->assertEquals('test_date=Works; expires=' . $formattedStringDate, $cookieStringDate); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testParseHeaderException() - { - Cookies::parseHeader(new \StdClass); - } -} diff --git a/tests/Http/EnvironmentTest.php b/tests/Http/EnvironmentTest.php deleted file mode 100644 index e427c6abe..000000000 --- a/tests/Http/EnvironmentTest.php +++ /dev/null @@ -1,59 +0,0 @@ -assertEquals($_SERVER, $env->all()); - } - - /** - * Test environment from mock data - */ - public function testMock() - { - $env = Environment::mock([ - 'SCRIPT_NAME' => '/foo/bar/index.php', - 'REQUEST_URI' => '/foo/bar?abc=123', - ]); - - $this->assertInstanceOf('\Slim\Interfaces\CollectionInterface', $env); - $this->assertEquals('/foo/bar/index.php', $env->get('SCRIPT_NAME')); - $this->assertEquals('/foo/bar?abc=123', $env->get('REQUEST_URI')); - $this->assertEquals('localhost', $env->get('HTTP_HOST')); - } -} diff --git a/tests/Http/HeadersTest.php b/tests/Http/HeadersTest.php deleted file mode 100644 index cccf700c0..000000000 --- a/tests/Http/HeadersTest.php +++ /dev/null @@ -1,228 +0,0 @@ - 'application/json', - ]); - $h = Headers::createFromEnvironment($e); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['accept'])); - $this->assertEquals('application/json', $prop->getValue($h)['accept']['value'][0]); - } - - public function testCreateFromEnvironmentWithSpecialHeaders() - { - $e = Environment::mock([ - 'CONTENT_TYPE' => 'application/json', - ]); - $h = Headers::createFromEnvironment($e); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['content-type'])); - $this->assertEquals('application/json', $prop->getValue($h)['content-type']['value'][0]); - } - - public function testCreateFromEnvironmentIgnoresHeaders() - { - $e = Environment::mock([ - 'CONTENT_TYPE' => 'text/csv', - 'HTTP_CONTENT_LENGTH' => 1230, // <-- Ignored - ]); - $h = Headers::createFromEnvironment($e); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertNotContains('content-length', $prop->getValue($h)); - } - - public function testConstructor() - { - $h = new Headers([ - 'Content-Length' => 100, - ]); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['content-length'])); - $this->assertEquals(100, $prop->getValue($h)['content-length']['value'][0]); - } - - public function testSetSingleValue() - { - $h = new Headers(); - $h->set('Content-Length', 100); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['content-length'])); - $this->assertEquals(100, $prop->getValue($h)['content-length']['value'][0]); - } - - public function testSetArrayValue() - { - $h = new Headers(); - $h->set('Allow', ['GET', 'POST']); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['allow'])); - $this->assertEquals(['GET', 'POST'], $prop->getValue($h)['allow']['value']); - } - - public function testGet() - { - $h = new Headers(); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - $prop->setValue($h, [ - 'allow' => [ - 'value' => ['GET', 'POST'], - 'originalKey' => 'Allow' - ] - ]); - - $this->assertEquals(['GET', 'POST'], $h->get('Allow')); - } - - public function testGetOriginalKey() - { - $h = new Headers(); - $h->set('http-test_key', 'testValue'); - $h->get('test-key'); - - $value = $h->get('test-key'); - - $this->assertEquals('testValue', reset($value)); - $this->assertEquals('http-test_key', $h->getOriginalKey('test-key')); - $this->assertNull($h->getOriginalKey('test-non-existing')); - } - - public function testGetNotExists() - { - $h = new Headers(); - - $this->assertNull($h->get('Foo')); - } - - public function testAddNewValue() - { - $h = new Headers(); - $h->add('Foo', 'Bar'); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['foo'])); - $this->assertEquals(['Bar'], $prop->getValue($h)['foo']['value']); - } - - public function testAddAnotherValue() - { - $h = new Headers(); - $h->add('Foo', 'Bar'); - $h->add('Foo', 'Xyz'); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['foo'])); - $this->assertEquals(['Bar', 'Xyz'], $prop->getValue($h)['foo']['value']); - } - - public function testAddArrayValue() - { - $h = new Headers(); - $h->add('Foo', 'Bar'); - $h->add('Foo', ['Xyz', '123']); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - - $this->assertTrue(is_array($prop->getValue($h)['foo'])); - $this->assertEquals(['Bar', 'Xyz', '123'], $prop->getValue($h)['foo']['value']); - } - - public function testHas() - { - $h = new Headers(); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - $prop->setValue($h, [ - 'allow' => [ - 'value' => ['GET', 'POST'], - 'originalKey' => 'Allow' - ] - ]); - $this->assertTrue($h->has('allow')); - $this->assertFalse($h->has('foo')); - } - - public function testRemove() - { - $h = new Headers(); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - $prop->setValue($h, [ - 'Allow' => [ - 'value' => ['GET', 'POST'], - 'originalKey' => 'Allow' - ] - ]); - $h->remove('Allow'); - - $this->assertNotContains('Allow', $prop->getValue($h)); - } - - public function testOriginalKeys() - { - $h = new Headers(); - $prop = new ReflectionProperty($h, 'data'); - $prop->setAccessible(true); - $prop->setValue($h, [ - 'Allow' => [ - 'value' => ['GET', 'POST'], - 'originalKey' => 'ALLOW' - ] - ]); - $all = $h->all(); - - $this->assertArrayHasKey('ALLOW', $all); - } - - public function testNormalizeKey() - { - $h = new Headers(); - $this->assertEquals('foo-bar', $h->normalizeKey('HTTP_FOO_BAR')); - $this->assertEquals('foo-bar', $h->normalizeKey('HTTP-FOO-BAR')); - $this->assertEquals('foo-bar', $h->normalizeKey('Http-Foo-Bar')); - $this->assertEquals('foo-bar', $h->normalizeKey('Http_Foo_Bar')); - $this->assertEquals('foo-bar', $h->normalizeKey('http_foo_bar')); - $this->assertEquals('foo-bar', $h->normalizeKey('http-foo-bar')); - } - - public function testDetermineAuthorization() - { - $e = Environment::mock([]); - $en = Headers::determineAuthorization($e); - $h = Headers::createFromEnvironment($e); - - $this->assertEquals('electrolytes', $en->get('HTTP_AUTHORIZATION')); - $this->assertEquals(['electrolytes'], $h->get('Authorization')); - } -} diff --git a/tests/Http/MessageTest.php b/tests/Http/MessageTest.php deleted file mode 100644 index 9beb7e4a4..000000000 --- a/tests/Http/MessageTest.php +++ /dev/null @@ -1,214 +0,0 @@ -protocolVersion = '1.0'; - - $this->assertEquals('1.0', $message->getProtocolVersion()); - } - - /** - * @covers Slim\Http\Message::withProtocolVersion - */ - public function testWithProtocolVersion() - { - $message = new MessageStub(); - $clone = $message->withProtocolVersion('1.0'); - - $this->assertEquals('1.0', $clone->protocolVersion); - } - - /** - * @covers Slim\Http\Message::withProtocolVersion - * @expectedException \InvalidArgumentException - */ - public function testWithProtocolVersionInvalidThrowsException() - { - $message = new MessageStub(); - $message->withProtocolVersion('3.0'); - } - - /******************************************************************************* - * Headers - ******************************************************************************/ - - /** - * @covers Slim\Http\Message::getHeaders - */ - public function testGetHeaders() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $headers->add('X-Foo', 'two'); - $headers->add('X-Foo', 'three'); - - $message = new MessageStub(); - $message->headers = $headers; - - $shouldBe = [ - 'X-Foo' => [ - 'one', - 'two', - 'three', - ], - ]; - - $this->assertEquals($shouldBe, $message->getHeaders()); - } - - /** - * @covers Slim\Http\Message::hasHeader - */ - public function testHasHeader() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - - $message = new MessageStub(); - $message->headers = $headers; - - $this->assertTrue($message->hasHeader('X-Foo')); - $this->assertFalse($message->hasHeader('X-Bar')); - } - - /** - * @covers Slim\Http\Message::getHeaderLine - */ - public function testGetHeaderLine() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $headers->add('X-Foo', 'two'); - $headers->add('X-Foo', 'three'); - - $message = new MessageStub(); - $message->headers = $headers; - - $this->assertEquals('one,two,three', $message->getHeaderLine('X-Foo')); - $this->assertEquals('', $message->getHeaderLine('X-Bar')); - } - - /** - * @covers Slim\Http\Message::getHeader - */ - public function testGetHeader() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $headers->add('X-Foo', 'two'); - $headers->add('X-Foo', 'three'); - - $message = new MessageStub(); - $message->headers = $headers; - - $this->assertEquals(['one', 'two', 'three'], $message->getHeader('X-Foo')); - $this->assertEquals([], $message->getHeader('X-Bar')); - } - - /** - * @covers Slim\Http\Message::withHeader - */ - public function testWithHeader() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $message = new MessageStub(); - $message->headers = $headers; - $clone = $message->withHeader('X-Foo', 'bar'); - - $this->assertEquals('bar', $clone->getHeaderLine('X-Foo')); - } - - /** - * @covers Slim\Http\Message::withAddedHeader - */ - public function testWithAddedHeader() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $message = new MessageStub(); - $message->headers = $headers; - $clone = $message->withAddedHeader('X-Foo', 'two'); - - $this->assertEquals('one,two', $clone->getHeaderLine('X-Foo')); - } - - /** - * @covers Slim\Http\Message::withoutHeader - */ - public function testWithoutHeader() - { - $headers = new Headers(); - $headers->add('X-Foo', 'one'); - $headers->add('X-Bar', 'two'); - $response = new MessageStub(); - $response->headers = $headers; - $clone = $response->withoutHeader('X-Foo'); - $shouldBe = [ - 'X-Bar' => ['two'], - ]; - - $this->assertEquals($shouldBe, $clone->getHeaders()); - } - - /******************************************************************************* - * Body - ******************************************************************************/ - - /** - * @covers Slim\Http\Message::getBody - */ - public function testGetBody() - { - $body = $this->getBody(); - $message = new MessageStub(); - $message->body = $body; - - $this->assertSame($body, $message->getBody()); - } - - /** - * @covers Slim\Http\Message::withBody - */ - public function testWithBody() - { - $body = $this->getBody(); - $body2 = $this->getBody(); - $message = new MessageStub(); - $message->body = $body; - $clone = $message->withBody($body2); - - $this->assertSame($body, $message->body); - $this->assertSame($body2, $clone->body); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|\Slim\Http\Body - */ - protected function getBody() - { - return $this->getMockBuilder('Slim\Http\Body')->disableOriginalConstructor()->getMock(); - } -} diff --git a/tests/Http/RequestBodyTest.php b/tests/Http/RequestBodyTest.php deleted file mode 100644 index 2aa8d966d..000000000 --- a/tests/Http/RequestBodyTest.php +++ /dev/null @@ -1,334 +0,0 @@ -body = new RequestBody(); - $this->body->write($this->text); - $this->body->rewind(); - } - - protected function tearDown() - { - if (is_resource($this->stream) === true) { - fclose($this->stream); - } - $this->body = null; - } - - /** - * This method creates a new resource, and it seeds - * the resource with lorem ipsum text. The returned - * resource is readable, writable, and seekable. - * - * @param string $mode - * - * @return resource - */ - public function resourceFactory($mode = 'r+') - { - $stream = fopen('php://temp', $mode); - fwrite($stream, $this->text); - rewind($stream); - - return $stream; - } - - public function testConstructorAttachesStream() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - - $this->assertInternalType('resource', $bodyStream->getValue($this->body)); - } - - public function testConstructorSetsMetadata() - { - $bodyMetadata = new ReflectionProperty($this->body, 'meta'); - $bodyMetadata->setAccessible(true); - - $this->assertTrue(is_array($bodyMetadata->getValue($this->body))); - } - - public function testGetMetadata() - { - $this->assertTrue(is_array($this->body->getMetadata())); - } - - public function testGetMetadataKey() - { - $this->assertEquals('php://temp', $this->body->getMetadata('uri')); - } - - public function testGetMetadataKeyNotFound() - { - $this->assertNull($this->body->getMetadata('foo')); - } - - public function testDetach() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - - $bodyMetadata = new ReflectionProperty($this->body, 'meta'); - $bodyMetadata->setAccessible(true); - - $bodyReadable = new ReflectionProperty($this->body, 'readable'); - $bodyReadable->setAccessible(true); - - $bodyWritable = new ReflectionProperty($this->body, 'writable'); - $bodyWritable->setAccessible(true); - - $bodySeekable = new ReflectionProperty($this->body, 'seekable'); - $bodySeekable->setAccessible(true); - - $result = $this->body->detach(); - - $this->assertInternalType('resource', $result); - $this->assertNull($bodyStream->getValue($this->body)); - $this->assertNull($bodyMetadata->getValue($this->body)); - $this->assertNull($bodyReadable->getValue($this->body)); - $this->assertNull($bodyWritable->getValue($this->body)); - $this->assertNull($bodySeekable->getValue($this->body)); - } - - public function testToStringAttached() - { - $this->assertEquals($this->text, (string)$this->body); - } - - public function testToStringAttachedRewindsFirst() - { - $this->assertEquals($this->text, (string)$this->body); - $this->assertEquals($this->text, (string)$this->body); - $this->assertEquals($this->text, (string)$this->body); - } - - public function testToStringDetached() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($this->body, null); - - $this->assertEquals('', (string)$this->body); - } - - /** - * @expectedException \RuntimeException - */ - public function testClose() - { - $this->body->close(); - - $this->assertAttributeEquals(null, 'stream', $this->body); - $this->assertFalse($this->body->isReadable()); - $this->assertFalse($this->body->isWritable()); - $this->assertEquals('', (string)$this->body); - - $this->body->tell(); - } - - public function testGetSizeAttached() - { - $this->assertEquals(mb_strlen($this->text), $this->body->getSize()); - } - - public function testGetSizeDetached() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($this->body, null); - - $this->assertNull($this->body->getSize()); - } - - public function testTellAttached() - { - $this->body->seek(10); - - $this->assertEquals(10, $this->body->tell()); - } - - /** - * @expectedException \RuntimeException - */ - public function testTellDetachedThrowsRuntimeException() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($this->body, null); - - $this->body->tell(); - } - - public function testEofAttachedFalse() - { - $this->body->seek(10); - - $this->assertFalse($this->body->eof()); - } - - public function testEofAttachedTrue() - { - while ($this->body->eof() === false) { - $this->body->read(1024); - } - - $this->assertTrue($this->body->eof()); - } - - public function testEofDetached() - { - $bodyStream = new ReflectionProperty($this->body, 'stream'); - $bodyStream->setAccessible(true); - $bodyStream->setValue($this->body, null); - - $this->assertTrue($this->body->eof()); - } - - public function testIsReadableAttachedTrue() - { - $this->assertTrue($this->body->isReadable()); - } - - public function testIsReadableDetached() - { - $this->body->detach(); - - $this->assertFalse($this->body->isReadable()); - } - - public function testIsWritableAttachedTrue() - { - $this->assertTrue($this->body->isWritable()); - } - - public function testIsWritableDetached() - { - $this->body->detach(); - - $this->assertFalse($this->body->isWritable()); - } - - public function isSeekableAttachedTrue() - { - $this->assertTrue($this->body->isSeekable()); - } - - // TODO: Is seekable is false when attached... how? - - public function testIsSeekableDetached() - { - $this->body->detach(); - - $this->assertFalse($this->body->isSeekable()); - } - - public function testSeekAttached() - { - $this->body->seek(10); - - $this->assertEquals(10, $this->body->tell()); - } - - /** - * @expectedException \RuntimeException - */ - public function testSeekDetachedThrowsRuntimeException() - { - $this->body->detach(); - $this->body->seek(10); - } - - public function testRewindAttached() - { - $this->body->seek(10); - $this->body->rewind(); - - $this->assertEquals(0, $this->body->tell()); - } - - /** - * @expectedException \RuntimeException - */ - public function testRewindDetachedThrowsRuntimeException() - { - $this->body->detach(); - $this->body->rewind(); - } - - public function testReadAttached() - { - $this->assertEquals(substr($this->text, 0, 10), $this->body->read(10)); - } - - /** - * @expectedException \RuntimeException - */ - public function testReadDetachedThrowsRuntimeException() - { - $this->body->detach(); - $this->body->read(10); - } - - public function testWriteAttached() - { - while ($this->body->eof() === false) { - $this->body->read(1024); - } - $this->body->write('foo'); - - $this->assertEquals($this->text . 'foo', (string)$this->body); - } - - /** - * @expectedException \RuntimeException - */ - public function testWriteDetachedThrowsRuntimeException() - { - $this->body->detach(); - $this->body->write('foo'); - } - - public function testGetContentsAttached() - { - $this->body->seek(10); - - $this->assertEquals(substr($this->text, 10), $this->body->getContents()); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetContentsDetachedThrowsRuntimeException() - { - $this->body->detach(); - $this->body->getContents(); - } -} diff --git a/tests/Http/RequestTest.php b/tests/Http/RequestTest.php deleted file mode 100644 index ab068c23e..000000000 --- a/tests/Http/RequestTest.php +++ /dev/null @@ -1,1170 +0,0 @@ - 'john', - 'id' => '123', - ]; - $serverParams = $env->all(); - $body = new RequestBody(); - $uploadedFiles = UploadedFile::createFromEnvironment($env); - $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); - - return $request; - } - - public function testDisableSetter() - { - $request = $this->requestFactory(); - $request->foo = 'bar'; - - $this->assertFalse(property_exists($request, 'foo')); - } - - public function testAddsHostHeaderFromUri() - { - $request = $this->requestFactory(); - $this->assertEquals('example.com', $request->getHeaderLine('Host')); - } - - /******************************************************************************* - * Method - ******************************************************************************/ - - public function testGetMethod() - { - $this->assertEquals('GET', $this->requestFactory()->getMethod()); - } - - public function testGetOriginalMethod() - { - $this->assertEquals('GET', $this->requestFactory()->getOriginalMethod()); - } - - public function testWithMethod() - { - $request = $this->requestFactory()->withMethod('PUT'); - - $this->assertAttributeEquals('PUT', 'method', $request); - $this->assertAttributeEquals('PUT', 'originalMethod', $request); - } - - public function testWithAllAllowedCharactersMethod() - { - $request = $this->requestFactory()->withMethod("!#$%&'*+.^_`|~09AZ-"); - - $this->assertAttributeEquals("!#$%&'*+.^_`|~09AZ-", 'method', $request); - $this->assertAttributeEquals("!#$%&'*+.^_`|~09AZ-", 'originalMethod', $request); - } - - /** - * @expectedException \Slim\Exception\HttpNotImplementedException - */ - public function testWithMethodInvalid() - { - $this->requestFactory()->withMethod('B@R'); - } - - public function testWithMethodNull() - { - $request = $this->requestFactory()->withMethod(null); - - $this->assertAttributeEquals(null, 'originalMethod', $request); - } - - /** - * @covers Slim\Http\Request::createFromEnvironment - */ - public function testCreateFromEnvironment() - { - $env = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo', - 'REQUEST_METHOD' => 'POST', - ]); - - $request = Request::createFromEnvironment($env); - $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals($env->all(), $request->getServerParams()); - } - - /** - * @covers Slim\Http\Request::createFromEnvironment - */ - public function testCreateFromEnvironmentWithMultipart() - { - $_POST['foo'] = 'bar'; - - $env = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo', - 'REQUEST_METHOD' => 'POST', - 'HTTP_CONTENT_TYPE' => 'multipart/form-data; boundary=---foo' - ]); - - $request = Request::createFromEnvironment($env); - unset($_POST); - - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - } - - /** - * @covers Slim\Http\Request::createFromEnvironment - */ - public function testCreateFromEnvironmentWithMultipartMethodOverride() - { - $_POST['_METHOD'] = 'PUT'; - - $env = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo', - 'REQUEST_METHOD' => 'POST', - 'HTTP_CONTENT_TYPE' => 'multipart/form-data; boundary=---foo' - ]); - - $request = Request::createFromEnvironment($env); - unset($_POST); - - $this->assertEquals('POST', $request->getOriginalMethod()); - $this->assertEquals('PUT', $request->getMethod()); - } - - public function testGetMethodWithOverrideHeader() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers([ - 'HTTP_X_HTTP_METHOD_OVERRIDE' => 'PUT', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals('PUT', $request->getMethod()); - $this->assertEquals('POST', $request->getOriginalMethod()); - } - - public function testGetMethodWithOverrideParameterFromBodyObject() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers([ - 'Content-Type' => 'application/x-www-form-urlencoded', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('_METHOD=PUT'); - $body->rewind(); - $request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals('PUT', $request->getMethod()); - $this->assertEquals('POST', $request->getOriginalMethod()); - } - - public function testGetMethodOverrideParameterFromBodyArray() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers([ - 'Content-Type' => 'application/x-www-form-urlencoded', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('_METHOD=PUT'); - $body->rewind(); - $request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); - $request->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) { - parse_str($input, $body); - return $body; // <-- Array - }); - - $this->assertEquals('PUT', $request->getMethod()); - } - - /** - * @expectedException \Slim\Exception\HttpNotImplementedException - */ - public function testCreateRequestWithInvalidMethodString() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers(); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('B@R', $uri, $headers, $cookies, $serverParams, $body); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testCreateRequestWithInvalidMethodOther() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers(); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request(10, $uri, $headers, $cookies, $serverParams, $body); - } - - public function testIsGet() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'GET'); - - $this->assertTrue($request->isGet()); - } - - public function testIsPost() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'POST'); - - $this->assertTrue($request->isPost()); - } - - public function testIsPut() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'PUT'); - - $this->assertTrue($request->isPut()); - } - - public function testIsPatch() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'PATCH'); - - $this->assertTrue($request->isPatch()); - } - - public function testIsDelete() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'DELETE'); - - $this->assertTrue($request->isDelete()); - } - - public function testIsHead() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'HEAD'); - - $this->assertTrue($request->isHead()); - } - - public function testIsOptions() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'originalMethod'); - $prop->setAccessible(true); - $prop->setValue($request, 'OPTIONS'); - - $this->assertTrue($request->isOptions()); - } - - public function testIsXhr() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers([ - 'Content-Type' => 'application/x-www-form-urlencoded', - 'X-Requested-With' => 'XMLHttpRequest', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body); - - $this->assertTrue($request->isXhr()); - } - - /******************************************************************************* - * URI - ******************************************************************************/ - - public function testGetRequestTarget() - { - $this->assertEquals('/foo/bar?abc=123', $this->requestFactory()->getRequestTarget()); - } - - public function testGetRequestTargetAlreadySet() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'requestTarget'); - $prop->setAccessible(true); - $prop->setValue($request, '/foo/bar?abc=123'); - - $this->assertEquals('/foo/bar?abc=123', $request->getRequestTarget()); - } - - public function testGetRequestTargetIfNoUri() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'uri'); - $prop->setAccessible(true); - $prop->setValue($request, null); - - $this->assertEquals('/', $request->getRequestTarget()); - } - - public function testWithRequestTarget() - { - $clone = $this->requestFactory()->withRequestTarget('/test?user=1'); - - $this->assertAttributeEquals('/test?user=1', 'requestTarget', $clone); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testWithRequestTargetThatHasSpaces() - { - $this->requestFactory()->withRequestTarget('/test/m ore/stuff?user=1'); - } - - public function testGetUri() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers(); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body); - - $this->assertSame($uri, $request->getUri()); - } - - public function testWithUri() - { - // Uris - $uri1 = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $uri2 = Uri::createFromString('https://example2.com:443/test?xyz=123'); - - // Request - $headers = new Headers(); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('GET', $uri1, $headers, $cookies, $serverParams, $body); - $clone = $request->withUri($uri2); - - $this->assertAttributeSame($uri2, 'uri', $clone); - } - - public function testWithUriPreservesHost() - { - // When `$preserveHost` is set to `true`, this method interacts with - // the Host header in the following ways: - - // - If the the Host header is missing or empty, and the new URI contains - // a host component, this method MUST update the Host header in the returned - // request. - $uri1 = Uri::createFromString(''); - $uri2 = Uri::createFromString('http://example2.com/test'); - - // Request - $headers = new Headers(); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $request = new Request('GET', $uri1, $headers, $cookies, $serverParams, $body); - - $clone = $request->withUri($uri2, true); - $this->assertSame('example2.com', $clone->getHeaderLine('Host')); - - // - If the Host header is missing or empty, and the new URI does not contain a - // host component, this method MUST NOT update the Host header in the returned - // request. - $uri3 = Uri::createFromString(''); - - $clone = $request->withUri($uri3, true); - $this->assertSame('', $clone->getHeaderLine('Host')); - - // - If a Host header is present and non-empty, this method MUST NOT update - // the Host header in the returned request. - $request = $request->withHeader('Host', 'example.com'); - $clone = $request->withUri($uri2, true); - $this->assertSame('example.com', $clone->getHeaderLine('Host')); - } - - public function testGetContentType() - { - $headers = new Headers([ - 'Content-Type' => ['application/json;charset=utf8'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals('application/json;charset=utf8', $request->getContentType()); - } - - public function testGetContentTypeEmpty() - { - $request = $this->requestFactory(); - - $this->assertNull($request->getContentType()); - } - - public function testGetMediaType() - { - $headers = new Headers([ - 'Content-Type' => ['application/json;charset=utf8'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals('application/json', $request->getMediaType()); - } - - public function testGetMediaTypeEmpty() - { - $request = $this->requestFactory(); - - $this->assertNull($request->getMediaType()); - } - - public function testGetMediaTypeParams() - { - $headers = new Headers([ - 'Content-Type' => ['application/json;charset=utf8;foo=bar'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals(['charset' => 'utf8', 'foo' => 'bar'], $request->getMediaTypeParams()); - } - - public function testGetMediaTypeParamsEmpty() - { - $headers = new Headers([ - 'Content-Type' => ['application/json'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals([], $request->getMediaTypeParams()); - } - - public function testGetMediaTypeParamsWithoutHeader() - { - $request = $this->requestFactory(); - - $this->assertEquals([], $request->getMediaTypeParams()); - } - - public function testGetContentCharset() - { - $headers = new Headers([ - 'Content-Type' => ['application/json;charset=utf8'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals('utf8', $request->getContentCharset()); - } - - public function testGetContentCharsetEmpty() - { - $headers = new Headers([ - 'Content-Type' => ['application/json'], - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertNull($request->getContentCharset()); - } - - public function testGetContentCharsetWithoutHeader() - { - $request = $this->requestFactory(); - - $this->assertNull($request->getContentCharset()); - } - - public function testGetContentLength() - { - $headers = new Headers([ - 'Content-Length' => '150', // <-- Note we define as a string - ]); - $request = $this->requestFactory(); - $headersProp = new ReflectionProperty($request, 'headers'); - $headersProp->setAccessible(true); - $headersProp->setValue($request, $headers); - - $this->assertEquals(150, $request->getContentLength()); - } - - public function testGetContentLengthWithoutHeader() - { - $request = $this->requestFactory(); - - $this->assertNull($request->getContentLength()); - } - - /******************************************************************************* - * Cookies - ******************************************************************************/ - - public function testGetCookieParam() - { - $shouldBe = 'john'; - - $this->assertEquals($shouldBe, $this->requestFactory()->getCookieParam('user')); - } - - public function testGetCookieParamWithDefault() - { - $shouldBe = 'bar'; - - $this->assertEquals($shouldBe, $this->requestFactory()->getCookieParam('foo', 'bar')); - } - - public function testGetCookieParams() - { - $shouldBe = [ - 'user' => 'john', - 'id' => '123', - ]; - - $this->assertEquals($shouldBe, $this->requestFactory()->getCookieParams()); - } - - public function testWithCookieParams() - { - $request = $this->requestFactory(); - $clone = $request->withCookieParams(['type' => 'framework']); - - $this->assertEquals(['type' => 'framework'], $clone->getCookieParams()); - } - - /******************************************************************************* - * Query Params - ******************************************************************************/ - - public function testGetQueryParams() - { - $this->assertEquals(['abc' => '123'], $this->requestFactory()->getQueryParams()); - } - - public function testGetQueryParamsAlreadySet() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'queryParams'); - $prop->setAccessible(true); - $prop->setValue($request, ['foo' => 'bar']); - - $this->assertEquals(['foo' => 'bar'], $request->getQueryParams()); - } - - public function testWithQueryParams() - { - $request = $this->requestFactory(); - $clone = $request->withQueryParams(['foo' => 'bar']); - $cloneUri = $clone->getUri(); - - $this->assertEquals('abc=123', $cloneUri->getQuery()); // <-- Unchanged - $this->assertEquals(['foo' => 'bar'], $clone->getQueryParams()); // <-- Changed - } - - public function testWithQueryParamsEmptyArray() - { - $request = $this->requestFactory(); - $clone = $request->withQueryParams([]); - $cloneUri = $clone->getUri(); - - $this->assertEquals('abc=123', $cloneUri->getQuery()); // <-- Unchanged - $this->assertEquals([], $clone->getQueryParams()); // <-- Changed - } - - public function testGetQueryParamsWithoutUri() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'uri'); - $prop->setAccessible(true); - $prop->setValue($request, null); - - $this->assertEquals([], $request->getQueryParams()); - } - - /******************************************************************************* - * Uploaded files - ******************************************************************************/ - - /** - * @covers Slim\Http\Request::withUploadedFiles - * @covers Slim\Http\Request::getUploadedFiles - */ - public function testWithUploadedFiles() - { - $files = [new UploadedFile('foo.txt'), new UploadedFile('bar.txt')]; - - $request = $this->requestFactory(); - $clone = $request->withUploadedFiles($files); - - $this->assertEquals([], $request->getUploadedFiles()); - $this->assertEquals($files, $clone->getUploadedFiles()); - } - - /******************************************************************************* - * Server Params - ******************************************************************************/ - - public function testGetServerParams() - { - $mockEnv = Environment::mock(["HTTP_AUTHORIZATION" => "test"]); - $request = $this->requestFactory(["HTTP_AUTHORIZATION" => "test"]); - - $serverParams = $request->getServerParams(); - foreach ($serverParams as $key => $value) { - if ($key == 'REQUEST_TIME' || $key == 'REQUEST_TIME_FLOAT') { - $this->assertGreaterThanOrEqual( - $mockEnv[$key], - $value, - sprintf("%s value of %s was less than expected value of %s", $key, $value, $mockEnv[$key]) - ); - } else { - $this->assertEquals( - $mockEnv[$key], - $value, - sprintf("%s value of %s did not equal expected value of %s", $key, $value, $mockEnv[$key]) - ); - } - } - } - - public function testGetServerParam() - { - $shouldBe = 'HTTP/1.1'; - $request = $this->requestFactory(['SERVER_PROTOCOL' => 'HTTP/1.1']); - - $this->assertEquals($shouldBe, $this->requestFactory()->getServerParam('SERVER_PROTOCOL')); - } - - public function testGetServerParamWithDefault() - { - $shouldBe = 'bar'; - - $this->assertEquals($shouldBe, $this->requestFactory()->getServerParam('HTTP_NOT_EXIST', 'bar')); - } - - /******************************************************************************* - * File Params - ******************************************************************************/ - - /******************************************************************************* - * Attributes - ******************************************************************************/ - - public function testGetAttributes() - { - $request = $this->requestFactory(); - $attrProp = new ReflectionProperty($request, 'attributes'); - $attrProp->setAccessible(true); - $attrProp->setValue($request, new Collection(['foo' => 'bar'])); - - $this->assertEquals(['foo' => 'bar'], $request->getAttributes()); - } - - public function testGetAttribute() - { - $request = $this->requestFactory(); - $attrProp = new ReflectionProperty($request, 'attributes'); - $attrProp->setAccessible(true); - $attrProp->setValue($request, new Collection(['foo' => 'bar'])); - - $this->assertEquals('bar', $request->getAttribute('foo')); - $this->assertNull($request->getAttribute('bar')); - $this->assertEquals(2, $request->getAttribute('bar', 2)); - } - - public function testWithAttribute() - { - $request = $this->requestFactory(); - $attrProp = new ReflectionProperty($request, 'attributes'); - $attrProp->setAccessible(true); - $attrProp->setValue($request, new Collection(['foo' => 'bar'])); - $clone = $request->withAttribute('test', '123'); - - $this->assertEquals('123', $clone->getAttribute('test')); - } - - public function testWithAttributes() - { - $request = $this->requestFactory(); - $attrProp = new ReflectionProperty($request, 'attributes'); - $attrProp->setAccessible(true); - $attrProp->setValue($request, new Collection(['foo' => 'bar'])); - $clone = $request->withAttributes(['test' => '123']); - - $this->assertNull($clone->getAttribute('foo')); - $this->assertEquals('123', $clone->getAttribute('test')); - } - - public function testWithoutAttribute() - { - $request = $this->requestFactory(); - $attrProp = new ReflectionProperty($request, 'attributes'); - $attrProp->setAccessible(true); - $attrProp->setValue($request, new Collection(['foo' => 'bar'])); - $clone = $request->withoutAttribute('foo'); - - $this->assertNull($clone->getAttribute('foo')); - } - - /******************************************************************************* - * Body - ******************************************************************************/ - - public function testGetParsedBodyForm() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/x-www-form-urlencoded;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('foo=bar'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - } - - public function testGetParsedBodyJson() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/json;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('{"foo":"bar"}'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - } - - public function testGetParsedBodyInvalidJson() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/json;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('{foo}bar'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertNull($request->getParsedBody()); - } - - public function testGetParsedBodySemiValidJson() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/json;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('"foo bar"'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertNull($request->getParsedBody()); - } - - public function testGetParsedBodyWithJsonStructuredSuffix() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/vnd.api+json;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('{"foo":"bar"}'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - } - - public function testGetParsedBodyXml() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/xml;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('Josh'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals('Josh', $request->getParsedBody()->name); - } - - public function testGetParsedBodyWithXmlStructuredSuffix() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/hal+xml;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('Josh'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals('Josh', $request->getParsedBody()->name); - } - - public function testGetParsedBodyXmlWithTextXMLMediaType() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'text/xml'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('Josh'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals('Josh', $request->getParsedBody()->name); - } - - /** - * Will fail if a simple_xml warning is created - */ - public function testInvalidXmlIsQuietForTextXml() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'text/xml'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('Josh'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals(null, $request->getParsedBody()); - } - - /** - * Will fail if a simple_xml warning is created - */ - public function testInvalidXmlIsQuietForApplicationXml() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/xml'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('Josh'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals(null, $request->getParsedBody()); - } - - - public function testGetParsedBodyWhenAlreadyParsed() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'bodyParsed'); - $prop->setAccessible(true); - $prop->setValue($request, ['foo' => 'bar']); - - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - } - - public function testGetParsedBodyWhenBodyDoesNotExist() - { - $request = $this->requestFactory(); - $prop = new ReflectionProperty($request, 'body'); - $prop->setAccessible(true); - $prop->setValue($request, null); - - $this->assertNull($request->getParsedBody()); - } - - public function testGetParsedBodyAfterCallReparseBody() - { - $uri = Uri::createFromString('https://example.com:443/?one=1'); - $headers = new Headers([ - 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf8', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('foo=bar'); - $body->rewind(); - $request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); - - $this->assertEquals(['foo' => 'bar'], $request->getParsedBody()); - - $newBody = new RequestBody(); - $newBody->write('abc=123'); - $newBody->rewind(); - $request = $request->withBody($newBody); - $request->reparseBody(); - - $this->assertEquals(['abc' => '123'], $request->getParsedBody()); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetParsedBodyAsArray() - { - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = new Headers([ - 'Content-Type' => 'application/json;charset=utf8', - ]); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('{"foo": "bar"}'); - $body->rewind(); - $request = new Request('POST', $uri, $headers, $cookies, $serverParams, $body); - $request->registerMediaTypeParser('application/json', function ($input) { - return 10; // <-- Return invalid body value - }); - $request->getParsedBody(); // <-- Triggers exception - } - - public function testWithParsedBody() - { - $clone = $this->requestFactory()->withParsedBody(['xyz' => '123']); - - $this->assertEquals(['xyz' => '123'], $clone->getParsedBody()); - } - - public function testWithParsedBodyEmptyArray() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/x-www-form-urlencoded;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('foo=bar'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - - $clone = $request->withParsedBody([]); - - $this->assertEquals([], $clone->getParsedBody()); - } - - public function testWithParsedBodyNull() - { - $method = 'GET'; - $uri = new Uri('https', 'example.com', 443, '/foo/bar', 'abc=123', '', ''); - $headers = new Headers(); - $headers->set('Content-Type', 'application/x-www-form-urlencoded;charset=utf8'); - $cookies = []; - $serverParams = []; - $body = new RequestBody(); - $body->write('foo=bar'); - $request = new Request($method, $uri, $headers, $cookies, $serverParams, $body); - - - $clone = $request->withParsedBody(null); - - $this->assertNull($clone->getParsedBody()); - } - - public function testGetParsedBodyReturnsNullWhenThereIsNoBodyData() - { - $request = $this->requestFactory(['REQUEST_METHOD' => 'POST']); - - $this->assertNull($request->getParsedBody()); - } - - public function testGetParsedBodyReturnsNullWhenThereIsNoMediaTypeParserRegistered() - { - $request = $this->requestFactory([ - 'REQUEST_METHOD' => 'POST', - 'CONTENT_TYPE' => 'text/csv', - ]); - $request->getBody()->write('foo,bar,baz'); - - $this->assertNull($request->getParsedBody()); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testWithParsedBodyInvalid() - { - $this->requestFactory()->withParsedBody(2); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testWithParsedBodyInvalidFalseValue() - { - $this->requestFactory()->withParsedBody(false); - } - - /******************************************************************************* - * Parameters - ******************************************************************************/ - - public function testGetParameterFromBody() - { - $body = new RequestBody(); - $body->write('foo=bar'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals('bar', $request->getParam('foo')); - } - - public function testGetParameterFromBodyWithBodyParemeterHelper() - { - $body = new RequestBody(); - $body->write('foo=bar'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals('bar', $request->getParsedBodyParam('foo')); - } - - public function testGetParameterFromQuery() - { - $request = $this->requestFactory()->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals('123', $request->getParam('abc')); - } - - public function testGetParameterFromQueryWithQueryParemeterHelper() - { - $request = $this->requestFactory()->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals('123', $request->getQueryParam('abc')); - } - - public function testGetParameterFromBodyOverQuery() - { - $body = new RequestBody(); - $body->write('abc=xyz'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - $this->assertEquals('xyz', $request->getParam('abc')); - } - - public function testGetParameterWithDefaultFromBodyOverQuery() - { - $body = new RequestBody(); - $body->write('abc=xyz'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - $this->assertEquals('xyz', $request->getParam('abc')); - $this->assertEquals('bar', $request->getParam('foo', 'bar')); - } - - public function testGetParameters() - { - $body = new RequestBody(); - $body->write('foo=bar'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals(['abc' => '123', 'foo' => 'bar'], $request->getParams()); - } - - public function testGetParametersWithBodyPriority() - { - $body = new RequestBody(); - $body->write('foo=bar&abc=xyz'); - $body->rewind(); - $request = $this->requestFactory() - ->withBody($body) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); - - $this->assertEquals(['abc' => 'xyz', 'foo' => 'bar'], $request->getParams()); - } - - /******************************************************************************* - * Protocol - ******************************************************************************/ - - public function testGetProtocolVersion() - { - $env = Environment::mock(['SERVER_PROTOCOL' => 'HTTP/1.0']); - $request = Request::createFromEnvironment($env); - - $this->assertEquals('1.0', $request->getProtocolVersion()); - } -} diff --git a/tests/Http/ResponseTest.php b/tests/Http/ResponseTest.php deleted file mode 100755 index 6e6cf7253..000000000 --- a/tests/Http/ResponseTest.php +++ /dev/null @@ -1,341 +0,0 @@ -assertAttributeEquals(200, 'status', $response); - $this->assertAttributeInstanceOf('\Slim\Http\Headers', 'headers', $response); - $this->assertAttributeInstanceOf('\Psr\Http\Message\StreamInterface', 'body', $response); - } - - public function testConstructorWithCustomArgs() - { - $headers = new Headers(); - $body = new Body(fopen('php://temp', 'r+')); - $response = new Response(404, $headers, $body); - - $this->assertAttributeEquals(404, 'status', $response); - $this->assertAttributeSame($headers, 'headers', $response); - $this->assertAttributeSame($body, 'body', $response); - } - - public function testDeepCopyClone() - { - $headers = new Headers(); - $body = new Body(fopen('php://temp', 'r+')); - $response = new Response(404, $headers, $body); - $clone = clone $response; - - $this->assertAttributeEquals('1.1', 'protocolVersion', $clone); - $this->assertAttributeEquals(404, 'status', $clone); - $this->assertAttributeNotSame($headers, 'headers', $clone); - $this->assertAttributeSame($body, 'body', $clone); - } - - public function testDisableSetter() - { - $response = new Response(); - $response->foo = 'bar'; - - $this->assertFalse(property_exists($response, 'foo')); - } - - /******************************************************************************* - * Status - ******************************************************************************/ - - public function testGetStatusCode() - { - $response = new Response(); - $responseStatus = new ReflectionProperty($response, 'status'); - $responseStatus->setAccessible(true); - $responseStatus->setValue($response, '404'); - - $this->assertEquals(404, $response->getStatusCode()); - } - - public function testWithStatus() - { - $response = new Response(); - $clone = $response->withStatus(302); - - $this->assertAttributeEquals(302, 'status', $clone); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testWithStatusInvalidStatusCodeThrowsException() - { - $response = new Response(); - $response->withStatus(800); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage ReasonPhrase must be a string - */ - public function testWithStatusInvalidReasonPhraseThrowsException() - { - $response = new Response(); - $response->withStatus(200, null); - } - - public function testWithStatusEmptyReasonPhrase() - { - $responseWithNoMessage = new Response(310); - - $this->assertEquals('', $responseWithNoMessage->getReasonPhrase()); - } - - public function testGetReasonPhrase() - { - $response = new Response(404); - - $this->assertEquals('Not Found', $response->getReasonPhrase()); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage ReasonPhrase must be supplied for this code - */ - public function testMustSetReasonPhraseForUnrecognisedCode() - { - $response = new Response(); - $response = $response->withStatus(199); - } - - public function testSetReasonPhraseForUnrecognisedCode() - { - $response = new Response(); - $response = $response->withStatus(199, 'Random Message'); - - $this->assertEquals('Random Message', $response->getReasonPhrase()); - } - - public function testGetCustomReasonPhrase() - { - $response = new Response(); - $clone = $response->withStatus(200, 'Custom Phrase'); - - $this->assertEquals('Custom Phrase', $clone->getReasonPhrase()); - } - - /** - * @covers Slim\Http\Response::withRedirect - */ - public function testWithRedirect() - { - $response = new Response(200); - $clone = $response->withRedirect('/foo', 301); - $cloneWithDefaultStatus = $response->withRedirect('/foo'); - $cloneWithStatusMethod = $response->withStatus(301)->withRedirect('/foo'); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertFalse($response->hasHeader('Location')); - - $this->assertSame(301, $clone->getStatusCode()); - $this->assertTrue($clone->hasHeader('Location')); - $this->assertEquals('/foo', $clone->getHeaderLine('Location')); - - $this->assertSame(302, $cloneWithDefaultStatus->getStatusCode()); - $this->assertTrue($cloneWithDefaultStatus->hasHeader('Location')); - $this->assertEquals('/foo', $cloneWithDefaultStatus->getHeaderLine('Location')); - - $this->assertSame(301, $cloneWithStatusMethod->getStatusCode()); - $this->assertTrue($cloneWithStatusMethod->hasHeader('Location')); - $this->assertEquals('/foo', $cloneWithStatusMethod->getHeaderLine('Location')); - } - - /******************************************************************************* - * Behaviors - ******************************************************************************/ - - public function testIsEmpty() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 204); - - $this->assertTrue($response->isEmpty()); - } - - public function testIsInformational() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 100); - - $this->assertTrue($response->isInformational()); - } - - public function testIsOk() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 200); - - $this->assertTrue($response->isOk()); - } - - public function testIsSuccessful() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 201); - - $this->assertTrue($response->isSuccessful()); - } - - public function testIsRedirect() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 302); - - $this->assertTrue($response->isRedirect()); - } - - public function testIsRedirection() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 308); - - $this->assertTrue($response->isRedirection()); - } - - public function testIsForbidden() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 403); - - $this->assertTrue($response->isForbidden()); - } - - public function testIsNotFound() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 404); - - $this->assertTrue($response->isNotFound()); - } - - public function testIsClientError() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 400); - - $this->assertTrue($response->isClientError()); - } - - public function testIsServerError() - { - $response = new Response(); - $prop = new ReflectionProperty($response, 'status'); - $prop->setAccessible(true); - $prop->setValue($response, 503); - - $this->assertTrue($response->isServerError()); - } - - public function testToString() - { - $output = 'HTTP/1.1 404 Not Found' . Response::EOL . - 'X-Foo: Bar' . Response::EOL . Response::EOL . - 'Where am I?'; - $this->expectOutputString($output); - $response = new Response(); - $response = $response->withStatus(404)->withHeader('X-Foo', 'Bar')->write('Where am I?'); - - echo $response; - } - - public function testWithJson() - { - $data = ['foo' => 'bar1&bar2']; - - $originalResponse = new Response(); - $response = $originalResponse->withJson($data, 201); - - $this->assertNotEquals($response->getStatusCode(), $originalResponse->getStatusCode()); - $this->assertEquals(201, $response->getStatusCode()); - $this->assertEquals('application/json;charset=utf-8', $response->getHeaderLine('Content-Type')); - - $body = $response->getBody(); - $body->rewind(); - $dataJson = $body->getContents(); //json_decode($body->getContents(), true); - - $originalBody = $originalResponse->getBody(); - $originalBody->rewind(); - $originalContents = $originalBody->getContents(); - - // test the original body hasn't be replaced - $this->assertNotEquals($dataJson, $originalContents); - - $this->assertEquals('{"foo":"bar1&bar2"}', $dataJson); - $this->assertEquals($data['foo'], json_decode($dataJson, true)['foo']); - - // Test encoding option - $response = $response->withJson($data, 200, JSON_HEX_AMP); - - $body = $response->getBody(); - $body->rewind(); - $dataJson = $body->getContents(); - - $this->assertEquals('{"foo":"bar1\u0026bar2"}', $dataJson); - $this->assertEquals($data['foo'], json_decode($dataJson, true)['foo']); - - $response = $response->withStatus(201)->withJson([]); - $this->assertEquals($response->getStatusCode(), 201); - } - - /** - * @expectedException \RuntimeException - */ - public function testWithInvalidJsonThrowsException() - { - $data = ['foo' => 'bar'.chr(233)]; - $this->assertEquals('bar'.chr(233), $data['foo']); - - $response = new Response(); - $response->withJson($data, 200); - - // Safety net: this assertion should not occur, since the RuntimeException - // must have been caught earlier by the test framework - $this->assertFalse(true); - } -} diff --git a/tests/Http/StreamTest.php b/tests/Http/StreamTest.php deleted file mode 100644 index 12104f149..000000000 --- a/tests/Http/StreamTest.php +++ /dev/null @@ -1,159 +0,0 @@ -pipeFh != null) { - stream_get_contents($this->pipeFh); // prevent broken pipe error message - } - } - - /** - * @covers Slim\Http\Stream::isPipe - */ - public function testIsPipe() - { - $this->openPipeStream(); - - $this->assertTrue($this->pipeStream->isPipe()); - - $this->pipeStream->detach(); - $this->assertFalse($this->pipeStream->isPipe()); - - $fhFile = fopen(__FILE__, 'r'); - $fileStream = new Stream($fhFile); - $this->assertFalse($fileStream->isPipe()); - } - - /** - * @covers Slim\Http\Stream::isReadable - */ - public function testIsPipeReadable() - { - $this->openPipeStream(); - - $this->assertTrue($this->pipeStream->isReadable()); - } - - /** - * @covers Slim\Http\Stream::isSeekable - */ - public function testPipeIsNotSeekable() - { - $this->openPipeStream(); - - $this->assertFalse($this->pipeStream->isSeekable()); - } - - /** - * @covers Slim\Http\Stream::seek - * @expectedException \RuntimeException - */ - public function testCannotSeekPipe() - { - $this->openPipeStream(); - - $this->pipeStream->seek(0); - } - - /** - * @covers Slim\Http\Stream::tell - * @expectedException \RuntimeException - */ - public function testCannotTellPipe() - { - $this->openPipeStream(); - - $this->pipeStream->tell(); - } - - /** - * @covers Slim\Http\Stream::rewind - * @expectedException \RuntimeException - */ - public function testCannotRewindPipe() - { - $this->openPipeStream(); - - $this->pipeStream->rewind(); - } - - /** - * @covers Slim\Http\Stream::getSize - */ - public function testPipeGetSizeYieldsNull() - { - $this->openPipeStream(); - - $this->assertNull($this->pipeStream->getSize()); - } - - /** - * @covers Slim\Http\Stream::close - */ - public function testClosePipe() - { - $this->openPipeStream(); - - stream_get_contents($this->pipeFh); // prevent broken pipe error message - $this->pipeStream->close(); - $this->pipeFh = null; - - $this->assertFalse($this->pipeStream->isPipe()); - } - - /** - * @covers Slim\Http\Stream::__toString - */ - public function testPipeToString() - { - $this->openPipeStream(); - - $this->assertSame('', (string) $this->pipeStream); - } - - /** - * @covers Slim\Http\Stream::getContents - */ - - public function testPipeGetContents() - { - $this->openPipeStream(); - - $contents = trim($this->pipeStream->getContents()); - $this->assertSame('12', $contents); - } - - /** - * Opens the pipe stream - * - * @see StreamTest::pipeStream - */ - private function openPipeStream() - { - $this->pipeFh = popen('echo 12', 'r'); - $this->pipeStream = new Stream($this->pipeFh); - } -} diff --git a/tests/Http/UploadedFilesTest.php b/tests/Http/UploadedFilesTest.php deleted file mode 100644 index 7c2035960..000000000 --- a/tests/Http/UploadedFilesTest.php +++ /dev/null @@ -1,486 +0,0 @@ -assertEquals($expected, $uploadedFile); - } - - /** - * @param array $input The input array to parse. - * - * @dataProvider providerCreateFromEnvironment - */ - public function testCreateFromEnvironmentFromUserData(array $input) - { - //If slim.files provided - it will return what was provided - $userData['slim.files'] = $input; - - $uploadedFile = UploadedFile::createFromEnvironment(Environment::mock($userData)); - $this->assertEquals($input, $uploadedFile); - } - - public function testCreateFromEnvironmentWithoutFile() - { - unset($_FILES); - - $uploadedFile = UploadedFile::createFromEnvironment(Environment::mock()); - $this->assertEquals([], $uploadedFile); - } - - /** - * @return UploadedFile - */ - public function testConstructor() - { - $attr = [ - 'tmp_name' => self::$filename, - 'name' => 'my-avatar.txt', - 'size' => 8, - 'type' => 'text/plain', - 'error' => 0, - ]; - - $uploadedFile = new UploadedFile( - $attr['tmp_name'], - $attr['name'], - $attr['type'], - $attr['size'], - $attr['error'], - false - ); - - - $this->assertEquals($attr['name'], $uploadedFile->getClientFilename()); - $this->assertEquals($attr['type'], $uploadedFile->getClientMediaType()); - $this->assertEquals($attr['size'], $uploadedFile->getSize()); - $this->assertEquals($attr['error'], $uploadedFile->getError()); - - return $uploadedFile; - } - - /** - * @depends testConstructor - * @param UploadedFile $uploadedFile - * @return UploadedFile - */ - public function testGetStream(UploadedFile $uploadedFile) - { - $stream = $uploadedFile->getStream(); - $this->assertEquals(true, $uploadedFile->getStream() instanceof Stream); - $stream->close(); - - return $uploadedFile; - } - - /** - * @depends testConstructor - * @param UploadedFile $uploadedFile - * @expectedException \InvalidArgumentException - */ - public function testMoveToNotWritable(UploadedFile $uploadedFile) - { - $tempName = uniqid('file-'); - $path = 'some_random_dir' . DIRECTORY_SEPARATOR . $tempName; - $uploadedFile->moveTo($path); - } - - /** - * @depends testConstructor - * @param UploadedFile $uploadedFile - * @return UploadedFile - */ - public function testMoveTo(UploadedFile $uploadedFile) - { - $tempName = uniqid('file-'); - $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $tempName; - $uploadedFile->moveTo($path); - - $this->assertFileExists($path); - - unlink($path); - - return $uploadedFile; - } - - /** - * @depends testMoveTo - * @param UploadedFile $uploadedFile - * @expectedException \RuntimeException - */ - public function testMoveToCannotBeDoneTwice(UploadedFile $uploadedFile) - { - $tempName = uniqid('file-'); - $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $tempName; - $uploadedFile->moveTo($path); - $this->assertFileExists($path); - unlink($path); - - $uploadedFile->moveTo($path); - } - - /** - * This test must run after testMoveTo - * - * @depends testConstructor - * @param UploadedFile $uploadedFile - * @expectedException \RuntimeException - */ - public function testMoveToAgain(UploadedFile $uploadedFile) - { - $tempName = uniqid('file-'); - $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $tempName; - $uploadedFile->moveTo($path); - } - - - /** - * This test must run after testMoveTo - * - * @depends testConstructor - * @param UploadedFile $uploadedFile - * @expectedException \RuntimeException - */ - public function testMovedStream($uploadedFile) - { - $uploadedFile->getStream(); - } - - public function testMoveToStream() - { - $uploadedFile = $this->generateNewTmpFile(); - $uploadedFile->moveTo('php://temp'); - } - - public function providerCreateFromEnvironment() - { - return [ - // no nest: - [ - // $_FILES array - [ - 'avatar' => [ - 'tmp_name' => 'phpUxcOty', - 'name' => 'my-avatar.png', - 'size' => 90996, - 'type' => 'image/png', - 'error' => 0, - ], - ], - // expected format of array - [ - 'avatar' => new UploadedFile('phpUxcOty', 'my-avatar.png', 'image/png', 90996, UPLOAD_ERR_OK, true) - ] - ], - // no nest, with error: - [ - // $_FILES array - [ - 'avatar' => [ - 'tmp_name' => 'phpUxcOty', - 'name' => 'my-avatar.png', - 'size' => 90996, - 'type' => 'image/png', - 'error' => 7, - ], - ], - // expected format of array - [ - 'avatar' => new UploadedFile( - 'phpUxcOty', - 'my-avatar.png', - 'image/png', - 90996, - UPLOAD_ERR_CANT_WRITE, - true - ) - ] - ], - - // array of files: - [ - // $_FILES array - [ - 'avatars' => [ - 'tmp_name' => [ - 0 => __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 1 => __DIR__ . DIRECTORY_SEPARATOR . 'file1.html', - ], - 'name' => [ - 0 => 'file0.txt', - 1 => 'file1.html', - ], - 'type' => [ - 0 => 'text/plain', - 1 => 'text/html', - ], - 'error' => [ - 0 => 0, - 1 => 0 - ], - 'size' => [ - 0 => 0, - 1 => 0 - ] - ], - ], - // expected format of array - [ - 'avatars' => [ - 0 => new UploadedFile( - __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 'file0.txt', - 'text/plain', - null, - UPLOAD_ERR_OK, - true - ), - 1 => new UploadedFile( - __DIR__ . DIRECTORY_SEPARATOR . 'file1.html', - 'file1.html', - 'text/html', - null, - UPLOAD_ERR_OK, - true - ), - ], - ] - ], - // array of files as multidimensional array: - [ - // $_FILES array - [ - [ - 'avatars' => [ - 'tmp_name' => [ - 0 => __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 1 => __DIR__ . DIRECTORY_SEPARATOR . 'file1.html', - ], - 'name' => [ - 0 => 'file0.txt', - 1 => 'file1.html', - ], - 'type' => [ - 0 => 'text/plain', - 1 => 'text/html', - ], - 'size' => [ - 0 => 0, - 1 => 0, - ], - ], - ], - ], - // expected format of array - [ - 0 => - [ - 'avatars' => - [ - 'tmp_name' => [], - 'name' => [], - 'type' => [], - 'size' => [], - ], - ], - ], - ], - // single nested file: - [ - // $_FILES array - [ - 'details' => [ - 'tmp_name' => [ - 'avatar' => __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - ], - 'name' => [ - 'avatar' => 'file0.txt', - ], - 'type' => [ - 'avatar' => 'text/plain', - ], - 'error' => [ - 'avatar' => 0, - ], - 'size' => [ - 'avatar' => 0, - ], - ], - ], - // expected format of array - [ - 'details' => [ - 'avatar' => new UploadedFile( - __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 'file0.txt', - 'text/plain', - null, - UPLOAD_ERR_OK, - true - ), - ], - ] - ], - // nested array of files: - [ - [ - 'files' => [ - 'tmp_name' => [ - 'details' => [ - 'avatar' => [ - 0 => __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 1 => __DIR__ . DIRECTORY_SEPARATOR . 'file1.html', - ], - ], - ], - 'name' => [ - 'details' => [ - 'avatar' => [ - 0 => 'file0.txt', - 1 => 'file1.html', - ], - ], - ], - 'type' => [ - 'details' => [ - 'avatar' => [ - 0 => 'text/plain', - 1 => 'text/html', - ], - ], - ], - 'error' => [ - 'details' => [ - 'avatar' => [ - 0 => 0, - 1 => 0 - ], - ], - ], - 'size' => [ - 'details' => [ - 'avatar' => [ - 0 => 0, - 1 => 0 - ], - ], - ], - ], - ], - // expected format of array - [ - 'files' => [ - 'details' => [ - 'avatar' => [ - 0 => new UploadedFile( - __DIR__ . DIRECTORY_SEPARATOR . 'file0.txt', - 'file0.txt', - 'text/plain', - null, - UPLOAD_ERR_OK, - true - ), - 1 => new UploadedFile( - __DIR__ . DIRECTORY_SEPARATOR . 'file1.html', - 'file1.html', - 'text/html', - null, - UPLOAD_ERR_OK, - true - ), - ], - ], - ], - ] - ], - ]; - } - - /** - * @param array $mockEnv An array representing a mock environment. - * - * @return Request - */ - public function requestFactory(array $mockEnv) - { - $env = Environment::mock(); - - $uri = Uri::createFromString('https://example.com:443/foo/bar?abc=123'); - $headers = Headers::createFromEnvironment($env); - $cookies = []; - $serverParams = $env->all(); - $body = new RequestBody(); - $uploadedFiles = UploadedFile::createFromEnvironment($env); - $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); - - return $request; - } -} diff --git a/tests/Http/UriTest.php b/tests/Http/UriTest.php deleted file mode 100644 index 6680dac93..000000000 --- a/tests/Http/UriTest.php +++ /dev/null @@ -1,609 +0,0 @@ -assertEquals('https', $this->uriFactory()->getScheme()); - } - - public function testWithScheme() - { - $uri = $this->uriFactory()->withScheme('http'); - - $this->assertAttributeEquals('http', 'scheme', $uri); - } - - public function testWithSchemeRemovesSuffix() - { - $uri = $this->uriFactory()->withScheme('http://'); - - $this->assertAttributeEquals('http', 'scheme', $uri); - } - - public function testWithSchemeEmpty() - { - $uri = $this->uriFactory()->withScheme(''); - - $this->assertAttributeEquals('', 'scheme', $uri); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri scheme must be one of: "", "https", "http" - */ - public function testWithSchemeInvalid() - { - $this->uriFactory()->withScheme('ftp'); - } - - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri scheme must be a string - */ - public function testWithSchemeInvalidType() - { - $this->uriFactory()->withScheme([]); - } - - /******************************************************************************** - * Authority - *******************************************************************************/ - - public function testGetAuthorityWithUsernameAndPassword() - { - $this->assertEquals('josh:sekrit@example.com', $this->uriFactory()->getAuthority()); - } - - public function testGetAuthorityWithUsername() - { - $scheme = 'https'; - $user = 'josh'; - $password = ''; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 443; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('josh@example.com', $uri->getAuthority()); - } - - public function testGetAuthority() - { - $scheme = 'https'; - $user = ''; - $password = ''; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 443; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('example.com', $uri->getAuthority()); - } - - public function testGetAuthorityWithNonStandardPort() - { - $scheme = 'https'; - $user = ''; - $password = ''; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 400; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('example.com:400', $uri->getAuthority()); - } - - public function testGetUserInfoWithUsernameAndPassword() - { - $scheme = 'https'; - $user = 'josh'; - $password = 'sekrit'; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 443; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('josh:sekrit', $uri->getUserInfo()); - } - - public function testGetUserInfoWithUsername() - { - $scheme = 'https'; - $user = 'josh'; - $password = ''; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 443; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('josh', $uri->getUserInfo()); - } - - public function testGetUserInfoNone() - { - $scheme = 'https'; - $user = ''; - $password = ''; - $host = 'example.com'; - $path = '/foo/bar'; - $port = 443; - $query = 'abc=123'; - $fragment = 'section3'; - $uri = new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $password); - - $this->assertEquals('', $uri->getUserInfo()); - } - - public function testWithUserInfo() - { - $uri = $this->uriFactory()->withUserInfo('bob', 'pass'); - - $this->assertAttributeEquals('bob', 'user', $uri); - $this->assertAttributeEquals('pass', 'password', $uri); - } - - public function testWithUserInfoRemovesPassword() - { - $uri = $this->uriFactory()->withUserInfo('bob'); - - $this->assertAttributeEquals('bob', 'user', $uri); - $this->assertAttributeEquals('', 'password', $uri); - } - - public function testGetHost() - { - $this->assertEquals('example.com', $this->uriFactory()->getHost()); - } - - public function testWithHost() - { - $uri = $this->uriFactory()->withHost('slimframework.com'); - - $this->assertAttributeEquals('slimframework.com', 'host', $uri); - } - - public function testGetPortWithSchemeAndNonDefaultPort() - { - $uri = new Uri('https', 'www.example.com', 4000); - - $this->assertEquals(4000, $uri->getPort()); - } - - public function testGetPortWithSchemeAndDefaultPort() - { - $uriHppt = new Uri('http', 'www.example.com', 80); - $uriHppts = new Uri('https', 'www.example.com', 443); - - $this->assertNull($uriHppt->getPort()); - $this->assertNull($uriHppts->getPort()); - } - - public function testGetPortWithoutSchemeAndPort() - { - $uri = new Uri('', 'www.example.com'); - - $this->assertNull($uri->getPort()); - } - - public function testGetPortWithSchemeWithoutPort() - { - $uri = new Uri('http', 'www.example.com'); - - $this->assertNull($uri->getPort()); - } - - public function testWithPort() - { - $uri = $this->uriFactory()->withPort(8000); - - $this->assertAttributeEquals(8000, 'port', $uri); - } - - public function testWithPortNull() - { - $uri = $this->uriFactory()->withPort(null); - - $this->assertAttributeEquals(null, 'port', $uri); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testWithPortInvalidInt() - { - $this->uriFactory()->withPort(70000); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testWithPortInvalidString() - { - $this->uriFactory()->withPort('Foo'); - } - - /******************************************************************************** - * Path - *******************************************************************************/ - - public function testGetPath() - { - $this->assertEquals('/foo/bar', $this->uriFactory()->getPath()); - } - - public function testWithPath() - { - $uri = $this->uriFactory()->withPath('/new'); - - $this->assertAttributeEquals('/new', 'path', $uri); - } - - public function testWithPathWithoutPrefix() - { - $uri = $this->uriFactory()->withPath('new'); - - $this->assertAttributeEquals('new', 'path', $uri); - } - - public function testWithPathEmptyValue() - { - $uri = $this->uriFactory()->withPath(''); - - $this->assertAttributeEquals('', 'path', $uri); - } - - public function testWithPathUrlEncodesInput() - { - $uri = $this->uriFactory()->withPath('/includes?/new'); - - $this->assertAttributeEquals('/includes%3F/new', 'path', $uri); - } - - public function testWithPathDoesNotDoubleEncodeInput() - { - $uri = $this->uriFactory()->withPath('/include%25s/new'); - - $this->assertAttributeEquals('/include%25s/new', 'path', $uri); - } - - /** - * @covers Slim\Http\Uri::withPath - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri path must be a string - */ - public function testWithPathInvalidType() - { - $this->uriFactory()->withPath(['foo']); - } - - /******************************************************************************** - * Query - *******************************************************************************/ - - public function testGetQuery() - { - $this->assertEquals('abc=123', $this->uriFactory()->getQuery()); - } - - public function testWithQuery() - { - $uri = $this->uriFactory()->withQuery('xyz=123'); - - $this->assertAttributeEquals('xyz=123', 'query', $uri); - } - - public function testWithQueryRemovesPrefix() - { - $uri = $this->uriFactory()->withQuery('?xyz=123'); - - $this->assertAttributeEquals('xyz=123', 'query', $uri); - } - - public function testWithQueryEmpty() - { - $uri = $this->uriFactory()->withQuery(''); - - $this->assertAttributeEquals('', 'query', $uri); - } - - public function testFilterQuery() - { - $uri = $this->uriFactory()->withQuery('?foobar=%match'); - - $this->assertAttributeEquals('foobar=%25match', 'query', $uri); - } - - /** - * @covers Slim\Http\Uri::withQuery - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri query must be a string - */ - public function testWithQueryInvalidType() - { - $this->uriFactory()->withQuery(['foo']); - } - - /******************************************************************************** - * Fragment - *******************************************************************************/ - - public function testGetFragment() - { - $this->assertEquals('section3', $this->uriFactory()->getFragment()); - } - - public function testWithFragment() - { - $uri = $this->uriFactory()->withFragment('other-fragment'); - - $this->assertAttributeEquals('other-fragment', 'fragment', $uri); - } - - public function testWithFragmentRemovesPrefix() - { - $uri = $this->uriFactory()->withFragment('#other-fragment'); - - $this->assertAttributeEquals('other-fragment', 'fragment', $uri); - } - - public function testWithFragmentEmpty() - { - $uri = $this->uriFactory()->withFragment(''); - - $this->assertAttributeEquals('', 'fragment', $uri); - } - - /** - * @covers Slim\Http\Uri::withFragment - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri fragment must be a string - */ - public function testWithFragmentInvalidType() - { - $this->uriFactory()->withFragment(['foo']); - } - - /******************************************************************************** - * Helpers - *******************************************************************************/ - - public function testToString() - { - $uri = $this->uriFactory(); - - $this->assertEquals('https://josh:sekrit@example.com/foo/bar?abc=123#section3', (string) $uri); - - $uri = $uri->withPath('bar'); - $this->assertEquals('https://josh:sekrit@example.com/bar?abc=123#section3', (string) $uri); - - $uri = $uri->withPath('/bar'); - $this->assertEquals('https://josh:sekrit@example.com/bar?abc=123#section3', (string) $uri); - - // ensure that a Uri with just a base path correctly converts to a string - // (This occurs via createFromEnvironment when index.php is in a subdirectory) - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/foo/index.php', - 'REQUEST_URI' => '/foo/', - 'HTTP_HOST' => 'example.com', - ]); - $uri = Uri::createFromEnvironment($environment); - $this->assertEquals('http://example.com/', (string) $uri); - } - - /** - * @covers Slim\Http\Uri::createFromString - */ - public function testCreateFromString() - { - $uri = Uri::createFromString('https://example.com:8080/foo/bar?abc=123'); - - $this->assertEquals('https', $uri->getScheme()); - $this->assertEquals('example.com', $uri->getHost()); - $this->assertEquals('8080', $uri->getPort()); - $this->assertEquals('/foo/bar', $uri->getPath()); - $this->assertEquals('abc=123', $uri->getQuery()); - } - - /** - * @covers Slim\Http\Uri::createFromString - * @expectedException InvalidArgumentException - * @expectedExceptionMessage Uri must be a string - */ - public function testCreateFromStringWithInvalidType() - { - Uri::createFromString(['https://example.com:8080/foo/bar?abc=123']); - } - - public function testCreateEnvironment() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo/bar', - 'PHP_AUTH_USER' => 'josh', - 'PHP_AUTH_PW' => 'sekrit', - 'QUERY_STRING' => 'abc=123', - 'HTTP_HOST' => 'example.com:8080', - 'SERVER_PORT' => 8080, - ]); - - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('josh:sekrit', $uri->getUserInfo()); - $this->assertEquals('example.com', $uri->getHost()); - $this->assertEquals('8080', $uri->getPort()); - $this->assertEquals('/foo/bar', $uri->getPath()); - $this->assertEquals('abc=123', $uri->getQuery()); - $this->assertEquals('', $uri->getFragment()); - } - - public function testCreateEnvironmentWithIPv6Host() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo/bar', - 'PHP_AUTH_USER' => 'josh', - 'PHP_AUTH_PW' => 'sekrit', - 'QUERY_STRING' => 'abc=123', - 'HTTP_HOST' => '[2001:db8::1]:8080', - 'REMOTE_ADDR' => '2001:db8::1', - 'SERVER_PORT' => 8080, - ]); - - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('josh:sekrit', $uri->getUserInfo()); - $this->assertEquals('[2001:db8::1]', $uri->getHost()); - $this->assertEquals('8080', $uri->getPort()); - $this->assertEquals('/foo/bar', $uri->getPath()); - $this->assertEquals('abc=123', $uri->getQuery()); - $this->assertEquals('', $uri->getFragment()); - } - - public function testCreateEnvironmentWithBasePathContainingSpace() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => "/f'oo bar/index.php", - 'REQUEST_URI' => "/f%27oo%20bar/baz", - ]); - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('baz', $uri->getPath()); - } - - public function testGetBaseUrl() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/foo/index.php', - 'REQUEST_URI' => '/foo/bar', - 'QUERY_STRING' => 'abc=123', - 'HTTP_HOST' => 'example.com:80', - 'SERVER_PORT' => 80 - ]); - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('http://example.com', $uri->getBaseUrl()); - } - - public function testGetBaseUrlWithNoBasePath() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo/bar', - 'QUERY_STRING' => 'abc=123', - 'HTTP_HOST' => 'example.com:80', - 'SERVER_PORT' => 80 - ]); - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('http://example.com', $uri->getBaseUrl()); - } - - public function testGetBaseUrlWithAuthority() - { - $environment = Environment::mock([ - 'SCRIPT_NAME' => '/foo/index.php', - 'REQUEST_URI' => '/foo/bar', - 'PHP_AUTH_USER' => 'josh', - 'PHP_AUTH_PW' => 'sekrit', - 'QUERY_STRING' => 'abc=123', - 'HTTP_HOST' => 'example.com:8080', - 'SERVER_PORT' => 8080 - ]); - $uri = Uri::createFromEnvironment($environment); - - $this->assertEquals('http://josh:sekrit@example.com:8080', $uri->getBaseUrl()); - } - - /** - * @covers Slim\Http\Uri::createFromEnvironment - * @ticket 1380 - */ - public function testWithPathWhenBaseRootIsEmpty() - { - $environment = \Slim\Http\Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/bar', - ]); - $uri = \Slim\Http\Uri::createFromEnvironment($environment); - - $this->assertEquals('http://localhost/test', (string) $uri->withPath('test')); - } - - /** - * When the URL is /foo/index.php/bar/baz, we need the baseURL to be - * /foo/index.php so that routing works correctly. - * - * @ticket 1639 as a fix to 1590 broke this. - */ - public function testRequestURIContainsIndexDotPhp() - { - $uri = Uri::createFromEnvironment( - Environment::mock( - [ - 'SCRIPT_NAME' => '/foo/index.php', - 'REQUEST_URI' => '/foo/index.php/bar/baz', - ] - ) - ); - $this->assertSame('bar/baz', $uri->getPath()); - } - - public function testRequestURICanContainParams() - { - $uri = Uri::createFromEnvironment( - Environment::mock( - [ - 'REQUEST_URI' => '/foo?abc=123', - ] - ) - ); - $this->assertEquals('abc=123', $uri->getQuery()); - } -} From 0da35de4e4593e6bdc81eafe6908ed6ec111538d Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 17 May 2017 13:06:40 -0600 Subject: [PATCH 33/36] Fix broken tests from upstream merge --- .../ErrorRenderers/PlainTextErrorRender.php | 25 +++++++++++++++++++ tests/ContainerTest.php | 6 ++--- tests/Exception/HttpExceptionTest.php | 12 +++------ 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php new file mode 100644 index 000000000..742547858 --- /dev/null +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php @@ -0,0 +1,25 @@ +exception->getMessage(); + } + + public function renderGenericOutput() + { + return $this->exception->getMessage(); + } +} diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 453889d96..a96a16b4a 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase; use Slim\Container; use Psr\Container\ContainerInterface; +use Slim\Tests\Mocks\MockErrorRenderer; class ContainerTest extends TestCase { @@ -29,11 +30,10 @@ public function setUp() */ public function testGet() { - $this->assertInstanceOf('\Slim\Handlers\NotFound', $this->container->get('notFoundHandler')); + $this->container['MockErrorRenderer'] = new MockErrorRenderer(new \Exception('oops')); + $this->assertInstanceOf(MockErrorRenderer::class, $this->container->get('MockErrorRenderer')); } - - /** * Test `get()` throws error if item does not exist * diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php index d845e3739..df6e897e0 100644 --- a/tests/Exception/HttpExceptionTest.php +++ b/tests/Exception/HttpExceptionTest.php @@ -16,7 +16,6 @@ use Slim\Http\Response; use Slim\Http\Uri; use Slim\Http\Headers; -use Slim\Http\Environment; class HttpExceptionTest extends TestCase { @@ -33,15 +32,10 @@ public function testThatConstructorWillSetDetailsAccordingly() public function testHttpExceptionRequestReponseGetterSetters() { // Prepare request and response objects - $env = Environment::mock([ - 'SCRIPT_NAME' => '/index.php', - 'REQUEST_URI' => '/foo', - 'REQUEST_METHOD' => 'GET', - ]); - $uri = Uri::createFromEnvironment($env); - $headers = Headers::createFromEnvironment($env); + $uri = Uri::createFromGlobals($_SERVER); + $headers = Headers::createFromGlobals($_SERVER); $cookies = []; - $serverParams = $env->all(); + $serverParams = $_SERVER; $body = new RequestBody(); $request = new Request('GET', $uri, $headers, $cookies, $serverParams, $body); $response = new Response(); From 8dbbaac42064eda6dab094598acc54c1b88c2535 Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Wed, 17 May 2017 13:17:04 -0600 Subject: [PATCH 34/36] Remove unused file. --- .../ErrorRenderers/PlainTextErrorRender.php | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php deleted file mode 100644 index 742547858..000000000 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRender.php +++ /dev/null @@ -1,25 +0,0 @@ -exception->getMessage(); - } - - public function renderGenericOutput() - { - return $this->exception->getMessage(); - } -} From 2d2411654a914bed5eeec05b583acc70c3bb38ed Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Mon, 12 Jun 2017 14:29:58 -0600 Subject: [PATCH 35/36] Refactor changes requested by Akrabat --- Slim/App.php | 43 +------------ Slim/Exception/PhpException.php | 14 ----- Slim/Handlers/AbstractErrorHandler.php | 8 +-- Slim/Handlers/AbstractErrorRenderer.php | 13 +--- .../ErrorRenderers/HtmlErrorRenderer.php | 32 +++------- .../ErrorRenderers/JsonErrorRenderer.php | 19 +----- .../ErrorRenderers/PlainTextErrorRenderer.php | 21 ++----- .../ErrorRenderers/XmlErrorRenderer.php | 12 +--- Slim/Interfaces/ErrorHandlerInterface.php | 5 +- Slim/Interfaces/ErrorRendererInterface.php | 10 --- tests/AppTest.php | 13 ---- tests/ContainerTest.php | 2 +- tests/Handlers/AbstractErrorHandlerTest.php | 2 +- tests/Handlers/AbstractErrorRendererTest.php | 61 ++++++------------- tests/Handlers/ErrorHandlerTest.php | 6 +- tests/Mocks/MockErrorRenderer.php | 10 ++- 16 files changed, 58 insertions(+), 213 deletions(-) delete mode 100644 Slim/Exception/PhpException.php diff --git a/Slim/App.php b/Slim/App.php index 8e2e55ce7..0f24021e4 100644 --- a/Slim/App.php +++ b/Slim/App.php @@ -15,7 +15,6 @@ use Slim\Exception\HttpException; use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotAllowedException; -use Slim\Exception\PhpException; use Slim\Handlers\ErrorHandler; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\ErrorHandlerInterface; @@ -439,42 +438,6 @@ public function getNotAllowedHandler() return $this->getErrorHandler(HttpNotAllowedException::class); } - /** - * Set callable to handle scenarios where a PHP error - * occurs when processing the current request. - * - * 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 \Exception - * 4. Boolean displayErrorDetails (optional) - * - * Or a subclass name of AbstractErrorHandler - * eg. CustomErrorHandler::class - * - * The callable MUST return an instance of - * \Psr\Http\Message\ResponseInterface. - * - * @param callable $handler - */ - public function setPhpErrorHandler($handler) - { - $this->setErrorHandler(PhpException::class, $handler); - } - - /** - * Get callable to handle scenarios where a PHP error - * occurs when processing the current request. - * - * @return callable - */ - public function getPhpErrorHandler() - { - return $this->getErrorHandler(PhpException::class); - } - /******************************************************************************** * Router proxy methods *******************************************************************************/ @@ -648,7 +611,7 @@ public function run($silent = false) } catch (Exception $e) { $response = $this->handleException($e, $request, $response); } catch (Throwable $e) { - $response = $this->handleException(new PhpException($e), $request, $response); + $response = $this->handleException($e, $request, $response); } if (!$silent) { @@ -827,12 +790,12 @@ protected function dispatchRouterAndPrepareRoute(ServerRequestInterface $request /** * Resolve custom error handler from container or use default ErrorHandler - * @param Exception $exception + * @param Exception|Throwable $exception * @param ServerRequestInterface $request * @param ResponseInterface $response * @return mixed */ - public function handleException(Exception $exception, ServerRequestInterface $request, ResponseInterface $response) + public function handleException($exception, ServerRequestInterface $request, ResponseInterface $response) { $exceptionType = get_class($exception); $handler = $this->getErrorHandler($exceptionType); diff --git a/Slim/Exception/PhpException.php b/Slim/Exception/PhpException.php deleted file mode 100644 index 6e96e26a4..000000000 --- a/Slim/Exception/PhpException.php +++ /dev/null @@ -1,14 +0,0 @@ -displayErrorDetails = $displayErrorDetails; $this->request = $request; diff --git a/Slim/Handlers/AbstractErrorRenderer.php b/Slim/Handlers/AbstractErrorRenderer.php index e55cc81ed..bcdf48bb2 100644 --- a/Slim/Handlers/AbstractErrorRenderer.php +++ b/Slim/Handlers/AbstractErrorRenderer.php @@ -8,7 +8,6 @@ */ namespace Slim\Handlers; -use Slim\Exception\PhpException; use Slim\Http\Body; use Slim\Interfaces\ErrorRendererInterface; @@ -34,22 +33,12 @@ abstract class AbstractErrorRenderer implements ErrorRendererInterface * @param \Exception|\Throwable $exception * @param bool $displayErrorDetails */ - public function __construct($exception, $displayErrorDetails = false) + public function __construct($exception, $displayErrorDetails) { $this->exception = $exception; $this->displayErrorDetails = $displayErrorDetails; } - /** - * @return string - */ - public function render() - { - return $this->exception instanceof PhpException - ? $this->renderPhpExceptionOutput() - : $this->renderGenericExceptionOutput(); - } - /** * @return Body */ diff --git a/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php index b84316a2c..8c1747918 100644 --- a/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/HtmlErrorRenderer.php @@ -16,7 +16,10 @@ */ class HtmlErrorRenderer extends AbstractErrorRenderer { - public function renderPhpExceptionOutput() + /** + * @return string + */ + public function render() { $e = $this->exception; $title = 'Slim Application Error'; @@ -32,26 +35,11 @@ public function renderPhpExceptionOutput() return $this->renderHtmlBody($title, $html); } - public function renderGenericExceptionOutput() - { - $e = $this->exception; - $title = ''; - $description = ''; - - if (method_exists($e, 'getTitle')) { - $title = $e->getTitle(); - } - - if (method_exists($e, 'getDescription')) { - $description = $e->getDescription(); - } elseif ($this->displayErrorDetails) { - $description = $e->getMessage(); - } - $html = "

${description}

"; - - return $this->renderHtmlBody($title, $html); - } - + /** + * @param string $title + * @param string $html + * @return string + */ public function renderHtmlBody($title = '', $html = '') { return sprintf( @@ -81,7 +69,7 @@ public function renderHtmlBody($title = '', $html = '') * @param Exception $exception * @return string */ - public function renderExceptionFragment($exception) + private function renderExceptionFragment($exception) { $html = sprintf('
Type: %s
', get_class($exception)); diff --git a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php index 1f231c246..6e3f715e4 100644 --- a/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/JsonErrorRenderer.php @@ -15,26 +15,13 @@ */ class JsonErrorRenderer extends AbstractErrorRenderer { - public function renderPhpExceptionOutput() - { - $message = 'Slim Application Error'; - return $this->formatExceptionPayload($message); - } - - public function renderGenericExceptionOutput() - { - $message = $this->exception->getMessage(); - return $this->formatExceptionPayload($message); - } - /** - * @param $message * @return string */ - public function formatExceptionPayload($message) + public function render() { $e = $this->exception; - $error = ['message' => $message]; + $error = ['message' => $e->getMessage()]; if ($this->displayErrorDetails) { $error['exception'] = []; @@ -50,7 +37,7 @@ public function formatExceptionPayload($message) * @param \Exception|\Throwable $e * @return array */ - public function formatExceptionFragment($e) + private function formatExceptionFragment($e) { return [ 'type' => get_class($e), diff --git a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php index 2f7e33116..fa876d75a 100644 --- a/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/PlainTextErrorRenderer.php @@ -15,21 +15,10 @@ */ class PlainTextErrorRenderer extends AbstractErrorRenderer { - public function renderPhpExceptionOutput() - { - return $this->formatExceptionBody(); - } - - public function renderGenericExceptionOutput() - { - if ($this->displayErrorDetails) { - return $this->formatExceptionBody(); - } - - return $this->exception->getMessage(); - } - - public function formatExceptionBody() + /** + * @return string + */ + public function render() { $e = $this->exception; @@ -48,7 +37,7 @@ public function formatExceptionBody() * @param \Exception|\Throwable $e * @return string */ - public function formatExceptionFragment($e) + private function formatExceptionFragment($e) { $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); diff --git a/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php index 6a05c928b..ed83742e2 100644 --- a/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php +++ b/Slim/Handlers/ErrorRenderers/XmlErrorRenderer.php @@ -18,10 +18,10 @@ class XmlErrorRenderer extends AbstractErrorRenderer /** * @return string */ - public function renderPhpExceptionOutput() + public function render() { $e = $this->exception; - $xml = "\n Slim Application Error\n"; + $xml = "\n {$e->getMessage()}\n"; if ($this->displayErrorDetails) { do { $xml .= " \n"; @@ -38,14 +38,6 @@ public function renderPhpExceptionOutput() return $xml; } - /** - * @return string - */ - public function renderGenericExceptionOutput() - { - return "{$this->exception->getMessage()}"; - } - /** * Returns a CDATA section with the given content. * diff --git a/Slim/Interfaces/ErrorHandlerInterface.php b/Slim/Interfaces/ErrorHandlerInterface.php index 10a2ac18b..f3db465c5 100644 --- a/Slim/Interfaces/ErrorHandlerInterface.php +++ b/Slim/Interfaces/ErrorHandlerInterface.php @@ -11,6 +11,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Exception; +use Throwable; /** * ErrorHandlerInterface @@ -23,14 +24,14 @@ interface ErrorHandlerInterface /** * @param ServerRequestInterface $request * @param ResponseInterface $response - * @param Exception $exception + * @param Exception|Throwable $exception * @param $displayErrorDetails * @return ResponseInterface */ public function __invoke( ServerRequestInterface $request, ResponseInterface $response, - Exception $exception, + $exception, $displayErrorDetails ); } diff --git a/Slim/Interfaces/ErrorRendererInterface.php b/Slim/Interfaces/ErrorRendererInterface.php index b5643b6f6..20bd925e3 100644 --- a/Slim/Interfaces/ErrorRendererInterface.php +++ b/Slim/Interfaces/ErrorRendererInterface.php @@ -31,14 +31,4 @@ public function render(); * @return \Slim\Http\Body */ public function renderWithBody(); - - /** - * @return string - */ - public function renderPhpExceptionOutput(); - - /** - * @return string - */ - public function renderGenericExceptionOutput(); } diff --git a/tests/AppTest.php b/tests/AppTest.php index 50536957f..94903becb 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -10,13 +10,11 @@ namespace Slim\Tests; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Slim\App; use Slim\Container; use Slim\Exception\HttpNotAllowedException; use Slim\Exception\HttpNotFoundException; -use Slim\Exception\PhpException; use Slim\Handlers\ErrorHandler; use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; use Slim\Handlers\Strategies\RequestResponseArgs; @@ -1064,20 +1062,9 @@ public function testErrorHandlerShortcuts() $handler = new MockErrorHandler(); $app->setNotAllowedHandler($handler); $app->setNotFoundHandler($handler); - $app->setPhpErrorHandler($handler); $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotAllowedException::class)); $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(HttpNotFoundException::class)); - $this->assertInstanceOf(MockErrorHandler::class, $app->getErrorHandler(PhpException::class)); - } - - public function testGetErrorHandlerWillReturnDefaultErrorHandlerForPhpExceptions() - { - $app = new App(); - $exception = PhpException::class; - $handler = $app->getErrorHandler($exception); - - $this->assertInstanceOf(ErrorHandler::class, $handler); } public function testGetErrorHandlerWillReturnDefaultErrorHandlerForUnhandledExceptions() diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index a96a16b4a..534a968b8 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -30,7 +30,7 @@ public function setUp() */ public function testGet() { - $this->container['MockErrorRenderer'] = new MockErrorRenderer(new \Exception('oops')); + $this->container['MockErrorRenderer'] = new MockErrorRenderer(new \Exception('oops'), false); $this->assertInstanceOf(MockErrorRenderer::class, $this->container->get('MockErrorRenderer')); } diff --git a/tests/Handlers/AbstractErrorHandlerTest.php b/tests/Handlers/AbstractErrorHandlerTest.php index 7d3dd253e..d03277036 100644 --- a/tests/Handlers/AbstractErrorHandlerTest.php +++ b/tests/Handlers/AbstractErrorHandlerTest.php @@ -113,7 +113,7 @@ public function testOptions() $exception = new HttpNotAllowedException(); $exception->setAllowedMethods(['POST', 'PUT']); /** @var Response $res */ - $res = $handler->__invoke($this->getRequest('OPTIONS'), new Response(), $exception); + $res = $handler->__invoke($this->getRequest('OPTIONS'), new Response(), $exception, false); $this->assertSame(200, $res->getStatusCode()); $this->assertTrue($res->hasHeader('Allow')); $this->assertEquals('POST, PUT', $res->getHeaderLine('Allow')); diff --git a/tests/Handlers/AbstractErrorRendererTest.php b/tests/Handlers/AbstractErrorRendererTest.php index ce65220e3..3c27e0519 100644 --- a/tests/Handlers/AbstractErrorRendererTest.php +++ b/tests/Handlers/AbstractErrorRendererTest.php @@ -9,41 +9,18 @@ namespace Slim\Tests\Handlers; use PHPUnit\Framework\TestCase; -use Slim\Exception\PhpException; use Slim\Handlers\ErrorRenderers\HtmlErrorRenderer; use Slim\Handlers\ErrorRenderers\JsonErrorRenderer; -use Slim\Handlers\ErrorRenderers\PlainTextErrorRenderer; use Slim\Handlers\ErrorRenderers\XmlErrorRenderer; use Exception; +use ReflectionClass; use RuntimeException; class AbstractErrorRendererTest extends TestCase { - public function testPlainTextErrorRenderDoesNotDisplayErrorDetails() - { - $exception = new Exception('Oops..'); - $renderer = new PlainTextErrorRenderer($exception); - - $this->assertEquals('Oops..', $renderer->render()); - } - - public function testHTMLErrorRendererOutputForPhpExceptions() - { - $exception = new Exception('Oops..'); - $genericRenderer = new HtmlErrorRenderer($exception); - $genericOutput = $genericRenderer->render(); - - $phpException = new PhpException(new RuntimeException('Oops..')); - $phpExceptionRenderer = new HtmlErrorRenderer($phpException); - $phpExceptionOutput = $phpExceptionRenderer->render(); - - $this->assertNotEquals($genericOutput, $phpExceptionOutput); - $this->assertRegExp('/.*Slim Application Error.*/', $phpExceptionOutput); - } - public function testHTMLErrorRendererDisplaysErrorDetails() { - $exception = new PhpException(new RuntimeException('Oops..')); + $exception = new RuntimeException('Oops..'); $renderer = new HtmlErrorRenderer($exception, true); $output = $renderer->render(); @@ -54,7 +31,10 @@ public function testHTMLErrorRendererRenderFragmentMethod() { $exception = new Exception('Oops..', 500); $renderer = new HtmlErrorRenderer($exception, true); - $output = $renderer->renderExceptionFragment($exception); + $reflectionRenderer = new ReflectionClass(HtmlErrorRenderer::class); + $method = $reflectionRenderer->getMethod('renderExceptionFragment'); + $method->setAccessible(true); + $output = $method->invoke($renderer, $exception); $this->assertRegExp('/.*Type:*/', $output); $this->assertRegExp('/.*Code:*/', $output); @@ -63,19 +43,14 @@ public function testHTMLErrorRendererRenderFragmentMethod() $this->assertRegExp('/.*Line*/', $output); } - public function testJSONErrorRendererPhpOutputForPhpExceptions() - { - $exception = new PhpException(new RuntimeException('Oops..')); - $renderer = new JsonErrorRenderer($exception); - $output = $renderer->renderPhpExceptionOutput(); - $this->assertRegExp('/.*Slim Application Error.*/', $output); - } - public function testJSONErrorRendererDisplaysErrorDetails() { $exception = new Exception('Oops..'); $renderer = new JsonErrorRenderer($exception, true); - $fragment = $renderer->formatExceptionFragment($exception); + $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class); + $method = $reflectionRenderer->getMethod('formatExceptionFragment'); + $method->setAccessible(true); + $fragment = $method->invoke($renderer, $exception); $output = json_encode(json_decode($renderer->render())); $expectedString = json_encode(['message' => 'Oops..', 'exception' => [$fragment]]); @@ -85,9 +60,8 @@ public function testJSONErrorRendererDisplaysErrorDetails() public function testJSONErrorRendererDoesNotDisplayErrorDetails() { $exception = new Exception('Oops..'); - $renderer = new JsonErrorRenderer($exception); + $renderer = new JsonErrorRenderer($exception, false); $output = json_encode(json_decode($renderer->render())); - $this->assertEquals($output, json_encode(['message' => 'Oops..'])); } @@ -96,11 +70,14 @@ public function testJSONErrorRendererDisplaysPreviousError() $previousException = new Exception('Oh no!'); $exception = new Exception('Oops..', 0, $previousException); $renderer = new JsonErrorRenderer($exception, true); + $reflectionRenderer = new ReflectionClass(JsonErrorRenderer::class); + $method = $reflectionRenderer->getMethod('formatExceptionFragment'); + $method->setAccessible(true); $output = json_encode(json_decode($renderer->render())); $fragments = [ - $renderer->formatExceptionFragment($exception), - $renderer->formatExceptionFragment($previousException), + $method->invoke($renderer, $exception), + $method->invoke($renderer, $previousException), ]; $expectedString = json_encode(['message' => 'Oops..', 'exception' => $fragments]); @@ -111,12 +88,12 @@ public function testJSONErrorRendererDisplaysPreviousError() public function testXMLErrorRendererDisplaysErrorDetails() { $previousException = new RuntimeException('Oops..'); - $exception = new PhpException($previousException); + $exception = new Exception('Ooops...', 0, $previousException); $renderer = new XmlErrorRenderer($exception, true); $output = simplexml_load_string($renderer->render()); - $this->assertEquals($output->message[0], 'Slim Application Error'); - $this->assertEquals((string)$output->exception[0]->type, 'Slim\Exception\PhpException'); + $this->assertEquals($output->message[0], 'Ooops...'); + $this->assertEquals((string)$output->exception[0]->type, 'Exception'); $this->assertEquals((string)$output->exception[1]->type, 'RuntimeException'); } } diff --git a/tests/Handlers/ErrorHandlerTest.php b/tests/Handlers/ErrorHandlerTest.php index b165e8a81..cb1751030 100644 --- a/tests/Handlers/ErrorHandlerTest.php +++ b/tests/Handlers/ErrorHandlerTest.php @@ -38,7 +38,7 @@ public function testErrorHandler($acceptHeader, $contentType, $startOfBody) $e = new Exception("Oops", 1, new Exception('Previous oops')); /** @var Response $res */ - $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e, false); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); @@ -56,7 +56,7 @@ public function testErrorHandlerDisplayDetails($acceptHeader, $contentType, $sta $e = new Exception('Oops', 1, new Exception('Oops before')); /** @var Response $res */ - $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e); + $res = $errorHandler->__invoke($this->getRequest('GET', $acceptHeader), new Response(), $e, true); $this->assertSame(500, $res->getStatusCode()); $this->assertSame($contentType, $res->getHeaderLine('Content-Type')); @@ -80,7 +80,7 @@ public function testPreviousException() $first = new Exception("First Oops"); $second = new Exception("Second Oops", 0, $first); - $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second); + $error->__invoke($this->getRequest('GET', 'application/json'), new Response(), $second, false); } /** diff --git a/tests/Mocks/MockErrorRenderer.php b/tests/Mocks/MockErrorRenderer.php index 5e53b8ec9..4839cbf13 100644 --- a/tests/Mocks/MockErrorRenderer.php +++ b/tests/Mocks/MockErrorRenderer.php @@ -15,12 +15,10 @@ */ class MockErrorRenderer extends AbstractErrorRenderer { - public function renderPhpExceptionOutput() - { - return ''; - } - - public function renderGenericExceptionOutput() + /** + * @return string + */ + public function render() { return ''; } From 24ad11bb9bdb814377a18a016795403d140ee22d Mon Sep 17 00:00:00 2001 From: l0gicgate Date: Mon, 12 Jun 2017 14:42:08 -0600 Subject: [PATCH 36/36] Fix Travis CI config file for HHVM build requirements --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4050d8c82..2b6eb897f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,3 +22,5 @@ after_script: notifications: slack: slimphp:0RNzx2JuhkAqIf0MXcUZ0asT + +dist: trusty