Skip to content

Commit 9bfa4ae

Browse files
committed
[WebProfilerBundle] show debugbar on StreamedResponse
1 parent a86878f commit 9bfa4ae

File tree

4 files changed

+191
-61
lines changed

4 files changed

+191
-61
lines changed

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for displaying profiles of multiple serializer instances
8+
* Show debug bar when using a streamed response
89

910
7.1
1011
---

src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\HttpFoundation\Request;
1818
use Symfony\Component\HttpFoundation\Response;
1919
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
20+
use Symfony\Component\HttpFoundation\StreamedResponse;
2021
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
2122
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2223
use Symfony\Component\HttpKernel\KernelEvents;
@@ -104,7 +105,6 @@ public function onKernelResponse(ResponseEvent $event): void
104105
// keep current flashes for one more request if using AutoExpireFlashBag
105106
$session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
106107
}
107-
108108
$response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location'), 'host' => $request->getSchemeAndHttpHost()]));
109109
$response->setStatusCode(200);
110110
$response->headers->remove('Location');
@@ -128,26 +128,59 @@ public function onKernelResponse(ResponseEvent $event): void
128128
*/
129129
protected function injectToolbar(Response $response, Request $request, array $nonces): void
130130
{
131-
$content = $response->getContent();
132-
$pos = strripos($content, '</body>');
133-
134-
if (false !== $pos) {
135-
$toolbar = "\n".str_replace("\n", '', $this->twig->render(
136-
'@WebProfiler/Profiler/toolbar_js.html.twig',
137-
[
138-
'full_stack' => class_exists(FullStack::class),
139-
'excluded_ajax_paths' => $this->excludedAjaxPaths,
140-
'token' => $response->headers->get('X-Debug-Token'),
141-
'request' => $request,
142-
'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null,
143-
'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null,
144-
]
145-
))."\n";
146-
$content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
147-
$response->setContent($content);
131+
if ($response instanceof StreamedResponse) {
132+
$callback = $response->getCallback();
133+
if (false !== strripos($response->headers->get('Content-Type'), 'text/html')) {
134+
$toolbarHTMLContent = $this->getToolbarHTML($request, $response->headers->get('X-Debug-Token'), $nonces);
135+
$injectedCallback = static function () use ($toolbarHTMLContent, $callback): void {
136+
ob_start(function (string $buffer, int $phase) use ($toolbarHTMLContent): string {
137+
$pos = strripos($buffer, '</body>');
138+
if (false !== $pos) {
139+
$buffer = substr($buffer, 0, $pos).$toolbarHTMLContent.substr($buffer, $pos);
140+
}
141+
142+
return $buffer;
143+
}, 8); // length of '</body>'
144+
145+
($callback)();
146+
ob_end_flush();
147+
};
148+
$response->setCallback($injectedCallback);
149+
}
150+
} else {
151+
$content = $response->getContent();
152+
$pos = strripos($content, '</body>');
153+
154+
if (false !== $pos) {
155+
$response->setContent(
156+
$this->renderToolbarInContent($content, $pos, $response->headers->get('X-Debug-Token'), $request, $nonces)
157+
);
158+
}
148159
}
149160
}
150161

162+
protected function renderToolbarInContent(string $content, int $pos, ?string $debugToken, Request $request, array $nonces): string
163+
{
164+
$toolbar = "\n".str_replace("\n", '', $this->getToolbarHTML($request, $debugToken, $nonces))."\n";
165+
166+
return substr($content, 0, $pos).$toolbar.substr($content, $pos);
167+
}
168+
169+
private function getToolbarHTML(Request $request, ?string $debugToken, array $nonces): string
170+
{
171+
return $this->twig->render(
172+
'@WebProfiler/Profiler/toolbar_js.html.twig',
173+
[
174+
'full_stack' => class_exists(FullStack::class),
175+
'excluded_ajax_paths' => $this->excludedAjaxPaths,
176+
'token' => $debugToken,
177+
'request' => $request,
178+
'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null,
179+
'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null,
180+
]
181+
);
182+
}
183+
151184
public static function getSubscribedEvents(): array
152185
{
153186
return [
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener;
13+
14+
use Symfony\Component\HttpFoundation\StreamedResponse;
15+
16+
class MockedStreamedResponse extends StreamedResponse
17+
{
18+
public function getContent(): string|false
19+
{
20+
ob_start();
21+
($this->callback)();
22+
$content = ob_get_contents();
23+
ob_end_clean();
24+
25+
return $content;
26+
}
27+
28+
public static function createFromContent(string $content = ''): self
29+
{
30+
$response = new self();
31+
$response->setCallback(function () use ($content) {
32+
echo $content;
33+
});
34+
$response->headers->set('Content-Type', 'text/html');
35+
36+
return $response;
37+
}
38+
}

0 commit comments

Comments
 (0)