diff --git a/README.md b/README.md index 3abdfb7..0e4cd82 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,15 @@ $app->run($request, $response); You don't need to copy any static assets from phpdebugbar vendor! +### How to force disable or enable PHP Debug Bar? + +Sometimes you want to have control when enable (or not) PHP Debug Bar: +* custom content negotiation, +* allow to debug redirects responses. + +We allow you to disable attaching phpdebugbar using `X-Enable-Debug-Bar: false` header, cookie or request attribute. +To force enable just send request with `X-Enable-Debug-Bar` header, cookie or request attribute with `true` value. + ### How to install on Zend Expressive? You need to register ConfigProvider and pipe provided middleware: diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 018c386..fd20daa 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -7,8 +7,7 @@ final class ConfigProvider { public static function getConfig(): array { - $self = new self(); - return $self(); + return (new self())(); } public function __invoke(): array diff --git a/src/JavascriptRendererFactory.php b/src/JavascriptRendererFactory.php index 5d961cd..6fa7ec4 100644 --- a/src/JavascriptRendererFactory.php +++ b/src/JavascriptRendererFactory.php @@ -12,8 +12,7 @@ final class JavascriptRendererFactory public function __invoke(ContainerInterface $container = null): JavascriptRenderer { if ($container === null || !$container->has(DebugBar::class)) { - $standardDebugBarFactory = new StandardDebugBarFactory(); - $debugbar = $standardDebugBarFactory($container); + $debugbar = (new StandardDebugBarFactory())($container); } else { $debugbar = $container->get(DebugBar::class); } diff --git a/src/PhpDebugBarMiddleware.php b/src/PhpDebugBarMiddleware.php index c29defd..5331054 100644 --- a/src/PhpDebugBarMiddleware.php +++ b/src/PhpDebugBarMiddleware.php @@ -5,13 +5,13 @@ use DebugBar\JavascriptRenderer as DebugBarRenderer; use Psr\Http\Message\MessageInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as ServerRequest; use Psr\Http\Message\UriInterface; use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Slim\Http\Uri; -use Zend\Diactoros\Response; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; +use Slim\Http\Uri as SlimUri; +use Zend\Diactoros\Response as DiactorosResponse; use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\Serializer; use Zend\Diactoros\Stream; @@ -23,6 +23,8 @@ */ final class PhpDebugBarMiddleware implements MiddlewareInterface { + public const FORCE_KEY = 'X-Enable-Debug-Bar'; + protected $debugBarRenderer; public function __construct(DebugBarRenderer $debugbarRenderer) @@ -33,7 +35,7 @@ public function __construct(DebugBarRenderer $debugbarRenderer) /** * @inheritDoc */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequest $request, RequestHandler $handler): Response { if ($staticFile = $this->getStaticFile($request->getUri())) { return $staticFile; @@ -41,7 +43,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $response = $handler->handle($request); - if (!$this->isHtmlAccepted($request)) { + $forceHeaderValue = $request->getHeaderLine(self::FORCE_KEY); + $forceCookieValue = $request->getCookieParams()[self::FORCE_KEY] ?? ''; + $forceAttibuteValue = $request->getAttribute(self::FORCE_KEY, ''); + $isForceEnable = in_array('true', [$forceHeaderValue, $forceCookieValue, $forceAttibuteValue], true); + $isForceDisable = in_array('false', [$forceHeaderValue, $forceCookieValue, $forceAttibuteValue], true); + + if ($isForceDisable || (!$isForceEnable && ($this->isRedirect($response) || !$this->isHtmlAccepted($request)))) { return $response; } @@ -51,19 +59,19 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $this->prepareHtmlResponseWithDebugBar($response); } - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface + public function __invoke(ServerRequest $request, Response $response, callable $next): Response { - $handler = new class($next, $response) implements RequestHandlerInterface { + $handler = new class($next, $response) implements RequestHandler { private $next; private $response; - public function __construct(callable $next, ResponseInterface $response) + public function __construct(callable $next, Response $response) { $this->next = $next; $this->response = $response; } - public function handle(ServerRequestInterface $request): ResponseInterface + public function handle(ServerRequest $request): Response { return ($this->next)($request, $this->response); } @@ -71,10 +79,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $this->process($request, $handler); } - /** - * @return HtmlResponse - */ - private function prepareHtmlResponseWithDebugBar(ResponseInterface $response) + private function prepareHtmlResponseWithDebugBar(Response $response): HtmlResponse { $head = $this->debugBarRenderer->renderHead(); $body = $this->debugBarRenderer->render(); @@ -86,10 +91,7 @@ private function prepareHtmlResponseWithDebugBar(ResponseInterface $response) return new HtmlResponse($result); } - /** - * @return ResponseInterface - */ - private function attachDebugBarToResponse(ResponseInterface $response) + private function attachDebugBarToResponse(Response $response): Response { $head = $this->debugBarRenderer->renderHead(); $body = $this->debugBarRenderer->render(); @@ -103,15 +105,12 @@ private function attachDebugBarToResponse(ResponseInterface $response) return $response; } - /** - * @return ResponseInterface|null - */ - private function getStaticFile(UriInterface $uri) + private function getStaticFile(UriInterface $uri): ?Response { $path = $this->extractPath($uri); if (strpos($path, $this->debugBarRenderer->getBaseUrl()) !== 0) { - return; + return null; } $pathToFile = substr($path, strlen($this->debugBarRenderer->getBaseUrl())); @@ -119,26 +118,21 @@ private function getStaticFile(UriInterface $uri) $fullPathToFile = $this->debugBarRenderer->getBasePath() . $pathToFile; if (!file_exists($fullPathToFile)) { - return; + return null; } $contentType = $this->getContentTypeByFileName($fullPathToFile); $stream = new Stream($fullPathToFile, 'r'); - return new Response($stream, 200, [ + return new DiactorosResponse($stream, 200, [ 'Content-type' => $contentType, ]); } - /** - * @param UriInterface $uri - * - * @return string - */ - private function extractPath(UriInterface $uri) + private function extractPath(UriInterface $uri): string { // Slim3 compatibility - if ($uri instanceof Uri) { + if ($uri instanceof SlimUri) { $basePath = $uri->getBasePath(); if (!empty($basePath)) { return $basePath; @@ -147,12 +141,7 @@ private function extractPath(UriInterface $uri) return $uri->getPath(); } - /** - * @param string $filename - * - * @return string - */ - private function getContentTypeByFileName($filename) + private function getContentTypeByFileName(string $filename): string { $ext = pathinfo($filename, PATHINFO_EXTENSION); @@ -170,35 +159,25 @@ private function getContentTypeByFileName($filename) return isset($map[$ext]) ? $map[$ext] : 'text/plain'; } - /** - * @param ResponseInterface $response - * - * @return bool - */ - private function isHtmlResponse(ResponseInterface $response) + private function isHtmlResponse(Response $response): bool { return $this->hasHeaderContains($response, 'Content-Type', 'text/html'); } - /** - * @param ServerRequestInterface $request - * - * @return bool - */ - private function isHtmlAccepted(ServerRequestInterface $request) + private function isHtmlAccepted(ServerRequest $request): bool { return $this->hasHeaderContains($request, 'Accept', 'text/html'); } - /** - * @param MessageInterface $message - * @param string $headerName - * @param string $value - * - * @return bool - */ - private function hasHeaderContains(MessageInterface $message, $headerName, $value) + private function hasHeaderContains(MessageInterface $message, string $headerName, string $value): bool { return strpos($message->getHeaderLine($headerName), $value) !== false; } + + private function isRedirect(Response $response): bool + { + $statusCode = $response->getStatusCode(); + + return ($statusCode >= 300 || $statusCode < 400) && $response->getHeaderLine('Location') !== ''; + } } diff --git a/src/PhpDebugBarMiddlewareFactory.php b/src/PhpDebugBarMiddlewareFactory.php index 8135d75..27dfea3 100644 --- a/src/PhpDebugBarMiddlewareFactory.php +++ b/src/PhpDebugBarMiddlewareFactory.php @@ -11,8 +11,7 @@ final class PhpDebugBarMiddlewareFactory public function __invoke(ContainerInterface $container = null): PhpDebugBarMiddleware { if ($container === null || !$container->has(JavascriptRenderer::class)) { - $rendererFactory = new JavascriptRendererFactory(); - $renderer = $rendererFactory($container); + $renderer = (new JavascriptRendererFactory())($container); } else { $renderer = $container->get(JavascriptRenderer::class); } diff --git a/src/ResponseInjector/AlwaysInjector.php b/src/ResponseInjector/AlwaysInjector.php deleted file mode 100644 index 1b84a42..0000000 --- a/src/ResponseInjector/AlwaysInjector.php +++ /dev/null @@ -1,40 +0,0 @@ -renderHead(); - $debugBarBody = $debugBarRenderer->render(); - - if ($this->isHtmlResponse($outResponse)) { - $body = $outResponse->getBody(); - if (! $body->eof() && $body->isSeekable()) { - $body->seek(0, SEEK_END); - } - $body->write($debugBarHead . $debugBarBody); - - return $outResponse; - } - - $outResponseBody = Serializer::toString($outResponse); - $template = '%s

DebugBar

Response:

%s
%s'; - $escapedOutResponseBody = htmlspecialchars($outResponseBody); - $result = sprintf($template, $debugBarHead, $escapedOutResponseBody, $debugBarBody); - - return new HtmlResponse($result); - } - - private function isHtmlResponse(ResponseInterface $response): bool - { - return $this->hasHeaderContains($response, 'Content-Type', 'text/html'); - } -} diff --git a/src/ResponseInjector/ResponseInjectorInterface.php b/src/ResponseInjector/ResponseInjectorInterface.php deleted file mode 100644 index 2c1910e..0000000 --- a/src/ResponseInjector/ResponseInjectorInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ -interface ResponseInjectorInterface -{ - public function injectPhpDebugBar(ResponseInterface $response, JavascriptRenderer $debugBar): ResponseInterface; -} diff --git a/src/StandardDebugBarFactory.php b/src/StandardDebugBarFactory.php index aedfb83..b21ac63 100644 --- a/src/StandardDebugBarFactory.php +++ b/src/StandardDebugBarFactory.php @@ -15,7 +15,7 @@ public function __invoke(ContainerInterface $container = null): StandardDebugBar if ($container !== null) { $config = $container->has('config') ? $container->get('config') : []; - $collectors = isset($config['phpmiddleware']['phpdebugbar']['collectors']) ? $config['phpmiddleware']['phpdebugbar']['collectors'] : []; + $collectors = $config['phpmiddleware']['phpdebugbar']['collectors'] ?: []; foreach ($collectors as $collectorName) { $collector = $container->get($collectorName); diff --git a/test/PhpDebugBarMiddlewareTest.php b/test/PhpDebugBarMiddlewareTest.php index f405259..9f912c7 100644 --- a/test/PhpDebugBarMiddlewareTest.php +++ b/test/PhpDebugBarMiddlewareTest.php @@ -24,6 +24,9 @@ class PhpDebugBarMiddlewareTest extends TestCase protected function setUp() { $this->debugbarRenderer = $this->getMockBuilder(JavascriptRenderer::class)->disableOriginalConstructor()->getMock(); + $this->debugbarRenderer->method('renderHead')->willReturn('RenderHead'); + $this->debugbarRenderer->method('render')->willReturn('RenderBody'); + $this->middleware = new PhpDebugBarMiddleware($this->debugbarRenderer); } @@ -31,6 +34,7 @@ public function testTwoPassCallingForCompatibility(): void { $request = new ServerRequest(); $response = new Response(); + $response->getBody()->write('ResponseBody'); $calledOut = false; $outFunction = function ($request, $response) use (&$calledOut) { $calledOut = true; @@ -40,6 +44,7 @@ public function testTwoPassCallingForCompatibility(): void $result = call_user_func($this->middleware, $request, $response, $outFunction); $this->assertTrue($calledOut, 'Out is not called'); + $this->assertSame('ResponseBody', (string) $result->getBody()); $this->assertSame($response, $result); } @@ -47,14 +52,54 @@ public function testNotAttachIfNotAccept(): void { $request = new ServerRequest(); $response = new Response(); + $response->getBody()->write('ResponseBody'); $requestHandler = new RequestHandlerStub($response); $result = $this->middleware->process($request, $requestHandler); $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame('ResponseBody', (string) $result->getBody()); $this->assertSame($response, $result); } + public function testForceAttachDebugbarIfHeaderPresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json', 'X-Enable-Debug-Bar' => 'true']); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + + public function testForceAttachDebugbarIfCookiePresents(): void + { + $cookies = ['X-Enable-Debug-Bar' => 'true']; + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json'], $cookies); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + + public function testForceAttachDebugbarIfAttributePresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'application/json']); + $request = $request->withAttribute('X-Enable-Debug-Bar', 'true'); + $response = new Response(); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); + } + public function testAttachToNoneHtmlResponse(): void { $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); @@ -63,9 +108,6 @@ public function testAttachToNoneHtmlResponse(): void $requestHandler = new RequestHandlerStub($response); - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); - $result = $this->middleware->process($request, $requestHandler); $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); @@ -73,6 +115,47 @@ public function testAttachToNoneHtmlResponse(): void $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 200 OK\r\n\r\nResponseBody
RenderBody", (string) $result->getBody()); } + public function testNotAttachToRedirectResponse(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(302)->withAddedHeader('Location', 'some-location'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + } + + public function testAttachToRedirectResponseWithoutLocation(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $response = (new Response())->withStatus(302); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertNotSame($response, $result); + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 302 Found\r\n\r\n
RenderBody", (string) $result->getBody()); + } + + public function testForceAttachToRedirectResponse(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Enable-Debug-Bar' => 'true']); + $response = (new Response())->withStatus(302)->withAddedHeader('Location', 'some-location'); + + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertNotSame($response, $result); + $this->assertSame("RenderHead

DebugBar

Response:

HTTP/1.1 302 Found\r\nLocation: some-location\r\n\r\n
RenderBody", (string) $result->getBody()); + } + public function testAttachToHtmlResponse(): void { $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); @@ -80,14 +163,55 @@ public function testAttachToHtmlResponse(): void $response->getBody()->write('ResponseBody'); $requestHandler = new RequestHandlerStub($response); - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBodyRenderHeadRenderBody', (string) $result->getBody()); + } + + public function testForceNotAttachDebugbarIfHeaderPresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Enable-Debug-Bar' => 'false']); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBody', (string) $result->getBody()); + } + + public function testForceNotAttachDebugbarIfCookiePresents(): void + { + $cookie = ['X-Enable-Debug-Bar' => 'false']; + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html'], $cookie); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); $result = $this->middleware->process($request, $requestHandler); $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); $this->assertSame($response, $result); - $this->assertSame("ResponseBodyRenderHeadRenderBody", (string) $result->getBody()); + $this->assertSame('ResponseBody', (string) $result->getBody()); + } + + public function testForceNotAttachDebugbarIfAttributePresents(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html']); + $request = $request->withAttribute('X-Enable-Debug-Bar', 'false'); + $response = new Response('php://memory', 200, ['Content-Type' => 'text/html']); + $response->getBody()->write('ResponseBody'); + $requestHandler = new RequestHandlerStub($response); + + $result = $this->middleware->process($request, $requestHandler); + + $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called'); + $this->assertSame($response, $result); + $this->assertSame('ResponseBody', (string) $result->getBody()); } public function testAppendsToEndOfHtmlResponse(): void @@ -97,9 +221,6 @@ public function testAppendsToEndOfHtmlResponse(): void $response = new Response\HtmlResponse($html); $requestHandler = new RequestHandlerStub($response); - $this->debugbarRenderer->expects($this->once())->method('renderHead')->willReturn('RenderHead'); - $this->debugbarRenderer->expects($this->once())->method('render')->willReturn('RenderBody'); - $result = $this->middleware->process($request, $requestHandler); $this->assertTrue($requestHandler->isCalled(), 'Request handler is not called');