vendor/symfony/web-profiler-bundle/Controller/ProfilerController.php line 387

  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\Controller;
  11. use Symfony\Bundle\FullStack;
  12. use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
  13. use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
  14. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  15. use Symfony\Component\HttpFoundation\RedirectResponse;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpFoundation\Response;
  18. use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
  19. use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
  20. use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;
  21. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  22. use Symfony\Component\HttpKernel\Profiler\Profiler;
  23. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  24. use Twig\Environment;
  25. /**
  26. * @author Fabien Potencier <fabien@symfony.com>
  27. *
  28. * @internal
  29. */
  30. class ProfilerController
  31. {
  32. private TemplateManager $templateManager;
  33. private UrlGeneratorInterface $generator;
  34. private ?Profiler $profiler;
  35. private Environment $twig;
  36. private array $templates;
  37. private ?ContentSecurityPolicyHandler $cspHandler;
  38. private ?string $baseDir;
  39. public function __construct(UrlGeneratorInterface $generator, ?Profiler $profiler, Environment $twig, array $templates, ?ContentSecurityPolicyHandler $cspHandler = null, ?string $baseDir = null)
  40. {
  41. $this->generator = $generator;
  42. $this->profiler = $profiler;
  43. $this->twig = $twig;
  44. $this->templates = $templates;
  45. $this->cspHandler = $cspHandler;
  46. $this->baseDir = $baseDir;
  47. }
  48. /**
  49. * Redirects to the last profiles.
  50. *
  51. * @throws NotFoundHttpException
  52. */
  53. public function homeAction(): RedirectResponse
  54. {
  55. $this->denyAccessIfProfilerDisabled();
  56. return new RedirectResponse($this->generator->generate('_profiler_search_results', ['token' => 'empty', 'limit' => 10]), 302, ['Content-Type' => 'text/html']);
  57. }
  58. /**
  59. * Renders a profiler panel for the given token.
  60. *
  61. * @throws NotFoundHttpException
  62. */
  63. public function panelAction(Request $request, string $token): Response
  64. {
  65. $this->denyAccessIfProfilerDisabled();
  66. $this->cspHandler?->disableCsp();
  67. $panel = $request->query->get('panel');
  68. $page = $request->query->get('page', 'home');
  69. $profileType = $request->query->get('type', 'request');
  70. if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null, null, fn ($profile) => $profileType === $profile['virtual_type']))) {
  71. $token = $latest['token'];
  72. }
  73. if (!$profile = $this->profiler->loadProfile($token)) {
  74. return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request, 'profile_type' => $profileType]);
  75. }
  76. $profileType = $profile->getVirtualType() ?? 'request';
  77. if (null === $panel) {
  78. $panel = $profileType;
  79. foreach ($profile->getCollectors() as $collector) {
  80. if ($collector instanceof ExceptionDataCollector && $collector->hasException()) {
  81. $panel = $collector->getName();
  82. break;
  83. }
  84. if ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) {
  85. $panel = $collector->getName();
  86. }
  87. }
  88. }
  89. if (!$profile->hasCollector($panel)) {
  90. throw new NotFoundHttpException(\sprintf('Panel "%s" is not available for token "%s".', $panel, $token));
  91. }
  92. return $this->renderWithCspNonces($request, $this->getTemplateManager()->getName($profile, $panel), [
  93. 'token' => $token,
  94. 'profile' => $profile,
  95. 'collector' => $profile->getCollector($panel),
  96. 'panel' => $panel,
  97. 'page' => $page,
  98. 'request' => $request,
  99. 'templates' => $this->getTemplateManager()->getNames($profile),
  100. 'is_ajax' => $request->isXmlHttpRequest(),
  101. 'profiler_markup_version' => 3, // 1 = original profiler, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
  102. 'profile_type' => $profileType,
  103. ]);
  104. }
  105. /**
  106. * Renders the Web Debug Toolbar.
  107. *
  108. * @throws NotFoundHttpException
  109. */
  110. public function toolbarAction(Request $request, ?string $token = null): Response
  111. {
  112. if (null === $this->profiler) {
  113. throw new NotFoundHttpException('The profiler must be enabled.');
  114. }
  115. if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()
  116. && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag
  117. ) {
  118. // keep current flashes for one more request if using AutoExpireFlashBag
  119. $session->getFlashBag()->setAll($session->getFlashBag()->peekAll());
  120. }
  121. if ('empty' === $token || null === $token) {
  122. return new Response('', 200, ['Content-Type' => 'text/html']);
  123. }
  124. $this->profiler->disable();
  125. if (!$profile = $this->profiler->loadProfile($token)) {
  126. return new Response('', 404, ['Content-Type' => 'text/html']);
  127. }
  128. $url = null;
  129. try {
  130. $url = $this->generator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
  131. } catch (\Exception) {
  132. // the profiler is not enabled
  133. }
  134. return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', [
  135. 'full_stack' => class_exists(FullStack::class),
  136. 'request' => $request,
  137. 'profile' => $profile,
  138. 'templates' => $this->getTemplateManager()->getNames($profile),
  139. 'profiler_url' => $url,
  140. 'token' => $token,
  141. 'profiler_markup_version' => 3, // 1 = original toolbar, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler
  142. ]);
  143. }
  144. /**
  145. * Renders the profiler search bar.
  146. *
  147. * @throws NotFoundHttpException
  148. */
  149. public function searchBarAction(Request $request): Response
  150. {
  151. $this->denyAccessIfProfilerDisabled();
  152. $this->cspHandler?->disableCsp();
  153. $session = null;
  154. if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
  155. $session = $request->getSession();
  156. }
  157. return new Response(
  158. $this->twig->render('@WebProfiler/Profiler/search.html.twig', [
  159. 'token' => $request->query->get('token', $session?->get('_profiler_search_token')),
  160. 'ip' => $request->query->get('ip', $session?->get('_profiler_search_ip')),
  161. 'method' => $request->query->get('method', $session?->get('_profiler_search_method')),
  162. 'status_code' => $request->query->get('status_code', $session?->get('_profiler_search_status_code')),
  163. 'url' => $request->query->get('url', $session?->get('_profiler_search_url')),
  164. 'start' => $request->query->get('start', $session?->get('_profiler_search_start')),
  165. 'end' => $request->query->get('end', $session?->get('_profiler_search_end')),
  166. 'limit' => $request->query->get('limit', $session?->get('_profiler_search_limit')),
  167. 'request' => $request,
  168. 'render_hidden_by_default' => false,
  169. 'profile_type' => $request->query->get('type', $session?->get('_profiler_search_type', 'request')),
  170. ]),
  171. 200,
  172. ['Content-Type' => 'text/html']
  173. );
  174. }
  175. /**
  176. * Renders the search results.
  177. *
  178. * @throws NotFoundHttpException
  179. */
  180. public function searchResultsAction(Request $request, string $token): Response
  181. {
  182. $this->denyAccessIfProfilerDisabled();
  183. $this->cspHandler?->disableCsp();
  184. $profile = $this->profiler->loadProfile($token);
  185. $ip = $request->query->get('ip');
  186. $method = $request->query->get('method');
  187. $statusCode = $request->query->get('status_code');
  188. $url = $request->query->get('url');
  189. $start = $request->query->get('start', null);
  190. $end = $request->query->get('end', null);
  191. $limit = $request->query->get('limit');
  192. $profileType = $request->query->get('type', 'request');
  193. return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [
  194. 'request' => $request,
  195. 'token' => $token,
  196. 'profile' => $profile,
  197. 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']),
  198. 'ip' => $ip,
  199. 'method' => $method,
  200. 'status_code' => $statusCode,
  201. 'url' => $url,
  202. 'start' => $start,
  203. 'end' => $end,
  204. 'limit' => $limit,
  205. 'panel' => null,
  206. 'profile_type' => $profileType,
  207. ]);
  208. }
  209. /**
  210. * Narrows the search bar.
  211. *
  212. * @throws NotFoundHttpException
  213. */
  214. public function searchAction(Request $request): Response
  215. {
  216. $this->denyAccessIfProfilerDisabled();
  217. $ip = $request->query->get('ip');
  218. $method = $request->query->get('method');
  219. $statusCode = $request->query->get('status_code');
  220. $url = $request->query->get('url');
  221. $start = $request->query->get('start', null);
  222. $end = $request->query->get('end', null);
  223. $limit = $request->query->get('limit');
  224. $token = $request->query->get('token');
  225. $profileType = $request->query->get('type', 'request');
  226. if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) {
  227. $session = $request->getSession();
  228. $session->set('_profiler_search_ip', $ip);
  229. $session->set('_profiler_search_method', $method);
  230. $session->set('_profiler_search_status_code', $statusCode);
  231. $session->set('_profiler_search_url', $url);
  232. $session->set('_profiler_search_start', $start);
  233. $session->set('_profiler_search_end', $end);
  234. $session->set('_profiler_search_limit', $limit);
  235. $session->set('_profiler_search_token', $token);
  236. $session->set('_profiler_search_type', $profileType);
  237. }
  238. if (!empty($token)) {
  239. return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']);
  240. }
  241. $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']);
  242. return new RedirectResponse($this->generator->generate('_profiler_search_results', [
  243. 'token' => $tokens ? $tokens[0]['token'] : 'empty',
  244. 'ip' => $ip,
  245. 'method' => $method,
  246. 'status_code' => $statusCode,
  247. 'url' => $url,
  248. 'start' => $start,
  249. 'end' => $end,
  250. 'limit' => $limit,
  251. 'type' => $profileType,
  252. ]), 302, ['Content-Type' => 'text/html']);
  253. }
  254. /**
  255. * Displays the PHP info.
  256. *
  257. * @throws NotFoundHttpException
  258. */
  259. public function phpinfoAction(): Response
  260. {
  261. $this->denyAccessIfProfilerDisabled();
  262. $this->cspHandler?->disableCsp();
  263. ob_start();
  264. phpinfo();
  265. $phpinfo = ob_get_clean();
  266. return new Response($phpinfo, 200, ['Content-Type' => 'text/html']);
  267. }
  268. /**
  269. * Displays the Xdebug info.
  270. *
  271. * @throws NotFoundHttpException
  272. */
  273. public function xdebugAction(): Response
  274. {
  275. $this->denyAccessIfProfilerDisabled();
  276. if (!\function_exists('xdebug_info')) {
  277. throw new NotFoundHttpException('Xdebug must be installed in version 3.');
  278. }
  279. $this->cspHandler?->disableCsp();
  280. ob_start();
  281. xdebug_info();
  282. $xdebugInfo = ob_get_clean();
  283. return new Response($xdebugInfo, 200, ['Content-Type' => 'text/html']);
  284. }
  285. /**
  286. * Returns the custom web fonts used in the profiler.
  287. *
  288. * @throws NotFoundHttpException
  289. */
  290. public function fontAction(string $fontName): Response
  291. {
  292. $this->denyAccessIfProfilerDisabled();
  293. if ('JetBrainsMono' !== $fontName) {
  294. throw new NotFoundHttpException(\sprintf('Font file "%s.woff2" not found.', $fontName));
  295. }
  296. $fontFile = \dirname(__DIR__).'/Resources/fonts/'.$fontName.'.woff2';
  297. if (!is_file($fontFile) || !is_readable($fontFile)) {
  298. throw new NotFoundHttpException(\sprintf('Cannot read font file "%s".', $fontFile));
  299. }
  300. $this->profiler?->disable();
  301. return new BinaryFileResponse($fontFile, 200, ['Content-Type' => 'font/woff2']);
  302. }
  303. /**
  304. * Displays the source of a file.
  305. *
  306. * @throws NotFoundHttpException
  307. */
  308. public function openAction(Request $request): Response
  309. {
  310. if (null === $this->baseDir) {
  311. throw new NotFoundHttpException('The base dir should be set.');
  312. }
  313. $this->profiler?->disable();
  314. $file = $request->query->get('file');
  315. $line = $request->query->get('line');
  316. $filename = $this->baseDir.\DIRECTORY_SEPARATOR.$file;
  317. if (preg_match("'(^|[/\\\\])\.'", $file) || !is_readable($filename)) {
  318. throw new NotFoundHttpException(\sprintf('The file "%s" cannot be opened.', $file));
  319. }
  320. return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/open.html.twig', [
  321. 'file_info' => new \SplFileInfo($filename),
  322. 'file' => $file,
  323. 'line' => $line,
  324. ]);
  325. }
  326. protected function getTemplateManager(): TemplateManager
  327. {
  328. return $this->templateManager ??= new TemplateManager($this->profiler, $this->twig, $this->templates);
  329. }
  330. private function denyAccessIfProfilerDisabled(): void
  331. {
  332. if (null === $this->profiler) {
  333. throw new NotFoundHttpException('The profiler must be enabled.');
  334. }
  335. $this->profiler->disable();
  336. }
  337. private function renderWithCspNonces(Request $request, string $template, array $variables, int $code = 200, array $headers = ['Content-Type' => 'text/html']): Response
  338. {
  339. $response = new Response('', $code, $headers);
  340. $nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : [];
  341. $variables['csp_script_nonce'] = $nonces['csp_script_nonce'] ?? null;
  342. $variables['csp_style_nonce'] = $nonces['csp_style_nonce'] ?? null;
  343. $response->setContent($this->twig->render($template, $variables));
  344. return $response;
  345. }
  346. }