From c1e7be0ad4e24b0275d3a463b98f8af16c4b8a9e Mon Sep 17 00:00:00 2001 From: Damien Fernandes Date: Fri, 11 Oct 2024 17:37:30 +0200 Subject: [PATCH 1/4] [WebProfilerBundle] show debugbar on StreamedResponse --- .../Bundle/WebProfilerBundle/CHANGELOG.md | 1 + .../EventListener/WebDebugToolbarListener.php | 69 ++++++--- .../EventListener/MockedStreamedResponse.php | 38 +++++ .../WebDebugToolbarListenerTest.php | 145 ++++++++++++------ 4 files changed, 191 insertions(+), 62 deletions(-) create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 539d814d2a438..109f947e3e28d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG --- * Add support for displaying profiles of multiple serializer instances + * Show debug bar when using a streamed response 7.1 --- diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index de7bb7b001ca0..510e4c7a51c3c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -109,7 +110,6 @@ public function onKernelResponse(ResponseEvent $event): void // keep current flashes for one more request if using AutoExpireFlashBag $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); } - $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()])); $response->setStatusCode(200); $response->headers->remove('Location'); @@ -133,26 +133,59 @@ public function onKernelResponse(ResponseEvent $event): void */ protected function injectToolbar(Response $response, Request $request, array $nonces): void { - $content = $response->getContent(); - $pos = strripos($content, ''); - - if (false !== $pos) { - $toolbar = "\n".str_replace("\n", '', $this->twig->render( - '@WebProfiler/Profiler/toolbar_js.html.twig', - [ - 'full_stack' => class_exists(FullStack::class), - 'excluded_ajax_paths' => $this->excludedAjaxPaths, - 'token' => $response->headers->get('X-Debug-Token'), - 'request' => $request, - 'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null, - 'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null, - ] - ))."\n"; - $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); - $response->setContent($content); + if ($response instanceof StreamedResponse) { + $callback = $response->getCallback(); + if (false !== strripos($response->headers->get('Content-Type'), 'text/html')) { + $toolbarHTMLContent = $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces); + $injectedCallback = static function () use ($toolbarHTMLContent, $callback): void { + ob_start(function (string $buffer, int $phase) use ($toolbarHTMLContent): string { + $pos = strripos($buffer, ''); + if (false !== $pos) { + $buffer = substr($buffer, 0, $pos).$toolbarHTMLContent.substr($buffer, $pos); + } + + return $buffer; + }, 8); // length of '' + + ($callback)(); + ob_end_flush(); + }; + $response->setCallback($injectedCallback); + } + } else { + $content = $response->getContent(); + $pos = strripos($content, ''); + + if (false !== $pos) { + $response->setContent( + $this->renderToolbarInContent($content, $pos, $response->headers->get('X-Debug-Token'), $request, $nonces) + ); + } } } + protected function renderToolbarInContent(string $content, int $pos, ?string $debugToken, Request $request, array $nonces): string + { + $toolbar = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $debugToken, $nonces))."\n"; + + return substr($content, 0, $pos).$toolbar.substr($content, $pos); + } + + private function getToolbarHTML(Request $request, ?string $debugToken, array $nonces): string + { + return $this->twig->render( + '@WebProfiler/Profiler/toolbar_js.html.twig', + [ + 'full_stack' => class_exists(FullStack::class), + 'excluded_ajax_paths' => $this->excludedAjaxPaths, + 'token' => $debugToken, + 'request' => $request, + 'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null, + 'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null, + ] + ); + } + public static function getSubscribedEvents(): array { return [ diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php new file mode 100644 index 0000000000000..430a327c4ff6e --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener; + +use Symfony\Component\HttpFoundation\StreamedResponse; + +class MockedStreamedResponse extends StreamedResponse +{ + public function getContent(): string|false + { + ob_start(); + ($this->callback)(); + $content = ob_get_contents(); + ob_end_clean(); + + return $content; + } + + public static function createFromContent(string $content = ''): self + { + $response = new self(); + $response->setCallback(function () use ($content) { + echo $content; + }); + $response->headers->set('Content-Type', 'text/html'); + + return $response; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index ff9bd096fb13f..83be0f91856b7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -28,13 +28,11 @@ class WebDebugToolbarListenerTest extends TestCase /** * @dataProvider getInjectToolbarTests */ - public function testInjectToolbar($content, $expected) + public function testInjectToolbar(Response $response, string $expected) { $listener = new WebDebugToolbarListener($this->getTwigMock()); $m = new \ReflectionMethod($listener, 'injectToolbar'); - $response = new Response($content); - $m->invoke($listener, $response, Request::create('/'), ['csp_script_nonce' => 'scripto', 'csp_style_nonce' => 'stylo']); $this->assertEquals($expected, $response->getContent()); } @@ -42,25 +40,44 @@ public function testInjectToolbar($content, $expected) public static function getInjectToolbarTests() { return [ - ['', "\nWDT\n"], - [' + [new Response(''), "\nWDT\n"], + [new Response(' - ', " + '), " \nWDT\n "], + [ + MockedStreamedResponse::createFromContent(''), + 'WDT', + ], + [ + MockedStreamedResponse::createFromContent( + ' + + + + + '), + ' + + + + WDT + ', + ], ]; } /** * @dataProvider provideRedirects */ - public function testHtmlRedirectionIsIntercepted($statusCode) + public function testHtmlRedirectionIsIntercepted(int $statusCode) { $response = new Response('Some content', $statusCode); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); @@ -86,9 +103,11 @@ public function testNonHtmlRedirectionIsNotIntercepted() $this->assertEquals('Some content', $response->getContent()); } - public function testToolbarIsInjected() + /** + * @dataProvider provideInjectedResponse + */ + public function testToolbarIsInjected(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -96,15 +115,40 @@ public function testToolbarIsInjected() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals("\nWDT\n", $response->getContent()); + $this->assertEquals($expected, $response->getContent()); + } + + public static function provideInjectedResponse(): array + { + return [ + [new Response(''), "\nWDT\n"], + [MockedStreamedResponse::createFromContent(''), 'WDT'], + ]; + } + + public static function provideNotInjectedResponse(): array + { + return [ + [new Response(''), ''], + [MockedStreamedResponse::createFromContent(''), ''], + ]; + } + + public static function provideEmptyResponse(): array + { + return [ + [new Response()], + [MockedStreamedResponse::createFromContent()], + ]; } /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnNonHtmlContentType() + public function testToolbarIsNotInjectedOnNonHtmlContentType(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Type', 'text/xml'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -112,15 +156,16 @@ public function testToolbarIsNotInjectedOnNonHtmlContentType() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnContentDispositionAttachment() + public function testToolbarIsNotInjectedOnContentDispositionAttachment(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Disposition', 'attachment; filename=test.html'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -128,7 +173,7 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } /** @@ -136,7 +181,7 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment() * * @dataProvider provideRedirects */ - public function testToolbarIsNotInjectedOnRedirection($statusCode) + public function testToolbarIsNotInjectedOnRedirection(int $statusCode) { $response = new Response('', $statusCode); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); @@ -158,25 +203,26 @@ public static function provideRedirects(): array /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader() + public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader(Response $response, string $expected) { - $response = new Response(''); - $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedWhenOnSubRequest() + public function testToolbarIsNotInjectedWhenOnSubRequest(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::SUB_REQUEST, $response); @@ -184,7 +230,7 @@ public function testToolbarIsNotInjectedWhenOnSubRequest() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } /** @@ -205,10 +251,11 @@ public function testToolbarIsNotInjectedOnIncompleteHtmlResponses() /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnXmlHttpRequests() + public function testToolbarIsNotInjectedOnXmlHttpRequests(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $request = new Request(); @@ -219,15 +266,16 @@ public function testToolbarIsNotInjectedOnXmlHttpRequests() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } /** * @depends testToolbarIsInjected + * + * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnNonHtmlRequests() + public function testToolbarIsNotInjectedOnNonHtmlRequests(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); @@ -235,12 +283,14 @@ public function testToolbarIsNotInjectedOnNonHtmlRequests() $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals('', $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } - public function testXDebugUrlHeader() + /** + * @dataProvider provideEmptyResponse + */ + public function testXDebugUrlHeader(Response $response) { - $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -259,9 +309,11 @@ public function testXDebugUrlHeader() $this->assertEquals('http://mydomain.com/_profiler/xxxxxxxx', $response->headers->get('X-Debug-Token-Link')); } - public function testThrowingUrlGenerator() + /** + * @dataProvider provideEmptyResponse + */ + public function testThrowingUrlGenerator(Response $response) { - $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -280,9 +332,11 @@ public function testThrowingUrlGenerator() $this->assertEquals('Exception: foo', $response->headers->get('X-Debug-Error')); } - public function testThrowingErrorCleanup() + /** + * @dataProvider provideEmptyResponse + */ + public function testThrowingErrorCleanup(Response $response) { - $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -301,9 +355,11 @@ public function testThrowingErrorCleanup() $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); } - public function testCspIsDisabledIfDumperWasUsed() + /** + * @dataProvider provideInjectedResponse + */ + public function testCspIsDisabledIfDumperWasUsed(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -319,12 +375,14 @@ public function testCspIsDisabledIfDumperWasUsed() $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); $listener->onKernelResponse($event); - $this->assertEquals("\nWDT\n", $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } - public function testCspIsKeptEnabledIfDumperWasNotUsed() + /** + * @dataProvider provideInjectedResponse + */ + public function testCspIsKeptEnabledIfDumperWasNotUsed(Response $response, string $expected) { - $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -340,7 +398,7 @@ public function testCspIsKeptEnabledIfDumperWasNotUsed() $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); $listener->onKernelResponse($event); - $this->assertEquals("\nWDT\n", $response->getContent()); + $this->assertEquals($expected, $response->getContent()); } public function testNullContentTypeWithNoDebugEnv() @@ -356,7 +414,6 @@ public function testNullContentTypeWithNoDebugEnv() $this->expectNotToPerformAssertions(); } - public function testAjaxReplaceHeaderOnDisabledToolbar() { $response = new Response(); @@ -417,7 +474,7 @@ public function testAjaxReplaceHeaderOnEnabledAndXHRButPreviouslySet() $this->assertSame('0', $response->headers->get('Symfony-Debug-Toolbar-Replace')); } - protected function getTwigMock($render = 'WDT') + protected function getTwigMock(string $render = 'WDT') { $templating = $this->createMock(Environment::class); $templating->expects($this->any()) From 70b2ba605686151b5017655014bc594fd4afc49a Mon Sep 17 00:00:00 2001 From: Damien Fernandes Date: Thu, 7 Nov 2024 17:55:06 +0100 Subject: [PATCH 2/4] review correction from stof --- .../EventListener/WebDebugToolbarListener.php | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 510e4c7a51c3c..e15a0b47edb55 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -110,6 +110,7 @@ public function onKernelResponse(ResponseEvent $event): void // keep current flashes for one more request if using AutoExpireFlashBag $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); } + $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()])); $response->setStatusCode(200); $response->headers->remove('Location'); @@ -135,42 +136,34 @@ protected function injectToolbar(Response $response, Request $request, array $no { if ($response instanceof StreamedResponse) { $callback = $response->getCallback(); - if (false !== strripos($response->headers->get('Content-Type'), 'text/html')) { - $toolbarHTMLContent = $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces); - $injectedCallback = static function () use ($toolbarHTMLContent, $callback): void { - ob_start(function (string $buffer, int $phase) use ($toolbarHTMLContent): string { - $pos = strripos($buffer, ''); - if (false !== $pos) { - $buffer = substr($buffer, 0, $pos).$toolbarHTMLContent.substr($buffer, $pos); - } - - return $buffer; - }, 8); // length of '' - - ($callback)(); - ob_end_flush(); - }; - $response->setCallback($injectedCallback); - } + $toolbarHTMLContent = $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces); + $injectedCallback = static function () use ($toolbarHTMLContent, $callback): void { + ob_start(function (string $buffer, int $phase) use ($toolbarHTMLContent): string { + $pos = strripos($buffer, ''); + if (false !== $pos) { + $buffer = substr($buffer, 0, $pos).$toolbarHTMLContent.substr($buffer, $pos); + } + + return $buffer; + }, 8); // length of '' + + ($callback)(); + ob_end_flush(); + }; + $response->setCallback($injectedCallback); } else { $content = $response->getContent(); $pos = strripos($content, ''); if (false !== $pos) { - $response->setContent( - $this->renderToolbarInContent($content, $pos, $response->headers->get('X-Debug-Token'), $request, $nonces) - ); + $toolbar = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces))."\n"; + + $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); + $response->setContent($content); } } } - protected function renderToolbarInContent(string $content, int $pos, ?string $debugToken, Request $request, array $nonces): string - { - $toolbar = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $debugToken, $nonces))."\n"; - - return substr($content, 0, $pos).$toolbar.substr($content, $pos); - } - private function getToolbarHTML(Request $request, ?string $debugToken, array $nonces): string { return $this->twig->render( From 6f0f7645516aa9da3580a5e75e9df5a7aef0ca52 Mon Sep 17 00:00:00 2001 From: Damien Fernandes Date: Fri, 8 Nov 2024 15:03:38 +0100 Subject: [PATCH 3/4] write a separate test for streamed response and remove MockedStreamedResponse class --- .../EventListener/WebDebugToolbarListener.php | 11 +- .../EventListener/MockedStreamedResponse.php | 38 -- .../WebDebugToolbarListenerTest.php | 141 ++----- ...bugToolbarStreamedResponseListenerTest.php | 386 ++++++++++++++++++ 4 files changed, 437 insertions(+), 139 deletions(-) delete mode 100644 src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarStreamedResponseListenerTest.php diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index e15a0b47edb55..d1eeeee1aa616 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -111,7 +111,14 @@ public function onKernelResponse(ResponseEvent $event): void $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); } - $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()])); + if ($response instanceof StreamedResponse) { + $twig = $this->twig; + $response->setCallback(function () use ($twig, $request, $response): void { + echo $twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()]); + }); + } else { + $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()])); + } $response->setStatusCode(200); $response->headers->remove('Location'); } @@ -136,7 +143,7 @@ protected function injectToolbar(Response $response, Request $request, array $no { if ($response instanceof StreamedResponse) { $callback = $response->getCallback(); - $toolbarHTMLContent = $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces); + $toolbarHTMLContent = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces))."\n"; $injectedCallback = static function () use ($toolbarHTMLContent, $callback): void { ob_start(function (string $buffer, int $phase) use ($toolbarHTMLContent): string { $pos = strripos($buffer, ''); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php deleted file mode 100644 index 430a327c4ff6e..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/MockedStreamedResponse.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener; - -use Symfony\Component\HttpFoundation\StreamedResponse; - -class MockedStreamedResponse extends StreamedResponse -{ - public function getContent(): string|false - { - ob_start(); - ($this->callback)(); - $content = ob_get_contents(); - ob_end_clean(); - - return $content; - } - - public static function createFromContent(string $content = ''): self - { - $response = new self(); - $response->setCallback(function () use ($content) { - echo $content; - }); - $response->headers->set('Content-Type', 'text/html'); - - return $response; - } -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index 83be0f91856b7..bd4ca434596fd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -28,11 +28,13 @@ class WebDebugToolbarListenerTest extends TestCase /** * @dataProvider getInjectToolbarTests */ - public function testInjectToolbar(Response $response, string $expected) + public function testInjectToolbar(string $content, string $expected) { $listener = new WebDebugToolbarListener($this->getTwigMock()); $m = new \ReflectionMethod($listener, 'injectToolbar'); + $response = new Response($content); + $m->invoke($listener, $response, Request::create('/'), ['csp_script_nonce' => 'scripto', 'csp_style_nonce' => 'stylo']); $this->assertEquals($expected, $response->getContent()); } @@ -40,37 +42,18 @@ public function testInjectToolbar(Response $response, string $expected) public static function getInjectToolbarTests() { return [ - [new Response(''), "\nWDT\n"], - [new Response(' + ['', "\nWDT\n"], + [' - '), " + ', " \nWDT\n "], - [ - MockedStreamedResponse::createFromContent(''), - 'WDT', - ], - [ - MockedStreamedResponse::createFromContent( - ' - - - - - '), - ' - - - - WDT - ', - ], ]; } @@ -92,7 +75,7 @@ public function testHtmlRedirectionIsIntercepted(int $statusCode) public function testNonHtmlRedirectionIsNotIntercepted() { - $response = new Response('Some content', '301'); + $response = new Response('Some content', 301); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); @@ -103,11 +86,9 @@ public function testNonHtmlRedirectionIsNotIntercepted() $this->assertEquals('Some content', $response->getContent()); } - /** - * @dataProvider provideInjectedResponse - */ - public function testToolbarIsInjected(Response $response, string $expected) + public function testToolbarIsInjected() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -115,40 +96,15 @@ public function testToolbarIsInjected(Response $response, string $expected) $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); - } - - public static function provideInjectedResponse(): array - { - return [ - [new Response(''), "\nWDT\n"], - [MockedStreamedResponse::createFromContent(''), 'WDT'], - ]; - } - - public static function provideNotInjectedResponse(): array - { - return [ - [new Response(''), ''], - [MockedStreamedResponse::createFromContent(''), ''], - ]; - } - - public static function provideEmptyResponse(): array - { - return [ - [new Response()], - [MockedStreamedResponse::createFromContent()], - ]; + $this->assertEquals("\nWDT\n", $response->getContent()); } /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnNonHtmlContentType(Response $response, string $expected) + public function testToolbarIsNotInjectedOnNonHtmlContentType() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Type', 'text/xml'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -156,16 +112,15 @@ public function testToolbarIsNotInjectedOnNonHtmlContentType(Response $response, $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnContentDispositionAttachment(Response $response, string $expected) + public function testToolbarIsNotInjectedOnContentDispositionAttachment() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Disposition', 'attachment; filename=test.html'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -173,7 +128,7 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment(Response $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } /** @@ -203,26 +158,25 @@ public static function provideRedirects(): array /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader(Response $response, string $expected) + public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader() { + $response = new Response(''); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedWhenOnSubRequest(Response $response, string $expected) + public function testToolbarIsNotInjectedWhenOnSubRequest() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::SUB_REQUEST, $response); @@ -230,7 +184,7 @@ public function testToolbarIsNotInjectedWhenOnSubRequest(Response $response, str $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } /** @@ -251,11 +205,10 @@ public function testToolbarIsNotInjectedOnIncompleteHtmlResponses() /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnXmlHttpRequests(Response $response, string $expected) + public function testToolbarIsNotInjectedOnXmlHttpRequests() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $request = new Request(); @@ -266,16 +219,15 @@ public function testToolbarIsNotInjectedOnXmlHttpRequests(Response $response, st $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } /** * @depends testToolbarIsInjected - * - * @dataProvider provideNotInjectedResponse */ - public function testToolbarIsNotInjectedOnNonHtmlRequests(Response $response, string $expected) + public function testToolbarIsNotInjectedOnNonHtmlRequests() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); @@ -283,14 +235,12 @@ public function testToolbarIsNotInjectedOnNonHtmlRequests(Response $response, st $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals('', $response->getContent()); } - /** - * @dataProvider provideEmptyResponse - */ - public function testXDebugUrlHeader(Response $response) + public function testXDebugUrlHeader() { + $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -309,11 +259,9 @@ public function testXDebugUrlHeader(Response $response) $this->assertEquals('http://mydomain.com/_profiler/xxxxxxxx', $response->headers->get('X-Debug-Token-Link')); } - /** - * @dataProvider provideEmptyResponse - */ - public function testThrowingUrlGenerator(Response $response) + public function testThrowingUrlGenerator() { + $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -332,11 +280,9 @@ public function testThrowingUrlGenerator(Response $response) $this->assertEquals('Exception: foo', $response->headers->get('X-Debug-Error')); } - /** - * @dataProvider provideEmptyResponse - */ - public function testThrowingErrorCleanup(Response $response) + public function testThrowingErrorCleanup() { + $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $urlGenerator = $this->createMock(UrlGeneratorInterface::class); @@ -355,11 +301,9 @@ public function testThrowingErrorCleanup(Response $response) $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); } - /** - * @dataProvider provideInjectedResponse - */ - public function testCspIsDisabledIfDumperWasUsed(Response $response, string $expected) + public function testCspIsDisabledIfDumperWasUsed() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -375,14 +319,12 @@ public function testCspIsDisabledIfDumperWasUsed(Response $response, string $exp $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals("\nWDT\n", $response->getContent()); } - /** - * @dataProvider provideInjectedResponse - */ - public function testCspIsKeptEnabledIfDumperWasNotUsed(Response $response, string $expected) + public function testCspIsKeptEnabledIfDumperWasNotUsed() { + $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); @@ -398,7 +340,7 @@ public function testCspIsKeptEnabledIfDumperWasNotUsed(Response $response, strin $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); $listener->onKernelResponse($event); - $this->assertEquals($expected, $response->getContent()); + $this->assertEquals("\nWDT\n", $response->getContent()); } public function testNullContentTypeWithNoDebugEnv() @@ -414,6 +356,7 @@ public function testNullContentTypeWithNoDebugEnv() $this->expectNotToPerformAssertions(); } + public function testAjaxReplaceHeaderOnDisabledToolbar() { $response = new Response(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarStreamedResponseListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarStreamedResponseListenerTest.php new file mode 100644 index 0000000000000..86f15c52d99ad --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarStreamedResponseListenerTest.php @@ -0,0 +1,386 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Twig\Environment; + +class WebDebugToolbarStreamedResponseListenerTest extends TestCase +{ + /** + * @dataProvider getInjectToolbarTests + */ + public function testInjectToolbar(string $content, string $expected) + { + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $m = new \ReflectionMethod($listener, 'injectToolbar'); + + $response = new StreamedResponse($this->createCallbackFromContent($content)); + + $m->invoke($listener, $response, Request::create('/'), ['csp_script_nonce' => 'scripto', 'csp_style_nonce' => 'stylo']); + $this->assertEquals($expected, $this->getContentFromStreamedResponse($response)); + } + + public static function getInjectToolbarTests() + { + return [ + ['', "\nWDT\n"], + [' + + + + + ', " + + + + \nWDT\n + "], + ]; + } + + /** + * @dataProvider provideRedirects + */ + public function testHtmlRedirectionIsIntercepted(int $statusCode) + { + $response = new StreamedResponse($this->createCallbackFromContent('Some content'), $statusCode); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock('Redirection'), true); + $listener->onKernelResponse($event); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('Redirection', $this->getContentFromStreamedResponse($response)); + } + + public function testNonHtmlRedirectionIsNotIntercepted() + { + $response = new StreamedResponse($this->createCallbackFromContent('Some content'), 301); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock('Redirection'), true); + $listener->onKernelResponse($event); + + $this->assertEquals(301, $response->getStatusCode()); + $this->assertEquals('Some content', $this->getContentFromStreamedResponse($response)); + } + + public function testToolbarIsInjected() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals("\nWDT\n", $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedOnNonHtmlContentType() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + $response->headers->set('Content-Type', 'text/xml'); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedOnContentDispositionAttachment() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + $response->headers->set('Content-Disposition', 'attachment; filename=test.html'); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + * + * @dataProvider provideRedirects + */ + public function testToolbarIsNotInjectedOnRedirection(int $statusCode) + { + $response = new StreamedResponse($this->createCallbackFromContent(''), $statusCode); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + public static function provideRedirects(): array + { + return [ + [301], + [302], + ]; + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedWhenOnSubRequest() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::SUB_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedOnIncompleteHtmlResponses() + { + $response = new StreamedResponse($this->createCallbackFromContent('
Some content
')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('
Some content
', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedOnXmlHttpRequests() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $request = new Request(); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + + $event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + /** + * @depends testToolbarIsInjected + */ + public function testToolbarIsNotInjectedOnNonHtmlRequests() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock()); + $listener->onKernelResponse($event); + + $this->assertEquals('', $this->getContentFromStreamedResponse($response)); + } + + public function testXDebugUrlHeader() + { + $response = new StreamedResponse(); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('_profiler', ['token' => 'xxxxxxxx'], UrlGeneratorInterface::ABSOLUTE_URL) + ->willReturn('http://mydomain.com/_profiler/xxxxxxxx') + ; + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); + $listener->onKernelResponse($event); + + $this->assertEquals('http://mydomain.com/_profiler/xxxxxxxx', $response->headers->get('X-Debug-Token-Link')); + } + + public function testThrowingUrlGenerator() + { + $response = new StreamedResponse(); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('_profiler', ['token' => 'xxxxxxxx']) + ->willThrowException(new \Exception('foo')) + ; + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); + $listener->onKernelResponse($event); + + $this->assertEquals('Exception: foo', $response->headers->get('X-Debug-Error')); + } + + public function testThrowingErrorCleanup() + { + $response = new StreamedResponse(); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('_profiler', ['token' => 'xxxxxxxx']) + ->willThrowException(new \Exception("This\nmultiline\r\ntabbed text should\tcome out\r on\n \ta single plain\r\nline")) + ; + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); + $listener->onKernelResponse($event); + + $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); + } + + public function testCspIsDisabledIfDumperWasUsed() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $cspHandler = $this->createMock(ContentSecurityPolicyHandler::class); + $cspHandler->expects($this->once()) + ->method('disableCsp'); + $dumpDataCollector = $this->createMock(DumpDataCollector::class); + $dumpDataCollector->expects($this->once()) + ->method('getDumpsCount') + ->willReturn(1); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); + $listener->onKernelResponse($event); + + $this->assertEquals("\nWDT\n", $this->getContentFromStreamedResponse($response)); + } + + public function testCspIsKeptEnabledIfDumperWasNotUsed() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $cspHandler = $this->createMock(ContentSecurityPolicyHandler::class); + $cspHandler->expects($this->never()) + ->method('disableCsp'); + $dumpDataCollector = $this->createMock(DumpDataCollector::class); + $dumpDataCollector->expects($this->once()) + ->method('getDumpsCount') + ->willReturn(0); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector); + $listener->onKernelResponse($event); + + $this->assertEquals("\nWDT\n", $this->getContentFromStreamedResponse($response)); + } + + public function testNullContentTypeWithNoDebugEnv() + { + $response = new StreamedResponse($this->createCallbackFromContent('')); + $response->headers->set('Content-Type', null); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null); + $listener->onKernelResponse($event); + + $this->expectNotToPerformAssertions(); + } + + protected function getTwigMock(string $render = 'WDT') + { + $templating = $this->createMock(Environment::class); + $templating->expects($this->any()) + ->method('render') + ->willReturn($render); + + return $templating; + } + + private function createCallbackFromContent(string $content): callable + { + return function () use ($content) { + echo $content; + }; + } + + private function getContentFromStreamedResponse(StreamedResponse $response): string + { + ob_start(); + $response->sendContent(); + $content = ob_get_contents(); + ob_end_clean(); + + return $content; + } +} From 677f95caa441bafe507cf9a5c9ba3e26e8d38cc7 Mon Sep 17 00:00:00 2001 From: Damien Fernandes Date: Fri, 24 Jan 2025 11:32:41 +0100 Subject: [PATCH 4/4] add changelog in 7.3 section --- src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 109f947e3e28d..50787452ea277 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -5,12 +5,12 @@ CHANGELOG --- * Add `ajax_replace` option for replacing toolbar on AJAX requests + * Show debug bar when using a streamed response 7.2 --- * Add support for displaying profiles of multiple serializer instances - * Show debug bar when using a streamed response 7.1 ---