vendor/symfony/web-profiler-bundle/EventListener/WebDebugToolbarListener.php line 76

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bundle\WebProfilerBundle\EventListener;
  11. use Symfony\Bundle\FullStack;
  12. use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
  13. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
  17. use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
  18. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  19. use Symfony\Component\HttpKernel\KernelEvents;
  20. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  21. use Twig\Environment;
  22. /**
  23. * WebDebugToolbarListener injects the Web Debug Toolbar.
  24. *
  25. * The onKernelResponse method must be connected to the kernel.response event.
  26. *
  27. * The WDT is only injected on well-formed HTML (with a proper </body> tag).
  28. * This means that the WDT is never included in sub-requests or ESI requests.
  29. *
  30. * @author Fabien Potencier <fabien@symfony.com>
  31. *
  32. * @final
  33. */
  34. class WebDebugToolbarListener implements EventSubscriberInterface
  35. {
  36. public const DISABLED = 1;
  37. public const ENABLED = 2;
  38. protected $twig;
  39. protected $urlGenerator;
  40. protected $interceptRedirects;
  41. protected $mode;
  42. protected $excludedAjaxPaths;
  43. private $cspHandler;
  44. private $dumpDataCollector;
  45. public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, ?UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ?ContentSecurityPolicyHandler $cspHandler = null, ?DumpDataCollector $dumpDataCollector = null)
  46. {
  47. $this->twig = $twig;
  48. $this->urlGenerator = $urlGenerator;
  49. $this->interceptRedirects = $interceptRedirects;
  50. $this->mode = $mode;
  51. $this->excludedAjaxPaths = $excludedAjaxPaths;
  52. $this->cspHandler = $cspHandler;
  53. $this->dumpDataCollector = $dumpDataCollector;
  54. }
  55. public function isEnabled(): bool
  56. {
  57. return self::DISABLED !== $this->mode;
  58. }
  59. public function setMode(int $mode): void
  60. {
  61. if (self::DISABLED !== $mode && self::ENABLED !== $mode) {
  62. throw new \InvalidArgumentException(sprintf('Invalid value provided for mode, use one of "%s::DISABLED" or "%s::ENABLED".', self::class, self::class));
  63. }
  64. $this->mode = $mode;
  65. }
  66. public function onKernelResponse(ResponseEvent $event)
  67. {
  68. $response = $event->getResponse();
  69. $request = $event->getRequest();
  70. if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) {
  71. try {
  72. $response->headers->set(
  73. 'X-Debug-Token-Link',
  74. $this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL)
  75. );
  76. } catch (\Exception $e) {
  77. $response->headers->set('X-Debug-Error', \get_class($e).': '.preg_replace('/\s+/', ' ', $e->getMessage()));
  78. }
  79. }
  80. if (!$event->isMainRequest()) {
  81. return;
  82. }
  83. $nonces = [];
  84. if ($this->cspHandler) {
  85. if ($this->dumpDataCollector && $this->dumpDataCollector->getDumpsCount() > 0) {
  86. $this->cspHandler->disableCsp();
  87. }
  88. $nonces = $this->cspHandler->updateResponseHeaders($request, $response);
  89. }
  90. // do not capture redirects or modify XML HTTP Requests
  91. if ($request->isXmlHttpRequest()) {
  92. return;
  93. }
  94. if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) {
  95. if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) {
  96. // keep current flashes for one more request if using AutoExpireFlashBag
  97. $session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
  98. }
  99. $response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location')]));
  100. $response->setStatusCode(200);
  101. $response->headers->remove('Location');
  102. }
  103. if (self::DISABLED === $this->mode
  104. || !$response->headers->has('X-Debug-Token')
  105. || $response->isRedirection()
  106. || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type') ?? '', 'html'))
  107. || 'html' !== $request->getRequestFormat()
  108. || false !== stripos($response->headers->get('Content-Disposition', ''), 'attachment;')
  109. ) {
  110. return;
  111. }
  112. $this->injectToolbar($response, $request, $nonces);
  113. }
  114. /**
  115. * Injects the web debug toolbar into the given Response.
  116. */
  117. protected function injectToolbar(Response $response, Request $request, array $nonces)
  118. {
  119. $content = $response->getContent();
  120. $pos = strripos($content, '</body>');
  121. if (false !== $pos) {
  122. $toolbar = "\n".str_replace("\n", '', $this->twig->render(
  123. '@WebProfiler/Profiler/toolbar_js.html.twig',
  124. [
  125. 'full_stack' => class_exists(FullStack::class),
  126. 'excluded_ajax_paths' => $this->excludedAjaxPaths,
  127. 'token' => $response->headers->get('X-Debug-Token'),
  128. 'request' => $request,
  129. 'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null,
  130. 'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null,
  131. ]
  132. ))."\n";
  133. $content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
  134. $response->setContent($content);
  135. }
  136. }
  137. public static function getSubscribedEvents(): array
  138. {
  139. return [
  140. KernelEvents::RESPONSE => ['onKernelResponse', -128],
  141. ];
  142. }
  143. }