diff --git a/src/ProblemDetailsResponseFactory.php b/src/ProblemDetailsResponseFactory.php index 48a8b61..ebfe5f5 100644 --- a/src/ProblemDetailsResponseFactory.php +++ b/src/ProblemDetailsResponseFactory.php @@ -167,17 +167,40 @@ class ProblemDetailsResponseFactory */ private $response; + /** + * Flag to enable show exception details in detail field. + * + * Disabled by default for security reasons. + * + * @var bool + */ + private $exceptionDetailsInResponse; + + /** + * Default detail field value. Will be visible when + * $exceptionDetailsInResponse disabled. + * + * Empty string by default + * + * @var string + */ + private $defaultDetailMessage; + public function __construct( bool $isDebug = self::EXCLUDE_THROWABLE_DETAILS, int $jsonFlags = null, ResponseInterface $response = null, - callable $bodyFactory = null + callable $bodyFactory = null, + bool $exceptionDetailsInResponse = false, + string $defaultDetailMessage = '' ) { $this->isDebug = $isDebug; $this->jsonFlags = $jsonFlags ?: JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION; $this->response = $response ?: new Response(); $this->bodyFactory = $bodyFactory ?: Closure::fromCallable([$this, 'generateStream']); + $this->exceptionDetailsInResponse = $exceptionDetailsInResponse; + $this->defaultDetailMessage = $defaultDetailMessage; } public function createResponse( @@ -224,18 +247,27 @@ public function createResponseFromThrowable( ); } + $detail = $this->isDebug || $this->exceptionDetailsInResponse ? $e->getMessage() : $this->defaultDetailMessage; $additionalDetails = $this->isDebug ? $this->createThrowableDetail($e) : []; - $code = is_int($e->getCode()) ? $e->getCode() : 0; + $code = $this->isDebug || $this->exceptionDetailsInResponse ? $this->getThrowableCode($e) : 500; + return $this->createResponse( $request, $code, - $e->getMessage(), + $detail, '', '', $additionalDetails ); } + protected function getThrowableCode(Throwable $e) : int + { + $code = $e->getCode(); + + return is_int($code) ? $code : 0; + } + protected function generateJsonResponse(array $payload) : ResponseInterface { return $this->generateResponse( @@ -340,7 +372,7 @@ private function createThrowableDetail(Throwable $e) : array ]; } - if (count($previous) > 0) { + if (! empty($previous)) { $detail['stack'] = $previous; } diff --git a/test/ProblemDetailsResponseFactoryTest.php b/test/ProblemDetailsResponseFactoryTest.php index 08e691d..9d85f70 100644 --- a/test/ProblemDetailsResponseFactoryTest.php +++ b/test/ProblemDetailsResponseFactoryTest.php @@ -13,13 +13,22 @@ use RuntimeException; use Zend\ProblemDetails\Exception\InvalidResponseBodyException; use Zend\ProblemDetails\Exception\ProblemDetailsException; -use Zend\ProblemDetails\ProblemDetailsResponse; use Zend\ProblemDetails\ProblemDetailsResponseFactory; class ProblemDetailsResponseFactoryTest extends TestCase { use ProblemDetailsAssertionsTrait; + /** + * @var ServerRequestInterface + */ + private $request; + + /** + * @var ProblemDetailsResponseFactory + */ + private $factory; + protected function setUp() : void { $this->request = $this->prophesize(ServerRequestInterface::class); @@ -118,6 +127,7 @@ public function testCreateResponseFromThrowableWillPullDetailsFromProblemDetails $payload = $this->getPayloadFromResponse($response); $this->assertSame(400, $payload['status']); + $this->assertSame(400, $response->getStatusCode()); $this->assertSame('Exception details', $payload['detail']); $this->assertSame('Invalid client request', $payload['title']); $this->assertSame('https://example.com/api/doc/invalid-client-request', $payload['type']); @@ -176,4 +186,53 @@ public function testFactoryRendersPreviousExceptionsInDebugMode() : void $this->assertEquals(101010, $payload['exception']['stack'][0]['code']); $this->assertEquals('first', $payload['exception']['stack'][0]['message']); } + + public function testFragileDataInExceptionMessageShouldBeHiddenInResponseBodyInNoDebugMode() + { + $fragileMessage = 'Your SQL or password here'; + $exception = new \Exception($fragileMessage); + + $response = $this->factory->createResponseFromThrowable($this->request->reveal(), $exception); + + $this->assertNotContains($fragileMessage, (string) $response->getBody()); + } + + public function testExceptionCodeShouldBeIgnoredAnd500ServedInResponseBodyInNoDebugMode() + { + $exception = new \Exception(null, 400); + + $response = $this->factory->createResponseFromThrowable($this->request->reveal(), $exception); + + $payload = $this->getPayloadFromResponse($response); + + $this->assertSame(500, $payload['status']); + $this->assertSame(500, $response->getStatusCode()); + } + + public function testFragileDataInExceptionMessageShouldBeVisibleInResponseBodyInNoneDebugModeWhenAllowToShowByFlag() + { + $fragileMessage = 'Your SQL or password here'; + $exception = new \Exception($fragileMessage); + + $factory = new ProblemDetailsResponseFactory(false, null, null, null, true); + + $response = $factory->createResponseFromThrowable($this->request->reveal(), $exception); + + $payload = $this->getPayloadFromResponse($response); + + $this->assertSame($fragileMessage, $payload['detail']); + } + + public function testCustomDetailMessageShouldBeVisible() + { + $detailMessage = 'Custom detail message'; + + $factory = new ProblemDetailsResponseFactory(false, null, null, null, false, $detailMessage); + + $response = $factory->createResponseFromThrowable($this->request->reveal(), new \Exception()); + + $payload = $this->getPayloadFromResponse($response); + + $this->assertSame($detailMessage, $payload['detail']); + } }