Skip to content

[WebProfilerBundle] Generate profiler urls with an useful panel #32491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<argument>null</argument>
<argument>%profiler_listener.only_exceptions%</argument>
<argument>%profiler_listener.only_master_requests%</argument>
<argument type="service" id="profile_stack" />
</service>

<service id="profile_stack" class="Symfony\Component\HttpKernel\Profiler\ProfileStack" />
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;

Expand All @@ -44,15 +47,17 @@ class WebDebugToolbarListener implements EventSubscriberInterface
protected $mode;
protected $excludedAjaxPaths;
private $cspHandler;
private $profileStack;

public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null)
public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null, ProfileStack $profileStack = null)
{
$this->twig = $twig;
$this->urlGenerator = $urlGenerator;
$this->interceptRedirects = $interceptRedirects;
$this->mode = $mode;
$this->excludedAjaxPaths = $excludedAjaxPaths;
$this->cspHandler = $cspHandler;
$this->profileStack = $profileStack;
}

public function isEnabled()
Expand All @@ -65,11 +70,38 @@ public function onKernelResponse(FilterResponseEvent $event)
$response = $event->getResponse();
$request = $event->getRequest();

if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) {
$hasProfile = $this->profileStack instanceof ProfileStack ? $this->profileStack->has($request) : $response->headers->has('X-Debug-Token');

if ($hasProfile && null !== $this->urlGenerator) {
$panel = null;

if ($this->profileStack instanceof ProfileStack) {
$profile = $this->profileStack->get($request);

$token = $profile->getToken();

foreach ($profile->getCollectors() as $collector) {
if ($collector instanceof ExceptionDataCollector && $collector->hasException()) {
$panel = $collector->getName();

break;
}

if ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) {
$panel = $collector->getName();
}
}
} else {
$token = $response->headers->get('X-Debug-Token');
}

try {
$response->headers->set(
'X-Debug-Token-Link',
$this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL)
$this->urlGenerator->generate('_profiler', [
'token' => $token,
'panel' => $panel,
], UrlGeneratorInterface::ABSOLUTE_URL)
);
} catch (\Exception $e) {
$response->headers->set('X-Debug-Error', \get_class($e).': '.preg_replace('/\s+/', ' ', $e->getMessage()));
Expand All @@ -87,7 +119,7 @@ public function onKernelResponse(FilterResponseEvent $event)
return;
}

if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) {
if ($hasProfile && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) {
if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) {
// keep current flashes for one more request if using AutoExpireFlashBag
$session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
Expand All @@ -99,7 +131,7 @@ public function onKernelResponse(FilterResponseEvent $event)
}

if (self::DISABLED === $this->mode
|| !$response->headers->has('X-Debug-Token')
|| !$hasProfile
|| $response->isRedirection()
|| ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $request->getRequestFormat()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<argument type="service" id="router" on-invalid="ignore" />
<argument /> <!-- paths that should be excluded from the AJAX requests shown in the toolbar -->
<argument type="service" id="web_profiler.csp.handler" />
<argument type="service" id="profile_stack" />
</service>
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
if (request.profilerUrl) {
profilerCell.textContent = '';
var profilerLink = document.createElement('a');
profilerLink.setAttribute('href', request.statusCode < 400 ? request.profilerUrl : request.profilerUrl + '?panel=exception');
profilerLink.setAttribute('href', request.profilerUrl);
profilerLink.textContent = request.profile;
profilerCell.appendChild(profilerLink);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Profiler\ProfileStack;

class WebProfilerExtensionTest extends TestCase
{
Expand Down Expand Up @@ -73,6 +74,7 @@ protected function setUp(): void
$this->container->setParameter('data_collector.templates', []);
$this->container->set('kernel', $this->kernel);
$this->container->addCompilerPass(new RegisterListenersPass());
$this->container->register('profile_stack', ProfileStack::class);
}

protected function tearDown(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\HttpKernel\Profiler\ProfileStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class WebDebugToolbarListenerTest extends TestCase
Expand Down Expand Up @@ -243,7 +248,7 @@ public function testXDebugUrlHeader()
$urlGenerator
->expects($this->once())
->method('generate')
->with('_profiler', ['token' => 'xxxxxxxx'], UrlGeneratorInterface::ABSOLUTE_URL)
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn('http://mydomain.com/_profiler/xxxxxxxx')
;

Expand All @@ -264,7 +269,7 @@ public function testThrowingUrlGenerator()
$urlGenerator
->expects($this->once())
->method('generate')
->with('_profiler', ['token' => 'xxxxxxxx'])
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null])
->willThrowException(new \Exception('foo'))
;

Expand All @@ -285,7 +290,7 @@ public function testThrowingErrorCleanup()
$urlGenerator
->expects($this->once())
->method('generate')
->with('_profiler', ['token' => 'xxxxxxxx'])
->with('_profiler', ['token' => 'xxxxxxxx', 'panel' => null])
->willThrowException(new \Exception("This\nmultiline\r\ntabbed text should\tcome out\r on\n \ta single plain\r\nline"))
;

Expand All @@ -297,6 +302,97 @@ 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 testToolbarIsInjectedWithProfileStack()
{
$response = new Response('<html><head></head><body></body></html>');

$event = new ResponseEvent($this->getKernelMock(), $request = $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response);

$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', null, $profileStack = new ProfileStack());

$profileStack->set($request, new Profile('foobar'));

$listener->onKernelResponse($event);

$this->assertEquals("<html><head></head><body>\nWDT\n</body></html>", $response->getContent());
}

/**
* @dataProvider linksToPanelsProvider
*/
public function testLinksToPanels(DataCollectorInterface $dataCollector, Request $request, Response $response)
{
$urlGenerator = $this->getUrlGeneratorMock();
$urlGenerator
->expects($this->once())
->method('generate')
->with('_profiler', ['token' => $token = 'xxxxxx', 'panel' => $dataCollector->getName()], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn($expectedLink = 'http://mydomain.com/_profiler/'.$dataCollector->getName());

$event = new ResponseEvent($this->getKernelMock(), $request, HttpKernelInterface::MASTER_REQUEST, $response);

$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator, '', null, $profileStack = new ProfileStack());

$profileStack->set($request, $profile = new Profile($token));
$profile->addCollector($dataCollector);
$profile->addCollector($this->createMock(DataCollectorInterface::class));

$listener->onKernelResponse($event);

$this->assertEquals($expectedLink, $response->headers->get('X-Debug-Token-Link'));
}

public function linksToPanelsProvider()
{
$exceptionDataCollector = new ExceptionDataCollector();
$exceptionDataCollector->collect($request = new Request(), $response = new Response(), new \DomainException());

yield [$exceptionDataCollector, $request, $response];

$dumpDataCollector = $this->createMock(DumpDataCollector::class);
$dumpDataCollector
->expects($this->atLeastOnce())
->method('getName')
->willReturn('dump');
$dumpDataCollector
->expects($this->atLeastOnce())
->method('getDumpsCount')
->willReturn(1);

yield [$dumpDataCollector, new Request(), new Response()];
}

public function testLinkToExceptionPanelPriority()
{
$exceptionDataCollector = new ExceptionDataCollector();
$exceptionDataCollector->collect($request = new Request(), $response = new Response(), new \DomainException());

$urlGenerator = $this->getUrlGeneratorMock();
$urlGenerator
->expects($this->once())
->method('generate')
->with('_profiler', ['token' => $token = 'xxxxxx', 'panel' => $exceptionDataCollector->getName()], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn($expectedLink = 'http://mydomain.com/_profiler/'.$exceptionDataCollector->getName());

$event = new ResponseEvent($this->getKernelMock(), $request, HttpKernelInterface::MASTER_REQUEST, $response);

$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator, '', null, $profileStack = new ProfileStack());

$profileStack->set($request, $profile = new Profile($token));
$profile->addCollector($exceptionDataCollector);

$dumpDataCollector = $this->createMock(DumpDataCollector::class);
$dumpDataCollector
->expects($this->never())
->method('getDumpsCount');

$profile->addCollector($dumpDataCollector);

$listener->onKernelResponse($event);

$this->assertEquals($expectedLink, $response->headers->get('X-Debug-Token-Link'));
}

protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true)
{
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(['getSession', 'isXmlHttpRequest', 'getRequestFormat'])->disableOriginalConstructor()->getMock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
namespace Symfony\Component\HttpKernel\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\HttpKernel\Profiler\ProfileStack;

/**
* ProfilerListener collects data for the current request by listening to the kernel events.
Expand All @@ -37,12 +38,13 @@ class ProfilerListener implements EventSubscriberInterface
protected $profiles;
protected $requestStack;
protected $parents;
private $profileStack;

/**
* @param bool $onlyException True if the profiler only collects data when an exception occurs, false otherwise
* @param bool $onlyMasterRequests True if the profiler only collects data when the request is a master request, false otherwise
*/
public function __construct(Profiler $profiler, RequestStack $requestStack, RequestMatcherInterface $matcher = null, bool $onlyException = false, bool $onlyMasterRequests = false)
public function __construct(Profiler $profiler, RequestStack $requestStack, RequestMatcherInterface $matcher = null, bool $onlyException = false, bool $onlyMasterRequests = false, ProfileStack $profileStack = null)
{
$this->profiler = $profiler;
$this->matcher = $matcher;
Expand All @@ -51,6 +53,7 @@ public function __construct(Profiler $profiler, RequestStack $requestStack, Requ
$this->profiles = new \SplObjectStorage();
$this->parents = new \SplObjectStorage();
$this->requestStack = $requestStack;
$this->profileStack = $profileStack;
}

/**
Expand Down Expand Up @@ -94,9 +97,13 @@ public function onKernelResponse(FilterResponseEvent $event)
$this->profiles[$request] = $profile;

$this->parents[$request] = $this->requestStack->getParentRequest();

if ($this->profileStack instanceof ProfileStack) {
$this->profileStack->set($request, $profile);
}
}

public function onKernelTerminate(PostResponseEvent $event)
public function onKernelTerminate()
{
// attach children to parents
foreach ($this->profiles as $request) {
Expand All @@ -114,6 +121,10 @@ public function onKernelTerminate(PostResponseEvent $event)

$this->profiles = new \SplObjectStorage();
$this->parents = new \SplObjectStorage();

if ($this->profileStack instanceof ProfileStack) {
$this->profileStack->reset();
}
}

public static function getSubscribedEvents()
Expand Down
54 changes: 54 additions & 0 deletions src/Symfony/Component/HttpKernel/Profiler/ProfileStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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\Component\HttpKernel\Profiler;

use Symfony\Component\HttpFoundation\Request;

/**
* @internal
*/
final class ProfileStack
{
/**
* @var \SplObjectStorage
*/
private $profiles;

public function __construct()
{
$this->reset();
}

public function has(Request $request): bool
{
return isset($this->profiles[$request]);
}

public function get(Request $request): Profile
{
try {
return $this->profiles[$request];
} catch (\UnexpectedValueException $e) {
throw new \InvalidArgumentException('There is no profile in the stack for the passed request.');
}
}

public function set(Request $request, Profile $profile): void
{
$this->profiles[$request] = $profile;
}

public function reset(): void
{
$this->profiles = new \SplObjectStorage();
}
}
Loading