diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b6a230..ece2bc5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,38 +1,57 @@ name: CI on: - - push + push: + branches: + - master + pull_request: jobs: phpstan: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: - php-version: 7.3 + php-version: '8.0' - name: Install dependencies with Composer - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v3 - name: Run phpstan run: vendor/bin/phpstan analyse --level=6 src/ tests: strategy: + fail-fast: false matrix: dependencies: - highest - lowest + # PHP 7.3 and 7.4 are not built here: no laminas-diactoros release + # that Composer 2.10+ accepts as a psr/http-factory-implementation + # provider supports them, so the dev dependencies cannot even be + # installed on those versions. php-versions: - - 7.3 - - 7.4 - - 8.0 - - 8.1 + - '8.0' + - '8.1' + - '8.2' + - '8.3' + exclude: + # Lowest bounds of the dev dependencies (Slim 3.0, Pimple 3.0, + # vfsStream 1.6.8) were released before PHP 8.1 and fatal on load + # there. That is a limitation of those old releases, not of this + # library: every tested PHP version is covered by the highest jobs. + - dependencies: lowest + php-versions: '8.1' + - dependencies: lowest + php-versions: '8.2' + - dependencies: lowest + php-versions: '8.3' runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -40,7 +59,7 @@ jobs: php-version: ${{ matrix.php-versions }} - name: Install dependencies with Composer - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v3 with: dependency-versions: ${{ matrix.dependencies }} diff --git a/README.md b/README.md index cf9b6ff..63e4e7a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,14 @@ Sometimes you want to have control when enable or disable PHP Debug Bar: 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. +### AJAX requests + +Requests sent with `X-Requested-With: XMLHttpRequest` are attached to the debug bar +already initialized by the main request: the middleware appends only the collected data +of the AJAX request (rendered as a `(ajax)` dataset) instead of the initialization code +and assets. Without it every AJAX response would create another debug bar on top of the +existing one. + ### PSR-17 This package isn't require any PSR-7 implementation - you need to provide it by own. Middleware require ResponseFactory and StreamFactory interfaces. [List of existing interfaces](https://packagist.org/providers/psr/http-factory-implementation). diff --git a/composer.json b/composer.json index 77bcca9..974638e 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ ], "require": { "php": "^7.3 || ^8.0", - "maximebf/debugbar": "^1.4", + "maximebf/debugbar": "^1.18", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", "psr/container-implementation": "^1.0 || ^2.0", @@ -23,6 +23,7 @@ "require-dev": { "phpunit/phpunit": "^9.1.4", "mikey179/vfsstream": "^1.6.8", + "symfony/var-dumper": "^4.4.30", "slim/slim": "^3.0", "mezzio/mezzio": "^3.0", "mezzio/mezzio-fastroute": "^3.0.1", diff --git a/src/PhpDebugBarMiddleware.php b/src/PhpDebugBarMiddleware.php index 8d4a892..82d1a0f 100644 --- a/src/PhpDebugBarMiddleware.php +++ b/src/PhpDebugBarMiddleware.php @@ -62,7 +62,7 @@ public function process(ServerRequest $request, RequestHandler $handler): Respon } if ($this->isHtmlResponse($response)) { - return $this->attachDebugBarToHtmlResponse($response); + return $this->attachDebugBarToHtmlResponse($response, !$this->isXmlHttpRequest($request)); } return $this->prepareHtmlResponseWithDebugBar($response); @@ -122,10 +122,16 @@ private function prepareHtmlResponseWithDebugBar(Response $response): Response ->withAddedHeader('Content-type', 'text/html'); } - private function attachDebugBarToHtmlResponse(Response $response): Response + /** + * @param bool $initialize Render the debug bar initialization code and assets. + * Must be false for responses attached to an already + * initialized debug bar (XMLHttpRequest), otherwise a + * second debug bar is created on top of the existing one. + */ + private function attachDebugBarToHtmlResponse(Response $response, bool $initialize = true): Response { - $head = $this->debugBarRenderer->renderHead(); - $body = $this->debugBarRenderer->render(); + $head = $initialize ? $this->debugBarRenderer->renderHead() : ''; + $body = $this->debugBarRenderer->render($initialize); $responseBody = $response->getBody(); if (! $responseBody->eof() && $responseBody->isSeekable()) { @@ -205,6 +211,11 @@ private function isHtml(MessageInterface $message, string $headerName): bool return strpos($message->getHeaderLine($headerName), 'text/html') !== false; } + private function isXmlHttpRequest(ServerRequest $request): bool + { + return strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest'; + } + private function isRedirect(Response $response): bool { $statusCode = $response->getStatusCode(); diff --git a/test/AbstractMiddlewareRunnerTest.php b/test/AbstractMiddlewareRunnerTest.php index a7dd572..7723332 100644 --- a/test/AbstractMiddlewareRunnerTest.php +++ b/test/AbstractMiddlewareRunnerTest.php @@ -32,6 +32,27 @@ final public function testAppendJsIntoHtmlContent(): void $this->assertStringContainsString('"/phpdebugbar/debugbar.js"', $responseBody); } + final public function testNotAppendInitializationCodeIntoXmlHttpRequestContent(): void + { + $response = $this->dispatchApplication([ + 'REQUEST_URI' => '/hello', + 'REQUEST_METHOD' => 'GET', + 'HTTP_ACCEPT' => 'text/html', + 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', + ], [ + '/hello' => function (ServerRequestInterface $request) { + return new Response\HtmlResponse('Hello!'); + }, + ]); + + $responseBody = (string) $response->getBody(); + + $this->assertStringContainsString('Hello!', $responseBody); + $this->assertStringContainsString('phpdebugbar.addDataSet(', $responseBody); + $this->assertStringNotContainsString('var phpdebugbar = new PhpDebugBar.DebugBar();', $responseBody); + $this->assertStringNotContainsString('"/phpdebugbar/debugbar.js"', $responseBody); + } + final public function testGetStatics(): void { $response = $this->dispatchApplication([ diff --git a/test/MezzioTest.php b/test/MezzioTest.php index f3e340d..92abd8f 100644 --- a/test/MezzioTest.php +++ b/test/MezzioTest.php @@ -22,6 +22,7 @@ use Mezzio\Container\ServerRequestErrorResponseGeneratorFactory; use Mezzio\MiddlewareContainer; use Mezzio\MiddlewareFactory; +use Mezzio\MiddlewareFactoryInterface; use Mezzio\Response\ServerRequestErrorResponseGenerator; use Mezzio\Router\FastRouteRouter; use Mezzio\Router\FastRouteRouterFactory; @@ -34,6 +35,7 @@ use Mezzio\Router\RouterInterface; use Laminas\HttpHandlerRunner\Emitter\EmitterInterface; use Laminas\HttpHandlerRunner\RequestHandlerRunner; +use Laminas\HttpHandlerRunner\RequestHandlerRunnerInterface; use Laminas\ServiceManager\Factory\InvokableFactory; use Laminas\ServiceManager\ServiceManager; use Laminas\Stratigility\MiddlewarePipe; @@ -104,6 +106,8 @@ private function createContainer(array $server): ContainerInterface $serviceManagerConfig['factories'][DispatchMiddleware::class] = DispatchMiddlewareFactory::class; $serviceManagerConfig['factories'][ResponseFactory::class] = InvokableFactory::class; $serviceManagerConfig['factories'][StreamFactory::class] = InvokableFactory::class; + $serviceManagerConfig['aliases'][MiddlewareFactoryInterface::class] = MiddlewareFactory::class; + $serviceManagerConfig['aliases'][RequestHandlerRunnerInterface::class] = RequestHandlerRunner::class; $serviceManagerConfig['aliases'][RouterInterface::class] = FastRouteRouter::class; $serviceManagerConfig['aliases'][\Mezzio\ApplicationPipeline::class] = MiddlewarePipe::class; $serviceManagerConfig['aliases'][ResponseFactoryInterface::class] = ResponseFactory::class; diff --git a/test/PhpDebugBarMiddlewareTest.php b/test/PhpDebugBarMiddlewareTest.php index 423b195..b9bc14b 100644 --- a/test/PhpDebugBarMiddlewareTest.php +++ b/test/PhpDebugBarMiddlewareTest.php @@ -29,7 +29,9 @@ protected function setUp(): void $this->debugbarRenderer = $this->getMockBuilder(JavascriptRenderer::class)->disableOriginalConstructor()->getMock(); $this->debugbarRenderer->method('renderHead')->willReturn('RenderHead'); $this->debugbarRenderer->method('getBaseUrl')->willReturn('/phpdebugbar'); - $this->debugbarRenderer->method('render')->willReturn('RenderBody'); + $this->debugbarRenderer->method('render')->willReturnCallback(function (bool $initialize = true): string { + return $initialize ? 'RenderBody' : 'RenderBodyWithoutInit'; + }); $responseFactory = new ResponseFactory(); $streamFactory = new StreamFactory(); @@ -244,6 +246,45 @@ public function testForceNotAttachDebugbarIfAttributePresents(): void $this->assertSame('ResponseBody', (string) $result->getBody()); } + public function testNotRenderInitializationCodeForXmlHttpRequest(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Requested-With' => 'XMLHttpRequest']); + $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('ResponseBodyRenderBodyWithoutInit', (string) $result->getBody()); + } + + public function testNotRenderInitializationCodeForLowercasedXmlHttpRequestHeaderValue(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Requested-With' => 'xmlhttprequest']); + $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->assertSame('ResponseBodyRenderBodyWithoutInit', (string) $result->getBody()); + } + + public function testNotAttachDebugbarToXmlHttpRequestIfForceDisabled(): void + { + $request = new ServerRequest([], [], null, null, 'php://input', ['Accept' => 'text/html', 'X-Requested-With' => 'XMLHttpRequest', '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->assertSame($response, $result); + $this->assertSame('ResponseBody', (string) $result->getBody()); + } + public function testAppendsToEndOfHtmlResponse(): void { $html = '