ProfilerController.php 14 KB

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