mirror of
https://github.com/symfony/web-profiler-bundle.git
synced 2026-03-24 00:02:13 +01:00
[WebProfilerBundle] add debugbar on StreamedResponse
This commit is contained in:
committed by
Nicolas Grekas
parent
4182b66a45
commit
0d882aaa7d
@@ -6,6 +6,7 @@ CHANGELOG
|
||||
|
||||
* Add error indicator to profiler list view for profiles with errors
|
||||
* Add cURL copy paste button in the Request/Response tab
|
||||
* Add support for streamed responses in the debug toolbar
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
@@ -19,6 +19,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ServerEvent;
|
||||
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;
|
||||
@@ -112,7 +113,15 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
||||
$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()]));
|
||||
$content = $this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()]);
|
||||
|
||||
if ($response instanceof StreamedResponse) {
|
||||
$response->setCallback(static function () use ($content): void {
|
||||
echo $content;
|
||||
});
|
||||
} else {
|
||||
$response->setContent($content);
|
||||
}
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->remove('Location');
|
||||
}
|
||||
@@ -156,24 +165,46 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
||||
*/
|
||||
protected function injectToolbar(Response $response, Request $request, array $nonces): void
|
||||
{
|
||||
$content = $response->getContent();
|
||||
$pos = strripos($content, '</body>');
|
||||
$responseRef = \WeakReference::create($response);
|
||||
$injectToolbar = function (string $buffer) use ($request, $responseRef, $nonces): string {
|
||||
if (false !== $pos = strripos($buffer, '</body>')) {
|
||||
$toolbar = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $responseRef->get()->headers->get('X-Debug-Token'), $nonces))."\n";
|
||||
$buffer = substr($buffer, 0, $pos).$toolbar.substr($buffer, $pos);
|
||||
}
|
||||
|
||||
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);
|
||||
return $buffer;
|
||||
};
|
||||
|
||||
if (!$response instanceof StreamedResponse) {
|
||||
$response->setContent($injectToolbar($response->getContent()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$callback = $response->getCallback();
|
||||
$response->setCallback(static function () use ($callback, $injectToolbar): void {
|
||||
ob_start($injectToolbar, 8); // length of '</body>'
|
||||
try {
|
||||
$callback(...\func_get_args());
|
||||
} finally {
|
||||
ob_end_flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -30,7 +30,7 @@ use Twig\Environment;
|
||||
class WebDebugToolbarListenerTest extends TestCase
|
||||
{
|
||||
#[DataProvider('getInjectToolbarTests')]
|
||||
public function testInjectToolbar($content, $expected)
|
||||
public function testInjectToolbar(string $content, string $expected)
|
||||
{
|
||||
$listener = new WebDebugToolbarListener($this->getTwigMock());
|
||||
$m = new \ReflectionMethod($listener, 'injectToolbar');
|
||||
@@ -60,7 +60,7 @@ class WebDebugToolbarListenerTest extends TestCase
|
||||
}
|
||||
|
||||
#[DataProvider('provideRedirects')]
|
||||
public function testHtmlRedirectionIsIntercepted($statusCode)
|
||||
public function testHtmlRedirectionIsIntercepted(int $statusCode)
|
||||
{
|
||||
$response = new Response('Some content', $statusCode);
|
||||
$response->headers->set('Location', 'https://example.com/');
|
||||
@@ -76,7 +76,7 @@ class WebDebugToolbarListenerTest extends TestCase
|
||||
|
||||
public function testNonHtmlRedirectionIsNotIntercepted()
|
||||
{
|
||||
$response = new Response('Some content', '301');
|
||||
$response = new Response('Some content', 301);
|
||||
$response->headers->set('Location', 'https://example.com/');
|
||||
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
|
||||
$event = new ResponseEvent($this->createStub(KernelInterface::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
@@ -131,7 +131,7 @@ class WebDebugToolbarListenerTest extends TestCase
|
||||
|
||||
#[DataProvider('provideRedirects')]
|
||||
#[Depends('testToolbarIsInjected')]
|
||||
public function testToolbarIsNotInjectedOnRedirection($statusCode)
|
||||
public function testToolbarIsNotInjectedOnRedirection(int $statusCode)
|
||||
{
|
||||
$response = new Response('<html><head></head><body></body></html>', $statusCode);
|
||||
$response->headers->set('Location', 'https://example.com/');
|
||||
@@ -485,7 +485,7 @@ class WebDebugToolbarListenerTest extends TestCase
|
||||
$response->send(false);
|
||||
}
|
||||
|
||||
protected function getTwigMock($render = 'WDT')
|
||||
protected function getTwigMock(string $render = 'WDT')
|
||||
{
|
||||
$templating = $this->createStub(Environment::class);
|
||||
$templating
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\Kernel;
|
||||
use Twig\Environment;
|
||||
|
||||
class WebDebugToolbarStreamedResponseListenerTest extends TestCase
|
||||
{
|
||||
#[DataProvider('provideInjectedToolbarHtml')]
|
||||
public function testInjectToolbar(string $content, string $expected)
|
||||
{
|
||||
$listener = new WebDebugToolbarListener($this->getTwigStub());
|
||||
$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->assertSame($expected, $this->getContentFromStreamedResponse($response));
|
||||
}
|
||||
|
||||
public static function provideInjectedToolbarHtml(): array
|
||||
{
|
||||
return [
|
||||
['<html><head></head><body></body></html>', "<html><head></head><body>\nWDT\n</body></html>"],
|
||||
['<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<textarea><html><head></head><body></body></html></textarea>
|
||||
</body>
|
||||
</html>', "<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<textarea><html><head></head><body></body></html></textarea>
|
||||
\nWDT\n</body>
|
||||
</html>"],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideRedirects')]
|
||||
public function testHtmlRedirectionIsIntercepted(int $statusCode)
|
||||
{
|
||||
$response = new StreamedResponse($this->createCallbackFromContent('Some content'), $statusCode);
|
||||
$response->headers->set('Location', 'https://example.com/');
|
||||
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
|
||||
$event = new ResponseEvent($this->createStub(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$listener = new WebDebugToolbarListener($this->getTwigStub('Redirection'), true);
|
||||
$listener->onKernelResponse($event);
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('Redirection', $this->getContentFromStreamedResponse($response));
|
||||
}
|
||||
|
||||
public static function provideRedirects(): array
|
||||
{
|
||||
return [
|
||||
[301],
|
||||
[302],
|
||||
];
|
||||
}
|
||||
|
||||
public function testToolbarIsInjected()
|
||||
{
|
||||
$response = new StreamedResponse($this->createCallbackFromContent('<html><head></head><body></body></html>'));
|
||||
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
|
||||
|
||||
$event = new ResponseEvent($this->createStub(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$listener = new WebDebugToolbarListener($this->getTwigStub());
|
||||
$listener->onKernelResponse($event);
|
||||
|
||||
$this->assertSame("<html><head></head><body>\nWDT\n</body></html>", $this->getContentFromStreamedResponse($response));
|
||||
}
|
||||
|
||||
public function testToolbarIsNotInjectedOnIncompleteHtmlResponses()
|
||||
{
|
||||
$response = new StreamedResponse($this->createCallbackFromContent('<div>Some content</div>'));
|
||||
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
|
||||
|
||||
$event = new ResponseEvent($this->createStub(Kernel::class), new Request(), HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$listener = new WebDebugToolbarListener($this->getTwigStub());
|
||||
$listener->onKernelResponse($event);
|
||||
|
||||
$this->assertSame('<div>Some content</div>', $this->getContentFromStreamedResponse($response));
|
||||
}
|
||||
|
||||
private function getTwigStub(string $render = 'WDT'): Environment
|
||||
{
|
||||
$templating = $this->createStub(Environment::class);
|
||||
$templating->method('render')
|
||||
->willReturn($render);
|
||||
|
||||
return $templating;
|
||||
}
|
||||
|
||||
private function createCallbackFromContent(string $content): callable
|
||||
{
|
||||
return static function () use ($content): void {
|
||||
echo $content;
|
||||
};
|
||||
}
|
||||
|
||||
private function getContentFromStreamedResponse(StreamedResponse $response): string
|
||||
{
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user