login consent app sql

This commit is contained in:
2022-05-03 08:54:45 +02:00
parent e7253acfd8
commit f9a6535906
1652 changed files with 187600 additions and 45 deletions

View File

@ -0,0 +1,95 @@
CHANGELOG
=========
5.4
---
* Add a "preview" tab in mailer profiler for HTML email
5.2.0
-----
* added session usage
5.0.0
-----
* removed the `ExceptionController`, use `ExceptionPanelController` instead
* removed the `TemplateManager::templateExists()` method
4.4.0
-----
* added support for the Mailer component
* added support for the HttpClient component
* added button to clear the ajax request tab
* deprecated the `ExceptionController::templateExists()` method
* deprecated the `TemplateManager::templateExists()` method
* deprecated the `ExceptionController` in favor of `ExceptionPanelController`
* marked all classes of the WebProfilerBundle as internal
* added a section with the stamps of a message after it is dispatched in the Messenger panel
4.3.0
-----
* Replaced the canvas performance graph renderer with an SVG renderer
4.1.0
-----
* added information about orphaned events
* made the toolbar auto-update with info from ajax reponses when they set the
`Symfony-Debug-Toolbar-Replace header` to `1`
4.0.0
-----
* removed the `WebProfilerExtension::dumpValue()` method
* removed the `getTemplates()` method of the `TemplateManager` class in favor of the ``getNames()`` method
* removed the `web_profiler.position` config option and the
`web_profiler.debug_toolbar.position` container parameter
3.4.0
-----
* Deprecated the `web_profiler.position` config option (in 4.0 version the toolbar
will always be displayed at the bottom) and the `web_profiler.debug_toolbar.position`
container parameter.
3.1.0
-----
* added information about redirected and forwarded requests to the profiler
3.0.0
-----
* removed profiler:import and profiler:export commands
2.8.0
-----
* deprecated profiler:import and profiler:export commands
2.7.0
-----
* [BC BREAK] if you are using a DB to store profiles, the table must be dropped
* added the HTTP status code to profiles
2.3.0
-----
* draw retina canvas if devicePixelRatio is bigger than 1
2.1.0
-----
* deprecated the verbose setting (not relevant anymore)
* [BC BREAK] You must clear old profiles after upgrading to 2.1 (don't forget
to remove the table if you are using a DB)
* added support for the request method
* added a routing panel
* added a timeline panel
* The toolbar position can now be configured via the `position` option (can
be `top` or `bottom`)

View File

@ -0,0 +1,61 @@
<?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\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
/**
* Renders the exception panel.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class ExceptionPanelController
{
private $errorRenderer;
private $profiler;
public function __construct(HtmlErrorRenderer $errorRenderer, Profiler $profiler = null)
{
$this->errorRenderer = $errorRenderer;
$this->profiler = $profiler;
}
/**
* Renders the exception panel stacktrace for the given token.
*/
public function body(string $token): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$exception = $this->profiler->loadProfile($token)
->getCollector('exception')
->getException()
;
return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']);
}
/**
* Renders the exception panel stylesheet.
*/
public function stylesheet(): Response
{
return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']);
}
}

View File

@ -0,0 +1,391 @@
<?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\Bundle\WebProfilerBundle\Controller;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
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\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ProfilerController
{
private $templateManager;
private $generator;
private $profiler;
private $twig;
private $templates;
private $cspHandler;
private $baseDir;
public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, Environment $twig, array $templates, ContentSecurityPolicyHandler $cspHandler = null, string $baseDir = null)
{
$this->generator = $generator;
$this->profiler = $profiler;
$this->twig = $twig;
$this->templates = $templates;
$this->cspHandler = $cspHandler;
$this->baseDir = $baseDir;
}
/**
* Redirects to the last profiles.
*
* @throws NotFoundHttpException
*/
public function homeAction(): RedirectResponse
{
$this->denyAccessIfProfilerDisabled();
return new RedirectResponse($this->generator->generate('_profiler_search_results', ['token' => 'empty', 'limit' => 10]), 302, ['Content-Type' => 'text/html']);
}
/**
* Renders a profiler panel for the given token.
*
* @throws NotFoundHttpException
*/
public function panelAction(Request $request, string $token): Response
{
$this->denyAccessIfProfilerDisabled();
if (null !== $this->cspHandler) {
$this->cspHandler->disableCsp();
}
$panel = $request->query->get('panel');
$page = $request->query->get('page', 'home');
if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null))) {
$token = $latest['token'];
}
if (!$profile = $this->profiler->loadProfile($token)) {
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request]);
}
if (null === $panel) {
$panel = 'request';
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();
}
}
}
if (!$profile->hasCollector($panel)) {
throw new NotFoundHttpException(sprintf('Panel "%s" is not available for token "%s".', $panel, $token));
}
return $this->renderWithCspNonces($request, $this->getTemplateManager()->getName($profile, $panel), [
'token' => $token,
'profile' => $profile,
'collector' => $profile->getCollector($panel),
'panel' => $panel,
'page' => $page,
'request' => $request,
'templates' => $this->getTemplateManager()->getNames($profile),
'is_ajax' => $request->isXmlHttpRequest(),
'profiler_markup_version' => 2, // 1 = original profiler, 2 = Symfony 2.8+ profiler
]);
}
/**
* Renders the Web Debug Toolbar.
*
* @throws NotFoundHttpException
*/
public function toolbarAction(Request $request, string $token = null): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
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());
}
if ('empty' === $token || null === $token) {
return new Response('', 200, ['Content-Type' => 'text/html']);
}
$this->profiler->disable();
if (!$profile = $this->profiler->loadProfile($token)) {
return new Response('', 404, ['Content-Type' => 'text/html']);
}
$url = null;
try {
$url = $this->generator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
} catch (\Exception $e) {
// the profiler is not enabled
}
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', [
'full_stack' => class_exists(FullStack::class),
'request' => $request,
'profile' => $profile,
'templates' => $this->getTemplateManager()->getNames($profile),
'profiler_url' => $url,
'token' => $token,
'profiler_markup_version' => 2, // 1 = original toolbar, 2 = Symfony 2.8+ toolbar
]);
}
/**
* Renders the profiler search bar.
*
* @throws NotFoundHttpException
*/
public function searchBarAction(Request $request): Response
{
$this->denyAccessIfProfilerDisabled();
if (null !== $this->cspHandler) {
$this->cspHandler->disableCsp();
}
if (!$request->hasSession()) {
$ip =
$method =
$statusCode =
$url =
$start =
$end =
$limit =
$token = null;
} else {
$session = $request->getSession();
$ip = $request->query->get('ip', $session->get('_profiler_search_ip'));
$method = $request->query->get('method', $session->get('_profiler_search_method'));
$statusCode = $request->query->get('status_code', $session->get('_profiler_search_status_code'));
$url = $request->query->get('url', $session->get('_profiler_search_url'));
$start = $request->query->get('start', $session->get('_profiler_search_start'));
$end = $request->query->get('end', $session->get('_profiler_search_end'));
$limit = $request->query->get('limit', $session->get('_profiler_search_limit'));
$token = $request->query->get('token', $session->get('_profiler_search_token'));
}
return new Response(
$this->twig->render('@WebProfiler/Profiler/search.html.twig', [
'token' => $token,
'ip' => $ip,
'method' => $method,
'status_code' => $statusCode,
'url' => $url,
'start' => $start,
'end' => $end,
'limit' => $limit,
'request' => $request,
]),
200,
['Content-Type' => 'text/html']
);
}
/**
* Renders the search results.
*
* @throws NotFoundHttpException
*/
public function searchResultsAction(Request $request, string $token): Response
{
$this->denyAccessIfProfilerDisabled();
if (null !== $this->cspHandler) {
$this->cspHandler->disableCsp();
}
$profile = $this->profiler->loadProfile($token);
$ip = $request->query->get('ip');
$method = $request->query->get('method');
$statusCode = $request->query->get('status_code');
$url = $request->query->get('url');
$start = $request->query->get('start', null);
$end = $request->query->get('end', null);
$limit = $request->query->get('limit');
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [
'request' => $request,
'token' => $token,
'profile' => $profile,
'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode),
'ip' => $ip,
'method' => $method,
'status_code' => $statusCode,
'url' => $url,
'start' => $start,
'end' => $end,
'limit' => $limit,
'panel' => null,
]);
}
/**
* Narrows the search bar.
*
* @throws NotFoundHttpException
*/
public function searchAction(Request $request): Response
{
$this->denyAccessIfProfilerDisabled();
$ip = $request->query->get('ip');
$method = $request->query->get('method');
$statusCode = $request->query->get('status_code');
$url = $request->query->get('url');
$start = $request->query->get('start', null);
$end = $request->query->get('end', null);
$limit = $request->query->get('limit');
$token = $request->query->get('token');
if ($request->hasSession()) {
$session = $request->getSession();
$session->set('_profiler_search_ip', $ip);
$session->set('_profiler_search_method', $method);
$session->set('_profiler_search_status_code', $statusCode);
$session->set('_profiler_search_url', $url);
$session->set('_profiler_search_start', $start);
$session->set('_profiler_search_end', $end);
$session->set('_profiler_search_limit', $limit);
$session->set('_profiler_search_token', $token);
}
if (!empty($token)) {
return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']);
}
$tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode);
return new RedirectResponse($this->generator->generate('_profiler_search_results', [
'token' => $tokens ? $tokens[0]['token'] : 'empty',
'ip' => $ip,
'method' => $method,
'status_code' => $statusCode,
'url' => $url,
'start' => $start,
'end' => $end,
'limit' => $limit,
]), 302, ['Content-Type' => 'text/html']);
}
/**
* Displays the PHP info.
*
* @throws NotFoundHttpException
*/
public function phpinfoAction(): Response
{
$this->denyAccessIfProfilerDisabled();
if (null !== $this->cspHandler) {
$this->cspHandler->disableCsp();
}
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
return new Response($phpinfo, 200, ['Content-Type' => 'text/html']);
}
/**
* Displays the source of a file.
*
* @throws NotFoundHttpException
*/
public function openAction(Request $request): Response
{
if (null === $this->baseDir) {
throw new NotFoundHttpException('The base dir should be set.');
}
if ($this->profiler) {
$this->profiler->disable();
}
$file = $request->query->get('file');
$line = $request->query->get('line');
$filename = $this->baseDir.\DIRECTORY_SEPARATOR.$file;
if (preg_match("'(^|[/\\\\])\.'", $file) || !is_readable($filename)) {
throw new NotFoundHttpException(sprintf('The file "%s" cannot be opened.', $file));
}
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/open.html.twig', [
'filename' => $filename,
'file' => $file,
'line' => $line,
]);
}
/**
* Gets the Template Manager.
*/
protected function getTemplateManager(): TemplateManager
{
if (null === $this->templateManager) {
$this->templateManager = new TemplateManager($this->profiler, $this->twig, $this->templates);
}
return $this->templateManager;
}
private function denyAccessIfProfilerDisabled()
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$this->profiler->disable();
}
private function renderWithCspNonces(Request $request, string $template, array $variables, int $code = 200, array $headers = ['Content-Type' => 'text/html']): Response
{
$response = new Response('', $code, $headers);
$nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : [];
$variables['csp_script_nonce'] = $nonces['csp_script_nonce'] ?? null;
$variables['csp_style_nonce'] = $nonces['csp_style_nonce'] ?? null;
$response->setContent($this->twig->render($template, $variables));
return $response;
}
}

View File

@ -0,0 +1,104 @@
<?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\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\Matcher\TraceableUrlMatcher;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class RouterController
{
private $profiler;
private $twig;
private $matcher;
private $routes;
/**
* @var ExpressionFunctionProviderInterface[]
*/
private $expressionLanguageProviders = [];
public function __construct(Profiler $profiler = null, Environment $twig, UrlMatcherInterface $matcher = null, RouteCollection $routes = null, iterable $expressionLanguageProviders = [])
{
$this->profiler = $profiler;
$this->twig = $twig;
$this->matcher = $matcher;
$this->routes = (null === $routes && $matcher instanceof RouterInterface) ? $matcher->getRouteCollection() : $routes;
$this->expressionLanguageProviders = $expressionLanguageProviders;
}
/**
* Renders the profiler panel for the given token.
*
* @throws NotFoundHttpException
*/
public function panelAction(string $token): Response
{
if (null === $this->profiler) {
throw new NotFoundHttpException('The profiler must be enabled.');
}
$this->profiler->disable();
if (null === $this->matcher || null === $this->routes) {
return new Response('The Router is not enabled.', 200, ['Content-Type' => 'text/html']);
}
$profile = $this->profiler->loadProfile($token);
/** @var RequestDataCollector $request */
$request = $profile->getCollector('request');
return new Response($this->twig->render('@WebProfiler/Router/panel.html.twig', [
'request' => $request,
'router' => $profile->getCollector('router'),
'traces' => $this->getTraces($request, $profile->getMethod()),
]), 200, ['Content-Type' => 'text/html']);
}
/**
* Returns the routing traces associated to the given request.
*/
private function getTraces(RequestDataCollector $request, string $method): array
{
$traceRequest = Request::create(
$request->getPathInfo(),
$request->getRequestServer(true)->get('REQUEST_METHOD'),
\in_array($request->getMethod(), ['DELETE', 'PATCH', 'POST', 'PUT'], true) ? $request->getRequestRequest()->all() : $request->getRequestQuery()->all(),
$request->getRequestCookies(true)->all(),
[],
$request->getRequestServer(true)->all()
);
$context = $this->matcher->getContext();
$context->setMethod($method);
$matcher = new TraceableUrlMatcher($this->routes, $context);
foreach ($this->expressionLanguageProviders as $provider) {
$matcher->addExpressionLanguageProvider($provider);
}
return $matcher->getTracesForRequest($traceRequest);
}
}

View File

@ -0,0 +1,270 @@
<?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\Bundle\WebProfilerBundle\Csp;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Handles Content-Security-Policy HTTP header for the WebProfiler Bundle.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class ContentSecurityPolicyHandler
{
private $nonceGenerator;
private $cspDisabled = false;
public function __construct(NonceGenerator $nonceGenerator)
{
$this->nonceGenerator = $nonceGenerator;
}
/**
* Returns an array of nonces to be used in Twig templates and Content-Security-Policy headers.
*
* Nonce can be provided by;
* - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin
* - The response - A call to getNonces() has already been done previously. Same nonce are returned
* - They are otherwise randomly generated
*/
public function getNonces(Request $request, Response $response): array
{
if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) {
return [
'csp_script_nonce' => $request->headers->get('X-SymfonyProfiler-Script-Nonce'),
'csp_style_nonce' => $request->headers->get('X-SymfonyProfiler-Style-Nonce'),
];
}
if ($response->headers->has('X-SymfonyProfiler-Script-Nonce') && $response->headers->has('X-SymfonyProfiler-Style-Nonce')) {
return [
'csp_script_nonce' => $response->headers->get('X-SymfonyProfiler-Script-Nonce'),
'csp_style_nonce' => $response->headers->get('X-SymfonyProfiler-Style-Nonce'),
];
}
$nonces = [
'csp_script_nonce' => $this->generateNonce(),
'csp_style_nonce' => $this->generateNonce(),
];
$response->headers->set('X-SymfonyProfiler-Script-Nonce', $nonces['csp_script_nonce']);
$response->headers->set('X-SymfonyProfiler-Style-Nonce', $nonces['csp_style_nonce']);
return $nonces;
}
/**
* Disables Content-Security-Policy.
*
* All related headers will be removed.
*/
public function disableCsp()
{
$this->cspDisabled = true;
}
/**
* Cleanup temporary headers and updates Content-Security-Policy headers.
*
* @return array Nonces used by the bundle in Content-Security-Policy header
*/
public function updateResponseHeaders(Request $request, Response $response): array
{
if ($this->cspDisabled) {
$this->removeCspHeaders($response);
return [];
}
$nonces = $this->getNonces($request, $response);
$this->cleanHeaders($response);
$this->updateCspHeaders($response, $nonces);
return $nonces;
}
private function cleanHeaders(Response $response)
{
$response->headers->remove('X-SymfonyProfiler-Script-Nonce');
$response->headers->remove('X-SymfonyProfiler-Style-Nonce');
}
private function removeCspHeaders(Response $response)
{
$response->headers->remove('X-Content-Security-Policy');
$response->headers->remove('Content-Security-Policy');
$response->headers->remove('Content-Security-Policy-Report-Only');
}
/**
* Updates Content-Security-Policy headers in a response.
*/
private function updateCspHeaders(Response $response, array $nonces = []): array
{
$nonces = array_replace([
'csp_script_nonce' => $this->generateNonce(),
'csp_style_nonce' => $this->generateNonce(),
], $nonces);
$ruleIsSet = false;
$headers = $this->getCspHeaders($response);
$types = [
'script-src' => 'csp_script_nonce',
'script-src-elem' => 'csp_script_nonce',
'style-src' => 'csp_style_nonce',
'style-src-elem' => 'csp_style_nonce',
];
foreach ($headers as $header => $directives) {
foreach ($types as $type => $tokenName) {
if ($this->authorizesInline($directives, $type)) {
continue;
}
if (!isset($headers[$header][$type])) {
if (null === $fallback = $this->getDirectiveFallback($directives, $type)) {
continue;
}
if (['\'none\''] === $fallback) {
// Fallback came from "default-src: 'none'"
// 'none' is invalid if it's not the only expression in the source list, so we leave it out
$fallback = [];
}
$headers[$header][$type] = $fallback;
}
$ruleIsSet = true;
if (!\in_array('\'unsafe-inline\'', $headers[$header][$type], true)) {
$headers[$header][$type][] = '\'unsafe-inline\'';
}
$headers[$header][$type][] = sprintf('\'nonce-%s\'', $nonces[$tokenName]);
}
}
if (!$ruleIsSet) {
return $nonces;
}
foreach ($headers as $header => $directives) {
$response->headers->set($header, $this->generateCspHeader($directives));
}
return $nonces;
}
/**
* Generates a valid Content-Security-Policy nonce.
*/
private function generateNonce(): string
{
return $this->nonceGenerator->generate();
}
/**
* Converts a directive set array into Content-Security-Policy header.
*/
private function generateCspHeader(array $directives): string
{
return array_reduce(array_keys($directives), function ($res, $name) use ($directives) {
return ('' !== $res ? $res.'; ' : '').sprintf('%s %s', $name, implode(' ', $directives[$name]));
}, '');
}
/**
* Converts a Content-Security-Policy header value into a directive set array.
*/
private function parseDirectives(string $header): array
{
$directives = [];
foreach (explode(';', $header) as $directive) {
$parts = explode(' ', trim($directive));
if (\count($parts) < 1) {
continue;
}
$name = array_shift($parts);
$directives[$name] = $parts;
}
return $directives;
}
/**
* Detects if the 'unsafe-inline' is prevented for a directive within the directive set.
*/
private function authorizesInline(array $directivesSet, string $type): bool
{
if (isset($directivesSet[$type])) {
$directives = $directivesSet[$type];
} elseif (null === $directives = $this->getDirectiveFallback($directivesSet, $type)) {
return false;
}
return \in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives);
}
private function hasHashOrNonce(array $directives): bool
{
foreach ($directives as $directive) {
if (!str_ends_with($directive, '\'')) {
continue;
}
if ('\'nonce-' === substr($directive, 0, 7)) {
return true;
}
if (\in_array(substr($directive, 0, 8), ['\'sha256-', '\'sha384-', '\'sha512-'], true)) {
return true;
}
}
return false;
}
private function getDirectiveFallback(array $directiveSet, string $type)
{
if (\in_array($type, ['script-src-elem', 'style-src-elem'], true) || !isset($directiveSet['default-src'])) {
// Let the browser fallback on it's own
return null;
}
return $directiveSet['default-src'];
}
/**
* Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from
* a response.
*/
private function getCspHeaders(Response $response): array
{
$headers = [];
if ($response->headers->has('Content-Security-Policy')) {
$headers['Content-Security-Policy'] = $this->parseDirectives($response->headers->get('Content-Security-Policy'));
}
if ($response->headers->has('Content-Security-Policy-Report-Only')) {
$headers['Content-Security-Policy-Report-Only'] = $this->parseDirectives($response->headers->get('Content-Security-Policy-Report-Only'));
}
if ($response->headers->has('X-Content-Security-Policy')) {
$headers['X-Content-Security-Policy'] = $this->parseDirectives($response->headers->get('X-Content-Security-Policy'));
}
return $headers;
}
}

View File

@ -0,0 +1,27 @@
<?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\Bundle\WebProfilerBundle\Csp;
/**
* Generates Content-Security-Policy nonce.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @internal
*/
class NonceGenerator
{
public function generate(): string
{
return bin2hex(random_bytes(16));
}
}

View File

@ -0,0 +1,46 @@
<?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\Bundle\WebProfilerBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This class contains the configuration information for the bundle.
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree builder.
*
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('web_profiler');
$treeBuilder->getRootNode()
->children()
->booleanNode('toolbar')->defaultFalse()->end()
->booleanNode('intercept_redirects')->defaultFalse()->end()
->scalarNode('excluded_ajax_paths')->defaultValue('^/((index|app(_[\w]+)?)\.php/)?_wdt')->end()
->end()
;
return $treeBuilder;
}
}

View File

@ -0,0 +1,72 @@
<?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\Bundle\WebProfilerBundle\DependencyInjection;
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
/**
* WebProfilerExtension.
*
* Usage:
*
* <webprofiler:config
* toolbar="true"
* intercept-redirects="true"
* />
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class WebProfilerExtension extends Extension
{
/**
* Loads the web profiler configuration.
*
* @param array $configs An array of configuration settings
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('profiler.php');
if ($config['toolbar'] || $config['intercept_redirects']) {
$loader->load('toolbar.php');
$container->getDefinition('web_profiler.debug_toolbar')->replaceArgument(4, $config['excluded_ajax_paths']);
$container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']);
$container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED);
}
$container->getDefinition('debug.file_link_formatter')
->replaceArgument(3, new ServiceClosureArgument(new Reference('debug.file_link_formatter.url_format')));
}
/**
* {@inheritdoc}
*/
public function getXsdValidationBasePath()
{
return __DIR__.'/../Resources/config/schema';
}
public function getNamespace()
{
return 'http://symfony.com/schema/dic/webprofiler';
}
}

View File

@ -0,0 +1,165 @@
<?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\Bundle\WebProfilerBundle\EventListener;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
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\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
/**
* WebDebugToolbarListener injects the Web Debug Toolbar.
*
* The onKernelResponse method must be connected to the kernel.response event.
*
* The WDT is only injected on well-formed HTML (with a proper </body> tag).
* This means that the WDT is never included in sub-requests or ESI requests.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class WebDebugToolbarListener implements EventSubscriberInterface
{
public const DISABLED = 1;
public const ENABLED = 2;
protected $twig;
protected $urlGenerator;
protected $interceptRedirects;
protected $mode;
protected $excludedAjaxPaths;
private $cspHandler;
private $dumpDataCollector;
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)
{
$this->twig = $twig;
$this->urlGenerator = $urlGenerator;
$this->interceptRedirects = $interceptRedirects;
$this->mode = $mode;
$this->excludedAjaxPaths = $excludedAjaxPaths;
$this->cspHandler = $cspHandler;
$this->dumpDataCollector = $dumpDataCollector;
}
public function isEnabled(): bool
{
return self::DISABLED !== $this->mode;
}
public function setMode(int $mode): void
{
if (self::DISABLED !== $mode && self::ENABLED !== $mode) {
throw new \InvalidArgumentException(sprintf('Invalid value provided for mode, use one of "%s::DISABLED" or "%s::ENABLED".', self::class, self::class));
}
$this->mode = $mode;
}
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();
if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) {
try {
$response->headers->set(
'X-Debug-Token-Link',
$this->urlGenerator->generate('_profiler', ['token' => $response->headers->get('X-Debug-Token')], UrlGeneratorInterface::ABSOLUTE_URL)
);
} catch (\Exception $e) {
$response->headers->set('X-Debug-Error', \get_class($e).': '.preg_replace('/\s+/', ' ', $e->getMessage()));
}
}
if (!$event->isMainRequest()) {
return;
}
$nonces = [];
if ($this->cspHandler) {
if ($this->dumpDataCollector && $this->dumpDataCollector->getDumpsCount() > 0) {
$this->cspHandler->disableCsp();
}
$nonces = $this->cspHandler->updateResponseHeaders($request, $response);
}
// do not capture redirects or modify XML HTTP Requests
if ($request->isXmlHttpRequest()) {
return;
}
if ($response->headers->has('X-Debug-Token') && $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());
}
$response->setContent($this->twig->render('@WebProfiler/Profiler/toolbar_redirect.html.twig', ['location' => $response->headers->get('Location')]));
$response->setStatusCode(200);
$response->headers->remove('Location');
}
if (self::DISABLED === $this->mode
|| !$response->headers->has('X-Debug-Token')
|| $response->isRedirection()
|| ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html'))
|| 'html' !== $request->getRequestFormat()
|| false !== stripos($response->headers->get('Content-Disposition', ''), 'attachment;')
) {
return;
}
$this->injectToolbar($response, $request, $nonces);
}
/**
* Injects the web debug toolbar into the given Response.
*/
protected function injectToolbar(Response $response, Request $request, array $nonces)
{
$content = $response->getContent();
$pos = strripos($content, '</body>');
if (false !== $pos) {
$toolbar = "\n".str_replace("\n", '', $this->twig->render(
'@WebProfiler/Profiler/toolbar_js.html.twig',
[
'full_stack' => class_exists(FullStack::class),
'excluded_ajax_paths' => $this->excludedAjaxPaths,
'token' => $response->headers->get('X-Debug-Token'),
'request' => $request,
'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null,
'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null,
]
))."\n";
$content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
$response->setContent($content);
}
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => ['onKernelResponse', -128],
];
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,90 @@
<?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\Bundle\WebProfilerBundle\Profiler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Artur Wielogórski <wodor@wodor.net>
*
* @internal
*/
class TemplateManager
{
protected $twig;
protected $templates;
protected $profiler;
public function __construct(Profiler $profiler, Environment $twig, array $templates)
{
$this->profiler = $profiler;
$this->twig = $twig;
$this->templates = $templates;
}
/**
* Gets the template name for a given panel.
*
* @return mixed
*
* @throws NotFoundHttpException
*/
public function getName(Profile $profile, string $panel)
{
$templates = $this->getNames($profile);
if (!isset($templates[$panel])) {
throw new NotFoundHttpException(sprintf('Panel "%s" is not registered in profiler or is not present in viewed profile.', $panel));
}
return $templates[$panel];
}
/**
* Gets template names of templates that are present in the viewed profile.
*
* @throws \UnexpectedValueException
*/
public function getNames(Profile $profile): array
{
$loader = $this->twig->getLoader();
$templates = [];
foreach ($this->templates as $arguments) {
if (null === $arguments) {
continue;
}
[$name, $template] = $arguments;
if (!$this->profiler->has($name) || !$profile->hasCollector($name)) {
continue;
}
if (str_ends_with($template, '.html.twig')) {
$template = substr($template, 0, -10);
}
if (!$loader->exists($template.'.html.twig')) {
throw new \UnexpectedValueException(sprintf('The profiler template "%s.html.twig" for data collector "%s" does not exist.', $template, $name));
}
$templates[$name] = $template.'.html.twig';
}
return $templates;
}
}

View File

@ -0,0 +1,16 @@
WebProfilerBundle
=================
WebProfilerBundle provides a **development tool** that gives detailed
information about the execution of any request.
**Never** enable it on production servers as it will lead to major security
vulnerabilities in your project.
Resources
---------
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1,5 @@
Icons License
=============
Icons created by Sensio (http://www.sensio.com/) are shared under a Creative
Commons Attribution license (http://creativecommons.org/licenses/by-sa/3.0/).

View File

@ -0,0 +1,83 @@
<?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\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController;
use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController;
use Symfony\Bundle\WebProfilerBundle\Controller\RouterController;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator;
use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
return static function (ContainerConfigurator $container) {
$container->services()
->set('web_profiler.controller.profiler', ProfilerController::class)
->public()
->args([
service('router')->nullOnInvalid(),
service('profiler')->nullOnInvalid(),
service('twig'),
param('data_collector.templates'),
service('web_profiler.csp.handler'),
param('kernel.project_dir'),
])
->set('web_profiler.controller.router', RouterController::class)
->public()
->args([
service('profiler')->nullOnInvalid(),
service('twig'),
service('router')->nullOnInvalid(),
null,
tagged_iterator('routing.expression_language_provider'),
])
->set('web_profiler.controller.exception_panel', ExceptionPanelController::class)
->public()
->args([
service('error_handler.error_renderer.html'),
service('profiler')->nullOnInvalid(),
])
->set('web_profiler.csp.handler', ContentSecurityPolicyHandler::class)
->args([
inline_service(NonceGenerator::class),
])
->set('twig.extension.webprofiler', WebProfilerExtension::class)
->args([
inline_service(HtmlDumper::class)
->args([null, param('kernel.charset'), HtmlDumper::DUMP_LIGHT_ARRAY])
->call('setDisplayOptions', [['maxStringLength' => 4096, 'fileLinkFormat' => service('debug.file_link_formatter')]]),
])
->tag('twig.extension')
->set('debug.file_link_formatter', FileLinkFormatter::class)
->args([
param('debug.file_link_format'),
service('request_stack')->ignoreOnInvalid(),
param('kernel.project_dir'),
'/_profiler/open?file=%%f&line=%%l#line%%l',
])
->set('debug.file_link_formatter.url_format', 'string')
->factory([FileLinkFormatter::class, 'generateUrlFormat'])
->args([
service('router'),
'_profiler_open_file',
'?file=%%f&line=%%l#line%%l',
])
;
};

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_profiler_home" path="/">
<default key="_controller">web_profiler.controller.profiler::homeAction</default>
</route>
<route id="_profiler_search" path="/search">
<default key="_controller">web_profiler.controller.profiler::searchAction</default>
</route>
<route id="_profiler_search_bar" path="/search_bar">
<default key="_controller">web_profiler.controller.profiler::searchBarAction</default>
</route>
<route id="_profiler_phpinfo" path="/phpinfo">
<default key="_controller">web_profiler.controller.profiler::phpinfoAction</default>
</route>
<route id="_profiler_search_results" path="/{token}/search/results">
<default key="_controller">web_profiler.controller.profiler::searchResultsAction</default>
</route>
<route id="_profiler_open_file" path="/open">
<default key="_controller">web_profiler.controller.profiler::openAction</default>
</route>
<route id="_profiler" path="/{token}">
<default key="_controller">web_profiler.controller.profiler::panelAction</default>
</route>
<route id="_profiler_router" path="/{token}/router">
<default key="_controller">web_profiler.controller.router::panelAction</default>
</route>
<route id="_profiler_exception" path="/{token}/exception">
<default key="_controller">web_profiler.controller.exception_panel::body</default>
</route>
<route id="_profiler_exception_css" path="/{token}/exception.css">
<default key="_controller">web_profiler.controller.exception_panel::stylesheet</default>
</route>
</routes>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_wdt" path="/{token}">
<default key="_controller">web_profiler.controller.profiler::toolbarAction</default>
</route>
</routes>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/webprofiler"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/webprofiler"
elementFormDefault="qualified">
<xsd:element name="config" type="config" />
<xsd:complexType name="config">
<xsd:attribute name="toolbar" type="xsd:boolean" />
<xsd:attribute name="intercept-redirects" type="xsd:boolean" />
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,31 @@
<?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\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
return static function (ContainerConfigurator $container) {
$container->services()
->set('web_profiler.debug_toolbar', WebDebugToolbarListener::class)
->args([
service('twig'),
param('web_profiler.debug_toolbar.intercept_redirects'),
param('web_profiler.debug_toolbar.mode'),
service('router')->ignoreOnInvalid(),
abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'),
service('web_profiler.csp.handler'),
service('data_collector.dump')->ignoreOnInvalid(),
])
->tag('kernel.event_subscriber')
;
};

View File

@ -0,0 +1,35 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{{ include('@WebProfiler/Icon/ajax.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">0</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<span class="sf-toolbar-header">
<b class="sf-toolbar-ajax-info"></b>
<b class="sf-toolbar-action">(<a class="sf-toolbar-ajax-clear" href="javascript:void(0);">Clear</a>)</b>
</span>
</div>
<div class="sf-toolbar-info-piece">
<table class="sf-toolbar-ajax-requests">
<thead>
<tr>
<th>#</th>
<th>Profile</th>
<th>Method</th>
<th>Type</th>
<th>Status</th>
<th>URL</th>
<th>Time</th>
</tr>
</thead>
<tbody class="sf-toolbar-ajax-request-list"></tbody>
</table>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }}
{% endblock %}

View File

@ -0,0 +1,153 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.totals.calls > 0 %}
{% set icon %}
{{ include('@WebProfiler/Icon/cache.svg') }}
<span class="sf-toolbar-value">{{ collector.totals.calls }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">in</span>
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.totals.time * 1000) }}</span>
<span class="sf-toolbar-label">ms</span>
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Cache Calls</b>
<span>{{ collector.totals.calls }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache hits</b>
<span>{{ collector.totals.hits }} / {{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache writes</b>
<span>{{ collector.totals.writes }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.totals.calls == 0 ? 'disabled' }}">
<span class="icon">
{{ include('@WebProfiler/Icon/cache.svg') }}
</span>
<strong>Cache</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Cache</h2>
{% if collector.totals.calls == 0 %}
<div class="empty">
<p>No cache calls were made.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.totals.calls }}</span>
<span class="label">Total calls</span>
</div>
<div class="metric">
<span class="value">{{ '%0.2f'|format(collector.totals.time * 1000) }} <span class="unit">ms</span></span>
<span class="label">Total time</span>
</div>
<div class="metric-divider"></div>
<div class="metric">
<span class="value">{{ collector.totals.reads }}</span>
<span class="label">Total reads</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.writes }}</span>
<span class="label">Total writes</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.deletes }}</span>
<span class="label">Total deletes</span>
</div>
<div class="metric-divider"></div>
<div class="metric">
<span class="value">{{ collector.totals.hits }}</span>
<span class="label">Total hits</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.misses }}</span>
<span class="label">Total misses</span>
</div>
<div class="metric">
<span class="value">
{{ collector.totals.hit_read_ratio ?? 0 }} <span class="unit">%</span>
</span>
<span class="label">Hits/reads</span>
</div>
</div>
<h2>Pools</h2>
<div class="sf-tabs">
{% for name, calls in collector.calls %}
<div class="tab {{ calls|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ collector.statistics[name].calls }}</span></h3>
<div class="tab-content">
{% if calls|length == 0 %}
<div class="empty">
<p>No calls were made for {{ name }} pool.</p>
</div>
{% else %}
<h4>Metrics</h4>
<div class="metrics">
{% for key, value in collector.statistics[name] %}
<div class="metric">
<span class="value">
{% if key == 'time' %}
{{ '%0.2f'|format(1000 * value) }} <span class="unit">ms</span>
{% elseif key == 'hit_read_ratio' %}
{{ value ?? 0 }} <span class="unit">%</span>
{% else %}
{{ value }}
{% endif %}
</span>
<span class="label">{{ key == 'hit_read_ratio' ? 'Hits/reads' : key|capitalize }}</span>
</div>
{% if key == 'time' or key == 'deletes' %}
<div class="metric-divider"></div>
{% endif %}
{% endfor %}
</div>
<h4>Calls</h4>
<table>
<thead>
<tr>
<th>#</th>
<th>Time</th>
<th>Call</th>
<th>Hit</th>
</tr>
</thead>
<tbody>
{% for call in calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms</td>
<td class="nowrap">{{ call.name }}()</td>
<td>{{ profiler_dump(call.value.result, maxDepth=2) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,226 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if 'unknown' == collector.symfonyState %}
{% set block_status = '' %}
{% set symfony_version_status = 'Unable to retrieve information about the Symfony version.' %}
{% elseif 'eol' == collector.symfonyState %}
{% set block_status = 'red' %}
{% set symfony_version_status = 'This Symfony version will no longer receive security fixes.' %}
{% elseif 'eom' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version will only receive security fixes.' %}
{% elseif 'dev' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version is still in the development phase.' %}
{% else %}
{% set block_status = '' %}
{% set symfony_version_status = '' %}
{% endif %}
{% set icon %}
<span class="sf-toolbar-label">
{{ include('@WebProfiler/Icon/symfony.svg') }}
</span>
<span class="sf-toolbar-value">{{ collector.symfonyState is defined ? collector.symfonyversion : 'n/a' }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Profiler token</b>
<span>
{% if profiler_url %}
<a href="{{ profiler_url }}">{{ collector.token }}</a>
{% else %}
{{ collector.token }}
{% endif %}
</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="sf-toolbar-info-piece">
<b>Environment</b>
<span>{{ collector.env }}</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="sf-toolbar-info-piece">
<b>Debug</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.debug ? 'green' : 'red' }}">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
</div>
{% endif %}
</div>
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece sf-toolbar-info-php">
<b>PHP version</b>
<span{% if collector.phpversionextra %} title="{{ collector.phpversion ~ collector.phpversionextra }}"{% endif %}>
{{ collector.phpversion }}
&nbsp; <a href="{{ path('_profiler_phpinfo') }}">View phpinfo()</a>
</span>
</div>
<div class="sf-toolbar-info-piece sf-toolbar-info-php-ext">
<b>PHP Extensions</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasxdebug ? 'green' : 'gray' }}">xdebug {{ collector.hasxdebug ? '✓' : '✗' }}</span>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasapcu ? 'green' : 'gray' }}">APCu {{ collector.hasapcu ? '✓' : '✗' }}</span>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.haszendopcache ? 'green' : 'red' }}">OPcache {{ collector.haszendopcache ? '✓' : '✗' }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP SAPI</b>
<span>{{ collector.sapiName }}</span>
</div>
</div>
<div class="sf-toolbar-info-group">
{% if collector.symfonyversion is defined %}
<div class="sf-toolbar-info-piece">
<b>Resources</b>
<span>
<a href="https://symfony.com/doc/{{ collector.symfonyversion }}/index.html" rel="help">
Read Symfony {{ collector.symfonyversion }} Docs
</a>
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Help</b>
<span>
<a href="https://symfony.com/support">
Symfony Support Channels
</a>
</span>
</div>
{% endif %}
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true, name: 'config', status: block_status, additional_classes: 'sf-toolbar-block-right', block_attrs: 'title="' ~ symfony_version_status ~ '"' }) }}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.symfonyState == 'eol' ? 'red' : collector.symfonyState in ['eom', 'dev'] ? 'yellow' }}">
<span class="icon">{{ include('@WebProfiler/Icon/config.svg') }}</span>
<strong>Configuration</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Symfony Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.symfonyversion }}</span>
<span class="label">Symfony version</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="metric">
<span class="value">{{ collector.env }}</span>
<span class="label">Environment</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="metric">
<span class="value">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
<span class="label">Debug</span>
</div>
{% endif %}
</div>
{% set symfony_status = { dev: 'Unstable Version', stable: 'Stable Version', eom: 'Maintenance Ended', eol: 'Version Expired' } %}
{% set symfony_status_class = { dev: 'warning', stable: 'success', eom: 'warning', eol: 'error' } %}
<table>
<thead class="small">
<tr>
<th>Symfony Status</th>
<th>Bugs {{ collector.symfonystate in ['eom', 'eol'] ? 'were' : 'are' }} fixed until</th>
<th>Security issues {{ collector.symfonystate == 'eol' ? 'were' : 'are' }} fixed until</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-normal">
<span class="label status-{{ symfony_status_class[collector.symfonystate] }}">{{ symfony_status[collector.symfonystate]|upper }}</span>
{% if collector.symfonylts %}
&nbsp; <span class="label status-success">Long-Term Support</span>
{% endif %}
</td>
<td class="font-normal">{{ collector.symfonyeom }}</td>
<td class="font-normal">{{ collector.symfonyeol }}</td>
<td class="font-normal">
<a href="https://symfony.com/releases/{{ collector.symfonyminorversion }}#release-checker">View roadmap</a>
</td>
</tr>
</tbody>
</table>
<h2>PHP Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.phpversion }}{% if collector.phpversionextra %} <span class="unit">{{ collector.phpversionextra }}</span>{% endif %}</span>
<span class="label">PHP version</span>
</div>
<div class="metric">
<span class="value">{{ collector.phparchitecture }} <span class="unit">bits</span></span>
<span class="label">Architecture</span>
</div>
<div class="metric">
<span class="value">{{ collector.phpintllocale }}</span>
<span class="label">Intl locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.phptimezone }}</span>
<span class="label">Timezone</span>
</div>
</div>
<div class="metrics">
<div class="metric">
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.haszendopcache ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">OPcache</span>
</div>
<div class="metric">
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.hasapcu ? 'yes' : 'no-gray') ~ '.svg') }}</span>
<span class="label">APCu</span>
</div>
<div class="metric">
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.hasxdebug ? 'yes' : 'no-gray') ~ '.svg') }}</span>
<span class="label">Xdebug</span>
</div>
</div>
<p>
<a href="{{ path('_profiler_phpinfo') }}">View full PHP configuration</a>
</p>
{% if collector.bundles %}
<h2>Enabled Bundles <small>({{ collector.bundles|length }})</small></h2>
<table>
<thead>
<tr>
<th class="key">Name</th>
<th>Class</th>
</tr>
</thead>
<tbody>
{% for name in collector.bundles|keys|sort %}
<tr>
<th scope="row" class="font-normal">{{ name }}</th>
<td class="font-normal">{{ profiler_dump(collector.bundles[name]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,119 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block menu %}
<span class="label">
<span class="icon">{{ include('@WebProfiler/Icon/event.svg') }}</span>
<strong>Events</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Event Dispatcher</h2>
{% if collector.calledlisteners is empty %}
<div class="empty">
<p>No events have been recorded. Check that debugging is enabled in the kernel.</p>
</div>
{% else %}
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Called Listeners <span class="badge">{{ collector.calledlisteners|length }}</span></h3>
<div class="tab-content">
{{ helper.render_table(collector.calledlisteners) }}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Not Called Listeners <span class="badge">{{ collector.notcalledlisteners|length }}</span></h3>
<div class="tab-content">
{% if collector.notcalledlisteners is empty %}
<div class="empty">
<p>
<strong>There are no uncalled listeners</strong>.
</p>
<p>
All listeners were called for this request or an error occurred
when trying to collect uncalled listeners (in which case check the
logs to get more information).
</p>
</div>
{% else %}
{{ helper.render_table(collector.notcalledlisteners) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Orphaned Events <span class="badge">{{ collector.orphanedEvents|length }}</span></h3>
<div class="tab-content">
{% if collector.orphanedEvents is empty %}
<div class="empty">
<p>
<strong>There are no orphaned events</strong>.
</p>
<p>
All dispatched events were handled or an error occurred
when trying to collect orphaned events (in which case check the
logs to get more information).
</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Event</th>
</tr>
</thead>
<tbody>
{% for event in collector.orphanedEvents %}
<tr>
<td class="font-normal">{{ event }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% macro render_table(listeners) %}
<table>
<thead>
<tr>
<th class="text-right">Priority</th>
<th>Listener</th>
</tr>
</thead>
{% set previous_event = (listeners|first).event %}
{% for listener in listeners %}
{% if loop.first or listener.event != previous_event %}
{% if not loop.first %}
</tbody>
{% endif %}
<tbody>
<tr>
<th colspan="2" class="colored font-normal">{{ listener.event }}</th>
</tr>
{% set previous_event = listener.event %}
{% endif %}
<tr>
<td class="text-right nowrap">{{ listener.priority|default('-') }}</td>
<td class="font-normal">{{ profiler_dump(listener.stub) }}</td>
</tr>
{% if loop.last %}
</tbody>
{% endif %}
{% endfor %}
</table>
{% endmacro %}

View File

@ -0,0 +1,32 @@
.container {
max-width: none;
margin: 0;
padding: 0;
}
.container .container {
padding: 0;
}
.exception-summary {
background: var(--base-0);
border: var(--border);
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
margin: 1em 0;
padding: 10px;
}
.exception-summary.exception-without-message {
display: none;
}
.exception-message {
color: var(--color-error);
}
.exception-metadata,
.exception-illustration {
display: none;
}
.exception-message-wrapper .container {
min-height: unset;
}

View File

@ -0,0 +1,37 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{% if collector.hasexception %}
<style>
{{ render(controller('web_profiler.controller.exception_panel::stylesheet', { token: token })) }}
{{ include('@WebProfiler/Collector/exception.css.twig') }}
</style>
{% endif %}
{{ parent() }}
{% endblock %}
{% block menu %}
<span class="label {{ collector.hasexception ? 'label-status-error' : 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/exception.svg') }}</span>
<strong>Exception</strong>
{% if collector.hasexception %}
<span class="count">
<span>1</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Exceptions</h2>
{% if not collector.hasexception %}
<div class="empty">
<p>No exception was thrown and caught during the request.</p>
</div>
{% else %}
<div class="sf-reset">
{{ render(controller('web_profiler.controller.exception_panel::body', { token: token })) }}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,730 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% from _self import form_tree_entry, form_tree_details %}
{% block toolbar %}
{% if collector.data.nb_errors > 0 or collector.data.forms|length %}
{% set status_color = collector.data.nb_errors ? 'red' %}
{% set icon %}
{{ include('@WebProfiler/Icon/form.svg') }}
<span class="sf-toolbar-value">
{{ collector.data.nb_errors ?: collector.data.forms|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Number of forms</b>
<span class="sf-toolbar-status">{{ collector.data.forms|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.data.nb_errors > 0 ? 'red' }}">{{ collector.data.nb_errors }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.data.nb_errors ? 'error' }} {{ collector.data.forms is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/form.svg') }}</span>
<strong>Forms</strong>
{% if collector.data.nb_errors > 0 %}
<span class="count">
<span>{{ collector.data.nb_errors }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block head %}
{{ parent() }}
<style>
#tree-menu {
float: left;
padding-right: 10px;
width: 230px;
}
#tree-menu ul {
list-style: none;
margin: 0;
padding-left: 0;
}
#tree-menu li {
margin: 0;
padding: 0;
width: 100%;
}
#tree-menu .empty {
border: 0;
padding: 0;
}
#tree-details-container {
border-left: 1px solid #DDD;
margin-left: 250px;
padding-left: 20px;
}
.tree-details {
padding-bottom: 40px;
}
.tree-details h3 {
font-size: 18px;
position: relative;
}
.toggle-icon {
display: inline-block;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgBAMAAADpp+X/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QweDgwx4LcKwAAAABVQTFRFAAAA////////////////ZmZm////bvjBwAAAAAV0Uk5TABZwsuCVEUjgAAAAAWJLR0QF+G/pxwAAAE1JREFUGNNjSHMSYGBgUEljSGYAAzMGBwiDhUEBwmBiEIAwGBmwgTQgQGWgA7h2uIFwK+CWwp1BpHvYEqDuATEYkBlY3IOmBq6dCPcAAIT5Eg2IksjQAAAAAElFTkSuQmCC") no-repeat top left #5eb5e0;
}
.closed .toggle-icon, .closed.toggle-icon {
background-position: bottom left;
}
.toggle-icon.empty {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAZgBmAGYHukptAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QweDhIf6CA40AAAAFRJREFUOMvtk7ENACEMA61vfx767MROWfO+AdGBHlNyTZrYUZRYDBII4NWE1pNdpFarfgLUbpDaBEgBYRiEVjsvDLa1l6O4Z3wkFWN+OfLKdpisOH/TlICzukmUJwAAAABJRU5ErkJggg==");
}
.tree .tree-inner {
cursor: pointer;
padding: 5px 7px 5px 22px;
position: relative;
}
.tree .toggle-button {
/* provide a bigger clickable area than just 10x10px */
width: 16px;
height: 16px;
margin-left: -18px;
}
.tree .toggle-icon {
width: 10px;
height: 10px;
/* position the icon in the center of the clickable area */
margin-left: 3px;
margin-top: 3px;
background-size: 10px 20px;
background-color: #AAA;
}
.tree .toggle-icon.empty {
width: 10px;
height: 10px;
position: absolute;
top: 50%;
margin-top: -5px;
margin-left: -15px;
background-size: 10px 10px;
}
.tree ul ul .tree-inner {
padding-left: 37px;
}
.tree ul ul ul .tree-inner {
padding-left: 52px;
}
.tree ul ul ul ul .tree-inner {
padding-left: 67px;
}
.tree ul ul ul ul ul .tree-inner {
padding-left: 82px;
}
.tree .tree-inner:hover {
background: #dfdfdf;
}
.tree .tree-inner:hover span:not(.has-error) {
color: var(--base-0);
}
.tree .tree-inner.active, .tree .tree-inner.active:hover {
background: var(--tree-active-background);
font-weight: bold;
}
.tree .tree-inner.active .toggle-icon, .tree .tree-inner:hover .toggle-icon, .tree .tree-inner.active:hover .toggle-icon {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgBAMAAADpp+X/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QweDhEYXWn+sAAAABhQTFRFAAAA39/f39/f39/f39/fZmZm39/f////gc3YPwAAAAV0Uk5TAAtAc6ZeVyCYAAAAAWJLR0QF+G/pxwAAAE1JREFUGNNjSHMSYGBgUEljSGYAAzMGBwiDhUEBwmBiEIAwGBmwgXIgQGWgA7h2uIFwK+CWwp1BpHvYC6DuATEYkBlY3IOmBq6dCPcAADqLE4MnBi/fAAAAAElFTkSuQmCC");
background-color: #999;
}
.tree .tree-inner.active .toggle-icon.empty, .tree .tree-inner:hover .toggle-icon.empty, .tree .tree-inner.active:hover .toggle-icon.empty {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QweDhoucSey4gAAABVQTFRFAAAA39/f39/f39/f39/fZmZm39/fD5Dx2AAAAAV0Uk5TAAtAc6ZeVyCYAAAAAWJLR0QF+G/pxwAAADJJREFUCNdjSHMSYGBgUEljSGYAAzMGBwiDhUEBwmBiEIAwGBnIA3DtcAPhVsAthTkDAFOfBKW9C1iqAAAAAElFTkSuQmCC");
}
.tree-details .toggle-icon {
width: 16px;
height: 16px;
/* vertically center the button */
position: absolute;
top: 50%;
margin-top: -9px;
margin-left: 6px;
}
.badge-error {
float: right;
background: var(--background-error);
color: #FFF;
padding: 1px 4px;
font-size: 10px;
font-weight: bold;
vertical-align: middle;
}
.has-error {
color: var(--color-error);
}
.errors h3 {
color: var(--color-error);
}
.errors th {
background: var(--background-error);
color: #FFF;
}
.errors .toggle-icon {
background-color: var(--background-error);
}
h3 a, h3 a:hover, h3 a:focus {
color: inherit;
text-decoration: inherit;
}
h2 + h3.form-data-type {
margin-top: 0;
}
h3.form-data-type + h3 {
margin-top: 1em;
}
.theme-dark .toggle-icon {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgBAMAAADpp+X/AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAVUExURUdwTH+Ag0lNUZiYmGRmbP///zU5P2n9VV4AAAAFdFJOUwCv+yror0g1sQAAAE1JREFUGNNjSFM0YGBgEEpjSGEAAzcGBQiDiUEAwmBkMIAwmBmwgVAgQGWgA7h2uIFwK+CWwp1BpHtYA6DuATEYkBlY3IOmBq6dCPcAAKMtEEs3tfChAAAAAElFTkSuQmCC');
}
.theme-dark .toggle-icon.empty {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAASUExURUdwTDI3OzQ5PS4uLjU3PzU5P4keoyIAAAAFdFJOUwBApgtzrnKGEwAAADJJREFUCNdjCFU0YGBgEAplCGEAA1cGBQiDiUEAwmBkMIAwmBnIA3DtcAPhVsAthTkDACsZBBmrTTSxAAAAAElFTkSuQmCC');
}
.theme-dark .tree .tree-inner.active .toggle-icon, .theme-dark .tree .tree-inner:hover .toggle-icon, .theme-dark .tree .tree-inner.active:hover .toggle-icon {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgBAMAAADpp+X/AAAAD1BMVEVHcEx/gIOYmJiZmZn///+IJ2wIAAAAA3RSTlMAryoIUq0uAAAAUElEQVQY02NgYFQ2NjYWYGBgMAYDBgZmCMOAQRjCMGRQhjCMoEqAipAYLkCAykBXA9cONxBuBdxShDOIc4+JM9Q9IIYxMgOLe9DUwLUT4R4AznguG0qfEa0AAAAASUVORK5CYII=');
background-color: transparent;
}
.theme-dark .tree .tree-inner.active .toggle-icon.empty, .theme-dark .tree .tree-inner:hover .toggle-icon.empty, .theme-dark .tree .tree-inner.active:hover .toggle-icon.empty {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVHcEwyNzuqqqrd9nIgAAAAAnRSTlMAQABPjKgAAAArSURBVAjXY2BctcqBgWvVqgUMWqtWrWDIWrVqJcMqICCGACsGawMbADIKANflJYEoGMqtAAAAAElFTkSuQmCC');
background-color: transparent;
}
</style>
{% endblock %}
{% block panel %}
<h2>Forms</h2>
{% if collector.data.forms|length %}
<div id="tree-menu" class="tree">
<ul>
{% for formName, formData in collector.data.forms %}
{{ form_tree_entry(formName, formData, true) }}
{% endfor %}
</ul>
</div>
<div id="tree-details-container">
{% for formName, formData in collector.data.forms %}
{{ form_tree_details(formName, formData, collector.data.forms_by_hash, loop.first) }}
{% endfor %}
</div>
{% else %}
<div class="empty">
<p>No forms were submitted for this request.</p>
</div>
{% endif %}
<script>
function Toggler(storage) {
"use strict";
var STORAGE_KEY = 'sf_toggle_data',
states = {},
isCollapsed = function (button) {
return Sfjs.hasClass(button, 'closed');
},
isExpanded = function (button) {
return !isCollapsed(button);
},
expand = function (button) {
var targetId = button.dataset.toggleTargetId,
target = document.getElementById(targetId);
if (!target) {
throw "Toggle target " + targetId + " does not exist";
}
if (isCollapsed(button)) {
Sfjs.removeClass(button, 'closed');
Sfjs.removeClass(target, 'hidden');
states[targetId] = 1;
storage.setItem(STORAGE_KEY, states);
}
},
collapse = function (button) {
var targetId = button.dataset.toggleTargetId,
target = document.getElementById(targetId);
if (!target) {
throw "Toggle target " + targetId + " does not exist";
}
if (isExpanded(button)) {
Sfjs.addClass(button, 'closed');
Sfjs.addClass(target, 'hidden');
states[targetId] = 0;
storage.setItem(STORAGE_KEY, states);
}
},
toggle = function (button) {
if (Sfjs.hasClass(button, 'closed')) {
expand(button);
} else {
collapse(button);
}
},
initButtons = function (buttons) {
states = storage.getItem(STORAGE_KEY, {});
// must be an object, not an array or anything else
// `typeof` returns "object" also for arrays, so the following
// check must be done
// see http://stackoverflow.com/questions/4775722/check-if-object-is-array
if ('[object Object]' !== Object.prototype.toString.call(states)) {
states = {};
}
for (var i = 0, l = buttons.length; i < l; ++i) {
var targetId = buttons[i].dataset.toggleTargetId,
target = document.getElementById(targetId);
if (!target) {
throw "Toggle target " + targetId + " does not exist";
}
// correct the initial state of the button
if (Sfjs.hasClass(target, 'hidden')) {
Sfjs.addClass(buttons[i], 'closed');
}
// attach listener for expanding/collapsing the target
clickHandler(buttons[i], toggle);
if (states.hasOwnProperty(targetId)) {
// open or collapse based on stored data
if (0 === states[targetId]) {
collapse(buttons[i]);
} else {
expand(buttons[i]);
}
}
}
};
return {
initButtons: initButtons,
toggle: toggle,
isExpanded: isExpanded,
isCollapsed: isCollapsed,
expand: expand,
collapse: collapse
};
}
function JsonStorage(storage) {
var setItem = function (key, data) {
storage.setItem(key, JSON.stringify(data));
},
getItem = function (key, defaultValue) {
var data = storage.getItem(key);
if (null !== data) {
try {
return JSON.parse(data);
} catch(e) {
}
}
return defaultValue;
};
return {
setItem: setItem,
getItem: getItem
};
}
function TabView() {
"use strict";
var activeTab = null,
activeTarget = null,
select = function (tab) {
var targetId = tab.dataset.tabTargetId,
target = document.getElementById(targetId);
if (!target) {
throw "Tab target " + targetId + " does not exist";
}
if (activeTab) {
Sfjs.removeClass(activeTab, 'active');
}
if (activeTarget) {
Sfjs.addClass(activeTarget, 'hidden');
}
Sfjs.addClass(tab, 'active');
Sfjs.removeClass(target, 'hidden');
activeTab = tab;
activeTarget = target;
},
initTabs = function (tabs) {
for (var i = 0, l = tabs.length; i < l; ++i) {
var targetId = tabs[i].dataset.tabTargetId,
target = document.getElementById(targetId);
if (!target) {
throw "Tab target " + targetId + " does not exist";
}
clickHandler(tabs[i], select);
Sfjs.addClass(target, 'hidden');
}
if (tabs.length > 0) {
select(tabs[0]);
}
};
return {
initTabs: initTabs,
select: select
};
}
var tabTarget = new TabView(),
toggler = new Toggler(new JsonStorage(sessionStorage)),
clickHandler = function (element, callback) {
Sfjs.addEventListener(element, 'click', function (e) {
if (!e) {
e = window.event;
}
callback(this);
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
e.stopPropagation();
return false;
});
};
tabTarget.initTabs(document.querySelectorAll('.tree .tree-inner'));
toggler.initButtons(document.querySelectorAll('a.toggle-button'));
</script>
{% endblock %}
{% macro form_tree_entry(name, data, is_root) %}
{% import _self as tree %}
{% set has_error = data.errors is defined and data.errors|length > 0 %}
<li>
<div class="tree-inner" data-tab-target-id="{{ data.id }}-details">
{% if has_error %}
<div class="badge-error">{{ data.errors|length }}</div>
{% endif %}
{% if data.children is not empty %}
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-children" href="#"><span class="toggle-icon"></span></a>
{% else %}
<div class="toggle-icon empty"></div>
{% endif %}
<span {% if has_error or data.has_children_error|default(false) %}class="has-error"{% endif %}>
{{ name|default('(no name)') }}
</span>
</div>
{% if data.children is not empty %}
<ul id="{{ data.id }}-children" {% if not is_root and not data.has_children_error|default(false) %}class="hidden"{% endif %}>
{% for childName, childData in data.children %}
{{ tree.form_tree_entry(childName, childData, false) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% macro form_tree_details(name, data, forms_by_hash, show) %}
{% import _self as tree %}
<div class="tree-details{% if not show|default(false) %} hidden{% endif %}" {% if data.id is defined %}id="{{ data.id }}-details"{% endif %}>
<h2>{{ name|default('(no name)') }}</h2>
{% if data.type_class is defined %}
<h3 class="dump-inline form-data-type">{{ profiler_dump(data.type_class) }}</h3>
{% endif %}
{% if data.errors is defined and data.errors|length > 0 %}
<div class="errors">
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-errors" href="#">
Errors <span class="toggle-icon"></span>
</a>
</h3>
<table id="{{ data.id }}-errors">
<thead>
<tr>
<th>Message</th>
<th>Origin</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
{% for error in data.errors %}
<tr>
<td>{{ error.message }}</td>
<td>
{% if error.origin is empty %}
<em>This form.</em>
{% elseif forms_by_hash[error.origin] is not defined %}
<em>Unknown.</em>
{% else %}
{{ forms_by_hash[error.origin].name }}
{% endif %}
</td>
<td>
{% if error.trace %}
<span class="newline">Caused by:</span>
{% for stacked in error.trace %}
{{ profiler_dump(stacked) }}
{% endfor %}
{% else %}
<em>Unknown.</em>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if data.default_data is defined %}
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-default_data" href="#">
Default Data <span class="toggle-icon"></span>
</a>
</h3>
<div id="{{ data.id }}-default_data">
<table>
<thead>
<tr>
<th width="180">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.default_data.model is defined %}
{{ profiler_dump(data.default_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.default_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.default_data.view is defined %}
{{ profiler_dump(data.default_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% endif %}
{% if data.submitted_data is defined %}
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-submitted_data" href="#">
Submitted Data <span class="toggle-icon"></span>
</a>
</h3>
<div id="{{ data.id }}-submitted_data">
{% if data.submitted_data.norm is defined %}
<table>
<thead>
<tr>
<th width="180">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.submitted_data.view is defined %}
{{ profiler_dump(data.submitted_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.submitted_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.submitted_data.model is defined %}
{{ profiler_dump(data.submitted_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="empty">
<p>This form was not submitted.</p>
</div>
{% endif %}
</div>
{% endif %}
{% if data.passed_options is defined %}
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-passed_options" href="#">
Passed Options <span class="toggle-icon"></span>
</a>
</h3>
<div id="{{ data.id }}-passed_options">
{% if data.passed_options|length %}
<table>
<thead>
<tr>
<th width="180">Option</th>
<th>Passed Value</th>
<th>Resolved Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.passed_options %}
<tr>
<th>{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
<td>
{# values can be stubs #}
{% set option_value = value.value|default(value) %}
{% set resolved_option_value = data.resolved_options[option].value|default(data.resolved_options[option]) %}
{% if resolved_option_value == option_value %}
<em class="font-normal text-muted">same as passed value</em>
{% else %}
{{ profiler_dump(data.resolved_options.seek(option)) }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>No options were passed when constructing this form.</p>
</div>
{% endif %}
</div>
{% endif %}
{% if data.resolved_options is defined %}
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-resolved_options" href="#">
Resolved Options <span class="toggle-icon"></span>
</a>
</h3>
<div id="{{ data.id }}-resolved_options" class="hidden">
<table>
<thead>
<tr>
<th width="180">Option</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.resolved_options %}
<tr>
<th scope="row">{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if data.view_vars is defined %}
<h3>
<a class="toggle-button" data-toggle-target-id="{{ data.id }}-view_vars" href="#">
View Variables <span class="toggle-icon"></span>
</a>
</h3>
<div id="{{ data.id }}-view_vars" class="hidden">
<table>
<thead>
<tr>
<th width="180">Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for variable, value in data.view_vars %}
<tr>
<th scope="row">{{ variable }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% for childName, childData in data.children %}
{{ tree.form_tree_details(childName, childData, forms_by_hash) }}
{% endfor %}
{% endmacro %}

View File

@ -0,0 +1,131 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.requestCount %}
{% set icon %}
{{ include('@WebProfiler/Icon/http-client.svg') }}
{% set status_color = '' %}
<span class="sf-toolbar-value">{{ collector.requestCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total requests</b>
<span>{{ collector.requestCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>HTTP errors</b>
<span class="sf-toolbar-status {{ collector.errorCount > 0 ? 'sf-toolbar-status-red' }}">{{ collector.errorCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.requestCount == 0 ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/http-client.svg') }}</span>
<strong>HTTP Client</strong>
{% if collector.requestCount %}
<span class="count">
{{ collector.requestCount }}
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>HTTP Client</h2>
{% if collector.requestCount == 0 %}
<div class="empty">
<p>No HTTP requests were made.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.requestCount }}</span>
<span class="label">Total requests</span>
</div>
<div class="metric">
<span class="value">{{ collector.errorCount }}</span>
<span class="label">HTTP errors</span>
</div>
</div>
<h2>Clients</h2>
<div class="sf-tabs">
{% for name, client in collector.clients %}
<div class="tab {{ client.traces|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3>
<div class="tab-content">
{% if client.traces|length == 0 %}
<div class="empty">
<p>No requests were made with the "{{ name }}" service.</p>
</div>
{% else %}
<h4>Requests</h4>
{% for trace in client.traces %}
{% set profiler_token = '' %}
{% set profiler_link = '' %}
{% if trace.info.response_headers is defined %}
{% for header in trace.info.response_headers %}
{% if header matches '/^x-debug-token: .*$/i' %}
{% set profiler_token = (header.getValue | slice('x-debug-token: ' | length)) %}
{% endif %}
{% if header matches '/^x-debug-token-link: .*$/i' %}
{% set profiler_link = (header.getValue | slice('x-debug-token-link: ' | length)) %}
{% endif %}
{% endfor %}
{% endif %}
<table>
<thead>
<tr>
<th>
<span class="label">{{ trace.method }}</span>
</th>
<th class="full-width">
{{ trace.url }}
{% if trace.options is not empty %}
{{ profiler_dump(trace.options, maxDepth=1) }}
{% endif %}
</th>
{% if profiler_token and profiler_link %}
<th>
Profile
</th>
{% endif %}
</tr>
</thead>
<tbody>
<tr>
<th>
{% if trace.http_code >= 500 %}
{% set responseStatus = 'error' %}
{% elseif trace.http_code >= 400 %}
{% set responseStatus = 'warning' %}
{% else %}
{% set responseStatus = 'success' %}
{% endif %}
<span class="label status-{{ responseStatus }}">
{{ trace.http_code }}
</span>
</th>
<td>
{{ profiler_dump(trace.info, maxDepth=1) }}
</td>
{% if profiler_token and profiler_link %}
<td>
<span><a href="{{ profiler_link }}" target="_blank">{{ profiler_token }}</a></span>
</td>
{% endif %}
</tr>
</tbody>
</table>
{% endfor %}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,274 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block toolbar %}
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
{% set icon %}
{% set status_color = collector.counterrors ? 'red' : collector.countwarnings ? 'yellow' : 'none' %}
{{ include('@WebProfiler/Icon/logger.svg') }}
<span class="sf-toolbar-value">{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.counterrors ? 'red' }}">{{ collector.counterrors|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Warnings</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countwarnings ? 'yellow' }}">{{ collector.countwarnings|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Deprecations</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countdeprecations ? 'none' }}">{{ collector.countdeprecations|default(0) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.counterrors ? 'error' : collector.countwarnings ? 'warning' : 'none' }} {{ collector.logs is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/logger.svg') }}</span>
<strong>Logs</strong>
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
<span class="count">
<span>{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Log Messages</h2>
{% if collector.processedLogs is empty %}
<div class="empty">
<p>No log messages available.</p>
</div>
{% else %}
{% set has_error_logs = collector.processedLogs|column('type')|filter(type => 'error' == type)|length > 0 %}
{% set has_deprecation_logs = collector.processedLogs|column('type')|filter(type => 'deprecation' == type)|length > 0 %}
{% set filters = collector.filters %}
<div class="log-filters">
<div id="log-filter-type" class="log-filter">
<ul class="tab-navigation">
<li class="{{ not has_error_logs and not has_deprecation_logs ? 'active' }}">
<input {{ not has_error_logs and not has_deprecation_logs ? 'checked' }} type="radio" id="filter-log-type-all" name="filter-log-type" value="all">
<label for="filter-log-type-all">All messages</label>
</li>
<li class="{{ has_error_logs ? 'active' }}">
<input {{ has_error_logs ? 'checked' }} type="radio" id="filter-log-type-error" name="filter-log-type" value="error">
<label for="filter-log-type-error">
Errors
<span class="badge status-{{ collector.counterrors ? 'error' }}">{{ collector.counterrors|default(0) }}</span>
</label>
</li>
<li class="{{ not has_error_logs and has_deprecation_logs ? 'active' }}">
<input {{ not has_error_logs and has_deprecation_logs ? 'checked' }} type="radio" id="filter-log-type-deprecation" name="filter-log-type" value="deprecation">
<label for="filter-log-type-deprecation">
Deprecations
<span class="badge status-{{ collector.countdeprecations ? 'warning' }}">{{ collector.countdeprecations|default(0) }}</span>
</label>
</li>
</ul>
</div>
<details id="log-filter-priority" class="log-filter">
<summary>
<span class="icon">{{ include('@WebProfiler/Icon/filter.svg') }}</span>
Level (<span class="filter-active-num">{{ filters.priority|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for label, value in filters.priority %}
<div class="log-filter-option">
<input {{ 'debug' != value ? 'checked' }} type="checkbox" id="filter-log-level-{{ value }}" name="filter-log-level-{{ value }}" value="{{ value }}">
<label for="filter-log-level-{{ value }}">{{ label }}</label>
</div>
{% endfor %}
</div>
</details>
<details id="log-filter-channel" class="log-filter">
<summary>
<span class="icon">{{ include('@WebProfiler/Icon/filter.svg') }}</span>
Channel (<span class="filter-active-num">{{ filters.channel|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for value in filters.channel %}
<div class="log-filter-option">
<input {{ 'event' != value ? 'checked' }} type="checkbox" id="filter-log-channel-{{ value }}" name="filter-log-channel-{{ value }}" value="{{ value }}">
<label for="filter-log-channel-{{ value }}">{{ value|title }}</label>
</div>
{% endfor %}
</div>
</details>
</div>
<table class="logs">
<colgroup>
<col width="140px">
<col>
</colgroup>
<thead>
<th>Time</th>
<th>Message</th>
</thead>
<tbody>
{% for log in collector.processedLogs %}
{% set css_class = 'error' == log.type ? 'error'
: (log.priorityName == 'WARNING' or 'deprecation' == log.type) ? 'warning'
: 'silenced' == log.type ? 'silenced'
%}
<tr class="log-status-{{ css_class }}" data-type="{{ log.type }}" data-priority="{{ log.priority }}" data-channel="{{ log.channel }}" style="{{ 'event' == log.channel or 'DEBUG' == log.priorityName ? 'display: none' }}">
<td class="log-timestamp">
<time title="{{ log.timestamp|date('r') }}" datetime="{{ log.timestamp|date('c') }}">
{{ log.timestamp|date('H:i:s.v') }}
</time>
{% if log.type in ['error', 'deprecation', 'silenced'] or 'WARNING' == log.priorityName %}
<span class="log-type-badge badge badge-{{ css_class }}">
{% if 'error' == log.type or 'WARNING' == log.priorityName %}
{{ log.priorityName|lower }}
{% else %}
{{ log.type|lower }}
{% endif %}
</span>
{% else %}
<span class="log-type-badge badge badge-{{ css_class }}">
{{ log.priorityName|lower }}
</span>
{% endif %}
</td>
<td class="font-normal">
{{ helper.render_log_message('debug', loop.index, log) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="no-logs-message empty">
<p>There are no log messages.</p>
</div>
<script>Sfjs.initializeLogsTable();</script>
{% endif %}
{% set compilerLogTotal = 0 %}
{% for logs in collector.compilerLogs %}
{% set compilerLogTotal = compilerLogTotal + logs|length %}
{% endfor %}
<details class="container-compilation-logs">
<summary>
<h4>Container Compilation Logs <span class="text-muted">({{ compilerLogTotal }})</span></h4>
<p class="text-muted">Log messages generated during the compilation of the service container.</p>
</summary>
{% if collector.compilerLogs is empty %}
<div class="empty">
<p>There are no compiler log messages.</p>
</div>
{% else %}
<table class="container-logs">
<thead>
<tr>
<th>Messages</th>
<th class="full-width">Class</th>
</tr>
</thead>
<tbody>
{% for class, logs in collector.compilerLogs %}
<tr>
<td class="font-normal text-right">{{ logs|length }}</td>
<td class="font-normal">
{% set context_id = 'context-compiler-' ~ loop.index %}
<button type="button" class="btn btn-link sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="{{ class }}">{{ class }}</button>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="break-long-words">
{% for log in logs %}
<li>{{ profiler_dump_log(log.message) }}</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</details>
{% endblock %}
{% macro render_log_message(category, log_index, log) %}
{% set has_context = log.context is defined and log.context is not empty %}
{% set has_trace = log.context.exception.trace is defined %}
{% if not has_context %}
{{ profiler_dump_log(log.message) }}
{% else %}
{{ profiler_dump_log(log.message, log.context) }}
{% endif %}
<div class="log-metadata">
{% if log.channel %}
<span class="badge">{{ log.channel }}</span>
{% endif %}
{% if log.errorCount is defined and log.errorCount > 1 %}
<span class="log-num-occurrences">{{ log.errorCount }} times</span>
{% endif %}
{% if has_context %}
{% set context_id = 'context-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</button></span>
{% endif %}
{% if has_trace %}
{% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ trace_id }}" data-toggle-alt-content="Hide trace">Show trace</button></span>
<div id="{{ trace_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
</div>
{% endif %}
{% if has_context %}
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context, maxDepth=1) }}
</div>
{% endif %}
{% if has_trace %}
<div id="{{ trace_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
</div>
{% endif %}
</div>
{% endmacro %}

View File

@ -0,0 +1,217 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{% include('@WebProfiler/Icon/mailer.svg') %}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Queued messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => e.isQueued())|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Sent messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => not e.isQueued())|length }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
/* utility classes */
.m-t-0 { margin-top: 0 !important; }
.m-t-10 { margin-top: 10px !important; }
/* basic grid */
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
position: relative;
width: 100%;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-4 {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
/* small tabs */
.sf-tabs-sm .tab-navigation li {
font-size: 14px;
padding: .3em .5em;
}
</style>
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/mailer.svg') }}</span>
<strong>E-mails</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Emails</h2>
{% if not events.messages|length %}
<div class="empty">
<p>No emails were sent.</p>
</div>
{% endif %}
<div class="metrics">
<div class="metric">
<span class="value">{{ events.events|filter(e => e.isQueued())|length }}</span>
<span class="label">Queued</span>
</div>
<div class="metric">
<span class="value">{{ events.events|filter(e => not e.isQueued())|length }}</span>
<span class="label">Sent</span>
</div>
</div>
{% for transport in events.transports %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% for event in events.events(transport) %}
{% set message = event.message %}
<div class="tab">
<h3 class="tab-title">Email {{ event.isQueued() ? 'queued' : 'sent via ' ~ transport }}</h3>
<div class="tab-content">
<div class="card">
{% if message.headers is not defined %}
{# RawMessage instance #}
<div class="card-block">
<pre class="prewrap" style="max-height: 600px">{{ message.toString() }}</pre>
</div>
{% else %}
{# Message instance #}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
<div class="tab">
<h3 class="tab-title">Headers</h3>
<div class="tab-content">
<span class="label">Subject</span>
<h2 class="m-t-10">{{ message.headers.get('subject').bodyAsString() ?? '(empty)' }}</h2>
<div class="row">
<div class="col col-4">
<span class="label">From</span>
<pre class="prewrap">{{ (message.headers.get('from').bodyAsString() ?? '(empty)')|replace({'From:': ''}) }}</pre>
<span class="label">To</span>
<pre class="prewrap">{{ (message.headers.get('to').bodyAsString() ?? '(empty)')|replace({'To:': ''}) }}</pre>
</div>
<div class="col">
<span class="label">Headers</span>
<pre class="prewrap">{% for header in message.headers.all|filter(header => (header.name ?? '') not in ['Subject', 'From', 'To']) %}
{{- header.toString }}
{%~ endfor %}</pre>
</div>
</div>
</div>
</div>
{% if message.htmlBody is defined %}
{# Email instance #}
{% set htmlBody = message.htmlBody() %}
{% if htmlBody is not null %}
<div class="tab">
<h3 class="tab-title">HTML Preview</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
<iframe
src="data:text/html;charset=utf-8;base64,{{ collector.base64Encode(htmlBody) }}"
style="height: 80vh;width: 100%;"
>
</iframe>
</pre>
</div>
</div>
<div class="tab">
<h3 class="tab-title">HTML Content</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{%- if message.htmlCharset() %}
{{- htmlBody|convert_encoding('UTF-8', message.htmlCharset()) }}
{%- else %}
{{- htmlBody }}
{%- endif -%}
</pre>
</div>
</div>
{% endif %}
{% set textBody = message.textBody() %}
{% if textBody is not null %}
<div class="tab">
<h3 class="tab-title">Text Content</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{%- if message.textCharset() %}
{{- textBody|convert_encoding('UTF-8', message.textCharset()) }}
{%- else %}
{{- textBody }}
{%- endif -%}
</pre>
</div>
</div>
{% endif %}
{% for attachment in message.attachments %}
<div class="tab">
<h3 class="tab-title">Attachment #{{ loop.index }}</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">{{ attachment.toString() }}</pre>
</div>
</div>
{% endfor %}
{% endif %}
<div class="tab">
<h3 class="tab-title">Parts Hierarchy</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">{{ message.body().asDebugString() }}</pre>
</div>
</div>
<div class="tab">
<h3 class="tab-title">Raw</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">{{ message.toString() }}</pre>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' %}
{{ include('@WebProfiler/Icon/memory.svg') }}
<span class="sf-toolbar-value">{{ '%.1f'|format(collector.memory / 1024 / 1024) }}</span>
<span class="sf-toolbar-label">MiB</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Peak memory usage</b>
<span>{{ '%.1f'|format(collector.memory / 1024 / 1024) }} MiB</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP memory limit</b>
<span>{{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MiB'|format(collector.memoryLimit / 1024 / 1024) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, name: 'time', status: status_color }) }}
{% endblock %}

View File

@ -0,0 +1,201 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block toolbar %}
{% if collector.messages|length > 0 %}
{% set status_color = collector.exceptionsCount ? 'red' %}
{% set icon %}
{{ include('@WebProfiler/Icon/messenger.svg') }}
<span class="sf-toolbar-value">{{ collector.messages|length }}</span>
{% endset %}
{% set text %}
{% for bus in collector.buses %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<div class="sf-toolbar-info-piece">
<b>{{ bus }}</b>
<span
title="{{ exceptionsCount }} message(s) with exceptions"
class="sf-toolbar-status sf-toolbar-status-{{ exceptionsCount ? 'red' }}"
>
{{ collector.messages(bus)|length }}
</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label{{ collector.exceptionsCount ? ' label-status-error' }}{{ collector.messages is empty ? ' disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/messenger.svg') }}</span>
<strong>Messages</strong>
{% if collector.exceptionsCount > 0 %}
<span class="count">
<span>{{ collector.exceptionsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block head %}
{{ parent() }}
<style>
.message-item thead th { position: relative; cursor: pointer; user-select: none; padding-right: 35px; }
.message-item tbody tr td:first-child { width: 170px; }
.message-item .label { float: right; padding: 1px 5px; opacity: .75; margin-left: 5px; }
.message-item .toggle-button { position: absolute; right: 6px; top: 6px; opacity: .5; pointer-events: none }
.message-item .icon svg { height: 24px; width: 24px; }
.message-item .sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; }
.message-item .sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; }
.message-bus .badge.status-some-errors { line-height: 16px; border-bottom: 2px solid #B0413E; }
.message-item tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; }
td.message-bus-dispatch-caller { background: #f1f2f3; }
.theme-dark td.message-bus-dispatch-caller { background: var(--base-1); }
</style>
{% endblock %}
{% block panel %}
{% import _self as helper %}
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty">
<p>No messages have been collected.</p>
</div>
{% else %}
<div class="sf-tabs message-bus">
<div class="tab">
{% set messages = collector.messages %}
{% set exceptionsCount = collector.exceptionsCount %}
<h3 class="tab-title">All<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of dispatched messages across all your buses</p>
{{ helper.render_bus_messages(messages, true) }}
</div>
</div>
{% for bus in collector.buses %}
<div class="tab message-bus">
{% set messages = collector.messages(bus) %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<h3 class="tab-title">{{ bus }}<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of messages dispatched on the <code>{{ bus }}</code> bus</p>
{{ helper.render_bus_messages(messages) }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% macro render_bus_messages(messages, showBus = false) %}
{% set discr = random() %}
{% for dispatchCall in messages %}
<table class="message-item">
<thead>
<tr>
<th colspan="2" class="sf-toggle"
data-toggle-selector="#message-item-{{ discr }}-{{ loop.index0 }}-details"
data-toggle-initial="{{ loop.first ? 'display' }}"
>
<span class="dump-inline">{{ profiler_dump(dispatchCall.message.type) }}</span>
{% if showBus %}
<span class="label">{{ dispatchCall.bus }}</span>
{% endif %}
{% if dispatchCall.exception is defined %}
<span class="label status-error">exception</span>
{% endif %}
<a class="toggle-button">
<span class="icon icon-close">{{ include('@WebProfiler/images/icon-minus-square.svg') }}</span>
<span class="icon icon-open">{{ include('@WebProfiler/images/icon-plus-square.svg') }}</span>
</a>
</th>
</tr>
</thead>
<tbody id="message-item-{{ discr }}-{{ loop.index0 }}-details" class="sf-toggle-content">
<tr>
<td colspan="2" class="message-bus-dispatch-caller">
<span class="metadata">In
{% set caller = dispatchCall.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <a class="text-small sf-toggle" data-toggle-selector="#sf-trace-{{ discr }}-{{ loop.index0 }}">{{ caller.line }}</a>
</span>
<div class="hidden" id="sf-trace-{{ discr }}-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
</td>
</tr>
{% if showBus %}
<tr>
<td class="text-bold">Bus</td>
<td>{{ dispatchCall.bus }}</td>
</tr>
{% endif %}
<tr>
<td class="text-bold">Message</td>
<td>{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}</td>
</tr>
<tr>
<td class="text-bold">Envelope stamps <span class="text-muted">when dispatching</span></td>
<td>
{% for item in dispatchCall.stamps %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted">No items</span>
{% endfor %}
</td>
</tr>
{% if dispatchCall.stamps_after_dispatch is defined %}
<tr>
<td class="text-bold">Envelope stamps <span class="text-muted">after dispatch</span></td>
<td>
{% for item in dispatchCall.stamps_after_dispatch %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted">No items</span>
{% endfor %}
</td>
</tr>
{% endif %}
{% if dispatchCall.exception is defined %}
<tr>
<td class="text-bold">Exception</td>
<td>
{{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }}
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
{% endmacro %}

View File

@ -0,0 +1,168 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{% include('@WebProfiler/Icon/notifier.svg') %}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Sent notifications</b>
<span class="sf-toolbar-status">{{ events.messages|length }}</span>
</div>
{% for transport in events.transports %}
<div class="sf-toolbar-info-piece">
<b>{{ transport }}</b>
<span class="sf-toolbar-status">{{ events.messages(transport)|length }}</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block head %}
{{ parent() }}
<style type="text/css">
/* utility classes */
.m-t-0 { margin-top: 0 !important; }
.m-t-10 { margin-top: 10px !important; }
/* basic grid */
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
position: relative;
width: 100%;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-4 {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
/* small tabs */
.sf-tabs-sm .tab-navigation li {
font-size: 14px;
padding: .3em .5em;
}
</style>
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages|length ? '' : 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/notifier.svg') }}</span>
<strong>Notifications</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Notifications</h2>
{% if not events.messages|length %}
<div class="empty">
<p>No notifications were sent.</p>
</div>
{% endif %}
<div class="metrics">
{% for transport in events.transports %}
<div class="metric">
<span class="value">{{ events.messages(transport)|length }}</span>
<span class="label">{{ transport }}</span>
</div>
{% endfor %}
</div>
{% for transport in events.transports %}
<h3>{{ transport }}</h3>
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% for event in events.events(transport) %}
{% set message = event.message %}
<div class="tab">
<h3 class="tab-title">Message #{{ loop.index }} <small>({{ event.isQueued() ? 'queued' : 'sent' }})</small></h3>
<div class="tab-content">
<div class="card">
<div class="card-block">
<span class="label">Subject</span>
<h2 class="m-t-10">{{ message.getSubject() ?? '(empty)' }}</h2>
</div>
{% if message.getNotification is defined %}
<div class="card-block">
<div class="row">
<div class="col">
<span class="label">Content</span>
<pre class="prewrap">{{ message.getNotification().getContent() ?? '(empty)' }}</pre>
<span class="label">Importance</span>
<pre class="prewrap">{{ message.getNotification().getImportance() }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% if message.getNotification is defined %}
<div class="tab">
<h3 class="tab-title">Notification</h3>
{% set notification = event.message.getNotification() %}
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{{- 'Subject: ' ~ notification.getSubject() }}<br/>
{{- 'Content: ' ~ notification.getContent() }}<br/>
{{- 'Importance: ' ~ notification.getImportance() }}<br/>
{{- 'Emoji: ' ~ (notification.getEmoji() is empty ? '(empty)' : notification.getEmoji()) }}<br/>
{{- 'Exception: ' ~ notification.getException() ?? '(empty)' }}<br/>
{{- 'ExceptionAsString: ' ~ (notification.getExceptionAsString() is empty ? '(empty)' : notification.getExceptionAsString()) }}
</pre>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Message Options</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{%- if message.getOptions() is null %}
{{- '(empty)' }}
{%- else %}
{{- message.getOptions()|json_encode(constant('JSON_PRETTY_PRINT')) }}
{%- endif %}
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,392 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% import _self as helper %}
{% set request_handler %}
{{ helper.set_handler(collector.controller) }}
{% endset %}
{% if collector.redirect %}
{% set redirect_handler %}
{{ helper.set_handler(collector.redirect.controller, collector.redirect.route, 'GET' != collector.redirect.method ? collector.redirect.method) }}
{% endset %}
{% endif %}
{% if collector.forwardtoken %}
{% set forward_profile = profile.childByToken(collector.forwardtoken) %}
{% set forward_handler %}
{{ helper.set_handler(forward_profile ? forward_profile.collector('request').controller : 'n/a') }}
{% endset %}
{% endif %}
{% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %}
{% set icon %}
<span class="sf-toolbar-status sf-toolbar-status-{{ request_status_code_color }}">{{ collector.statuscode }}</span>
{% if collector.route %}
{% if collector.redirect %}{{ include('@WebProfiler/Icon/redirect.svg') }}{% endif %}
{% if collector.forwardtoken %}{{ include('@WebProfiler/Icon/forward.svg') }}{% endif %}
<span class="sf-toolbar-label">{{ 'GET' != collector.method ? collector.method }} @</span>
<span class="sf-toolbar-value sf-toolbar-info-piece-additional">{{ collector.route }}</span>
{% endif %}
{% endset %}
{% set text %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>HTTP status</b>
<span>{{ collector.statuscode }} {{ collector.statustext }}</span>
</div>
{% if 'GET' != collector.method -%}
<div class="sf-toolbar-info-piece">
<b>Method</b>
<span>{{ collector.method }}</span>
</div>
{%- endif %}
<div class="sf-toolbar-info-piece">
<b>Controller</b>
<span>{{ request_handler }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Route name</b>
<span>{{ collector.route|default('n/a') }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Has session</b>
<span>{% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Stateless Check</b>
<span>{% if collector.statelesscheck %}yes{% else %}no{% endif %}</span>
</div>
</div>
{% if redirect_handler is defined -%}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>
<span class="sf-toolbar-redirection-status sf-toolbar-status-yellow">{{ collector.redirect.status_code }}</span>
Redirect from
</b>
<span>
{{ redirect_handler }}
(<a href="{{ path('_profiler', { token: collector.redirect.token }) }}">{{ collector.redirect.token }}</a>)
</span>
</div>
</div>
{% endif %}
{% if forward_handler is defined %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Forwarded to</b>
<span>
{{ forward_handler }}
(<a href="{{ path('_profiler', { token: collector.forwardtoken }) }}">{{ collector.forwardtoken }}</a>)
</span>
</div>
</div>
{% endif %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ include('@WebProfiler/Icon/request.svg') }}</span>
<strong>Request / Response</strong>
</span>
{% endblock %}
{% block panel %}
{% import _self as helper %}
<h2>
{{ helper.set_handler(collector.controller) }}
</h2>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Request</h3>
<div class="tab-content">
<h3>GET Parameters</h3>
{% if collector.requestquery.all is empty %}
<div class="empty">
<p>No GET parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestquery, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h3>POST Parameters</h3>
{% if collector.requestrequest.all is empty %}
<div class="empty">
<p>No POST parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestrequest, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h4>Uploaded Files</h4>
{% if collector.requestfiles is empty %}
<div class="empty">
<p>No files were uploaded</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestfiles, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h3>Request Attributes</h3>
{% if collector.requestattributes.all is empty %}
<div class="empty">
<p>No attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestattributes }, with_context = false) }}
{% endif %}
<h3>Request Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
<h3>Request Content</h3>
{% if collector.content == false %}
<div class="empty">
<p>Request content not available (it was retrieved as a resource).</p>
</div>
{% elseif collector.content %}
<div class="sf-tabs">
{% set prettyJson = collector.isJsonRequest ? collector.prettyJson : null %}
{% if prettyJson is not null %}
<div class="tab">
<h3 class="tab-title">Pretty</h3>
<div class="tab-content">
<div class="card" style="max-height: 500px; overflow-y: auto;">
<pre class="break-long-words">{{ prettyJson }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Raw</h3>
<div class="tab-content">
<div class="card">
<pre class="break-long-words">{{ collector.content }}</pre>
</div>
</div>
</div>
</div>
{% else %}
<div class="empty">
<p>No content</p>
</div>
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Response</h3>
<div class="tab-content">
<h3>Response Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responseheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
</div>
</div>
<div class="tab {{ collector.requestcookies.all is empty and collector.responsecookies.all is empty ? 'disabled' }}">
<h3 class="tab-title">Cookies</h3>
<div class="tab-content">
<h3>Request Cookies</h3>
{% if collector.requestcookies.all is empty %}
<div class="empty">
<p>No request cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestcookies }, with_context = false) }}
{% endif %}
<h3>Response Cookies</h3>
{% if collector.responsecookies.all is empty %}
<div class="empty">
<p>No response cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responsecookies }, with_context = true) }}
{% endif %}
</div>
</div>
<div class="tab {{ collector.sessionmetadata is empty ? 'disabled' }}">
<h3 class="tab-title">Session{% if collector.sessionusages is not empty %} <span class="badge">{{ collector.sessionusages|length }}</span>{% endif %}</h3>
<div class="tab-content">
<h3>Session Metadata</h3>
{% if collector.sessionmetadata is empty %}
<div class="empty">
<p>No session metadata</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionmetadata }, with_context = false) }}
{% endif %}
<h3>Session Attributes</h3>
{% if collector.sessionattributes is empty %}
<div class="empty">
<p>No session attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionattributes, labels: ['Attribute', 'Value'] }, with_context = false) }}
{% endif %}
<h3>Session Usage</h3>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.sessionusages|length }}</span>
<span class="label">Usages</span>
</div>
<div class="metric">
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.statelesscheck ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Stateless check enabled</span>
</div>
</div>
{% if collector.sessionusages is empty %}
<div class="empty">
<p>Session not used.</p>
</div>
{% else %}
<table class="session_usages">
<thead>
<tr>
<th class="full-width">Usage</th>
</tr>
</thead>
<tbody>
{% for key, usage in collector.sessionusages %}
<tr>
<td class="font-normal">
{%- set link = usage.file|file_link(usage.line) %}
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
{{ usage.name }}
{%- if link %}</a>{% else %}</span>{% endif %}
<div class="text-small font-normal">
{% set usage_id = 'session-usage-trace-' ~ key %}
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
</div>
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(usage.trace, maxDepth=2) }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="tab {{ collector.flashes is empty ? 'disabled' }}">
<h3 class="tab-title">Flashes</h3>
<div class="tab-content">
<h3>Flashes</h3>
{% if collector.flashes is empty %}
<div class="empty">
<p>No flash messages were created.</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.flashes }, with_context = false) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Server Parameters</h3>
<div class="tab-content">
<h3>Server Parameters</h3>
<h4>Defined in .env</h4>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.dotenvvars }, with_context = false) }}
<h4>Defined as regular env variables</h4>
{% set requestserver = [] %}
{% for key, value in collector.requestserver|filter((_, key) => key not in collector.dotenvvars.keys) %}
{% set requestserver = requestserver|merge({(key): value}) %}
{% endfor %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }}
</div>
</div>
{% if profile.parent %}
<div class="tab">
<h3 class="tab-title">Parent Request</h3>
<div class="tab-content">
<h3>
<a href="{{ path('_profiler', { token: profile.parent.token }) }}">Return to parent request</a>
<small>(token = {{ profile.parent.token }})</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: profile.parent.getcollector('request').requestattributes }, with_context = false) }}
</div>
</div>
{% endif %}
{% if profile.children|length %}
<div class="tab">
<h3 class="tab-title">Sub Requests <span class="badge">{{ profile.children|length }}</span></h3>
<div class="tab-content">
{% for child in profile.children %}
<h3>
{{ helper.set_handler(child.getcollector('request').controller) }}
<small>(token = <a href="{{ path('_profiler', { token: child.token }) }}">{{ child.token }}</a>)</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: child.getcollector('request').requestattributes }, with_context = false) }}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% macro set_handler(controller, route, method) %}
{% if controller.class is defined -%}
{%- if method|default(false) %}<span class="sf-toolbar-status sf-toolbar-redirection-method">{{ method }}</span>{% endif -%}
{%- set link = controller.file|file_link(controller.line) %}
{%- if link %}<a href="{{ link }}" title="{{ controller.class }}">{% else %}<span title="{{ controller.class }}">{% endif %}
{%- if route|default(false) -%}
@{{ route }}
{%- else -%}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{%- endif -%}
{%- if link %}</a>{% else %}</span>{% endif %}
{%- else -%}
<span>{{ route|default(controller) }}</span>
{%- endif %}
{% endmacro %}

View File

@ -0,0 +1,14 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ include('@WebProfiler/Icon/router.svg') }}</span>
<strong>Routing</strong>
</span>
{% endblock %}
{% block panel %}
{{ render(controller('web_profiler.controller.router::panelAction', { token: token })) }}
{% endblock %}

View File

@ -0,0 +1,64 @@
/* Legend */
.sf-profiler-timeline .legends .timeline-category {
border: none;
background: none;
border-left: 1em solid transparent;
line-height: 1em;
margin: 0 1em 0 0;
padding: 0 0.5em;
display: none;
opacity: 0.5;
}
.sf-profiler-timeline .legends .timeline-category.active {
opacity: 1;
}
.sf-profiler-timeline .legends .timeline-category.present {
display: inline-block;
}
.timeline-graph {
margin: 1em 0;
width: 100%;
background-color: var(--table-background);
border: 1px solid var(--table-border);
}
/* Typography */
.timeline-graph .timeline-label {
font-family: var(--font-sans-serif);
font-size: 12px;
line-height: 12px;
font-weight: normal;
fill: var(--color-text);
}
.timeline-graph .timeline-label .timeline-sublabel {
margin-left: 1em;
fill: var(--color-muted);
}
.timeline-graph .timeline-subrequest,
.timeline-graph .timeline-border {
fill: none;
stroke: var(--table-border);
stroke-width: 1px;
}
.timeline-graph .timeline-subrequest {
fill: url(#subrequest);
fill-opacity: 0.5;
}
.timeline-subrequest-pattern {
fill: var(--table-border);
}
/* Timeline periods */
.timeline-graph .timeline-period {
stroke-width: 0;
}

View File

@ -0,0 +1,214 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block toolbar %}
{% set has_time_events = collector.events|length > 0 %}
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
{% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %}
{% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' %}
{% set icon %}
{{ include('@WebProfiler/Icon/time.svg') }}
<span class="sf-toolbar-value">{{ total_time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ total_time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Initialization time</b>
<span>{{ initialization_time }} ms</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ include('@WebProfiler/Icon/time.svg') }}</span>
<strong>Performance</strong>
</span>
{% endblock %}
{% block panel %}
{% set has_time_events = collector.events|length > 0 %}
<h2>Performance metrics</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.duration) }} <span class="unit">ms</span></span>
<span class="label">Total execution time</span>
</div>
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.inittime) }} <span class="unit">ms</span></span>
<span class="label">Symfony initialization</span>
</div>
{% if profile.collectors.memory %}
<div class="metric">
<span class="value">{{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} <span class="unit">MiB</span></span>
<span class="label">Peak memory usage</span>
</div>
{% endif %}
{% if profile.children|length > 0 %}
<div class="metric-divider"></div>
<div class="metric">
<span class="value">{{ profile.children|length }}</span>
<span class="label">Sub-Request{{ profile.children|length > 1 ? 's' }}</span>
</div>
{% if has_time_events %}
{% set subrequests_time = 0 %}
{% for child in profile.children %}
{% set subrequests_time = subrequests_time + child.getcollector('time').events.__section__.duration %}
{% endfor %}
{% else %}
{% set subrequests_time = 'n/a' %}
{% endif %}
<div class="metric">
<span class="value">{{ subrequests_time }} <span class="unit">ms</span></span>
<span class="label">Sub-Request{{ profile.children|length > 1 ? 's' }} time</span>
</div>
{% endif %}
</div>
<h2>Execution timeline</h2>
{% if not collector.isStopwatchInstalled() %}
<div class="empty">
<p>The Stopwatch component is not installed. If you want to see timing events, run: <code>composer require symfony/stopwatch</code>.</p>
</div>
{% elseif collector.events is empty %}
<div class="empty">
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
</div>
{% else %}
{{ block('panelContent') }}
{% endif %}
{% endblock %}
{% block panelContent %}
<form id="timeline-control" action="" method="get">
<input type="hidden" name="panel" value="time">
<label for="threshold">Threshold</label>
<input type="number" name="threshold" id="threshold" value="1" min="0" placeholder="1.1"> ms
<span class="help">(timeline only displays events with a duration longer than this threshold)</span>
</form>
{% if profile.parent %}
<h3 class="dump-inline">
Sub-Request {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }}
<small>
{{ collector.events.__section__.duration }} ms
<a class="newline" href="{{ path('_profiler', { token: profile.parent.token, panel: 'time' }) }}">Return to parent request</a>
</small>
</h3>
{% elseif profile.children|length > 0 %}
<h3>
Main Request <small>{{ collector.events.__section__.duration }} ms</small>
</h3>
{% endif %}
{{ helper.display_timeline(token, collector.events, collector.events.__section__.origin) }}
{% if profile.children|length %}
<p class="help">Note: sections with a striped background correspond to sub-requests.</p>
<h3>Sub-requests <small>({{ profile.children|length }})</small></h3>
{% for child in profile.children %}
{% set events = child.getcollector('time').events %}
<h4>
<a href="{{ path('_profiler', { token: child.token, panel: 'time' }) }}">{{ child.getcollector('request').identifier }}</a>
<small>{{ events.__section__.duration }} ms</small>
</h4>
{{ helper.display_timeline(child.token, events, collector.events.__section__.origin) }}
{% endfor %}
{% endif %}
<svg id="timeline-template" width="0" height="0">
<defs>
<pattern id="subrequest" class="timeline-subrequest-pattern" patternUnits="userSpaceOnUse" width="20" height="20" viewBox="0 0 40 40">
<path d="M0 40L40 0H20L0 20M40 40V20L20 40"/>
</pattern>
</defs>
</svg>
<style type="text/css">
{% include '@WebProfiler/Collector/time.css.twig' %}
</style>
<script>
{% include '@WebProfiler/Collector/time.js' %}
</script>
{% endblock %}
{% macro dump_request_data(token, events, origin) %}
{% autoescape 'js' %}
{% from _self import dump_events %}
{
id: "{{ token }}",
left: {{ "%F"|format(events.__section__.origin - origin) }},
end: "{{ '%F'|format(events.__section__.endtime) }}",
events: [ {{ dump_events(events) }} ],
}
{% endautoescape %}
{% endmacro %}
{% macro dump_events(events) %}
{% autoescape 'js' %}
{% for name, event in events %}
{% if '__section__' != name %}
{
name: "{{ name }}",
category: "{{ event.category }}",
origin: {{ "%F"|format(event.origin) }},
starttime: {{ "%F"|format(event.starttime) }},
endtime: {{ "%F"|format(event.endtime) }},
duration: {{ "%F"|format(event.duration) }},
memory: {{ "%.1F"|format(event.memory / 1024 / 1024) }},
elements: {},
periods: [
{%- for period in event.periods -%}
{
start: {{ "%F"|format(period.starttime) }},
end: {{ "%F"|format(period.endtime) }},
duration: {{ "%F"|format(period.duration) }},
elements: {}
},
{%- endfor -%}
],
},
{% endif %}
{% endfor %}
{% endautoescape %}
{% endmacro %}
{% macro display_timeline(token, events, origin) %}
{% import _self as helper %}
<div class="sf-profiler-timeline">
<div id="legend-{{ token }}" class="legends"></div>
<svg id="timeline-{{ token }}" class="timeline-graph"></svg>
<script>{% autoescape 'js' %}
window.addEventListener('load', function onLoad() {
const theme = new Theme();
new TimelineEngine(
theme,
new SvgRenderer(document.getElementById('timeline-{{ token }}')),
new Legend(document.getElementById('legend-{{ token }}'), theme),
document.getElementById('threshold'),
{{ helper.dump_request_data(token, events, origin) }}
);
});
{% endautoescape %}</script>
</div>
{% endmacro %}

View File

@ -0,0 +1,457 @@
'use strict';
class TimelineEngine {
/**
* @param {Theme} theme
* @param {Renderer} renderer
* @param {Legend} legend
* @param {Element} threshold
* @param {Object} request
* @param {Number} eventHeight
* @param {Number} horizontalMargin
*/
constructor(theme, renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
this.theme = theme;
this.renderer = renderer;
this.legend = legend;
this.threshold = threshold;
this.request = request;
this.scale = renderer.width / request.end;
this.eventHeight = eventHeight;
this.horizontalMargin = horizontalMargin;
this.labelY = Math.round(this.eventHeight * 0.48);
this.periodY = Math.round(this.eventHeight * 0.66);
this.FqcnMatcher = /\\([^\\]+)$/i;
this.origin = null;
this.createEventElements = this.createEventElements.bind(this);
this.createBackground = this.createBackground.bind(this);
this.createPeriod = this.createPeriod.bind(this);
this.render = this.render.bind(this);
this.renderEvent = this.renderEvent.bind(this);
this.renderPeriod = this.renderPeriod.bind(this);
this.onResize = this.onResize.bind(this);
this.isActive = this.isActive.bind(this);
this.threshold.addEventListener('change', this.render);
this.legend.addEventListener('change', this.render);
window.addEventListener('resize', this.onResize);
this.createElements();
this.render();
}
onResize() {
this.renderer.measure();
this.setScale(this.renderer.width / this.request.end);
}
setScale(scale) {
if (scale !== this.scale) {
this.scale = scale;
this.render();
}
}
createElements() {
this.origin = this.renderer.setFullVerticalLine(this.createBorder(), 0);
this.renderer.add(this.origin);
this.request.events
.filter(event => event.category === 'section')
.map(this.createBackground)
.forEach(this.renderer.add);
this.request.events
.map(this.createEventElements)
.forEach(this.renderer.add);
}
createBackground(event) {
const subrequest = event.name === '__section__.child';
const background = this.renderer.create('rect', subrequest ? 'timeline-subrequest' : 'timeline-border');
event.elements = Object.assign(event.elements || {}, { background });
return background;
}
createEventElements(event) {
const { name, category, duration, memory, periods } = event;
const border = this.renderer.setFullHorizontalLine(this.createBorder(), 0);
const lines = periods.map(period => this.createPeriod(period, category));
const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]);
const title = this.renderer.createTitle(name);
const group = this.renderer.group([title, border, label].concat(lines), this.theme.getCategoryColor(event.category));
event.elements = Object.assign(event.elements || {}, { group, label, border });
this.legend.add(event.category)
return group;
}
createLabel(name, duration, memory, period) {
const label = this.renderer.createText(name, period.start * this.scale, this.labelY, 'timeline-label');
const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} MiB`, 'timeline-sublabel');
label.appendChild(sublabel);
return label;
}
createPeriod(period, category) {
const timeline = this.renderer.createPath(null, 'timeline-period', this.theme.getCategoryColor(category));
period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine;
period.elements = Object.assign(period.elements || {}, { timeline });
return timeline;
}
createBorder() {
return this.renderer.createPath(null, 'timeline-border');
}
isActive(event) {
const { duration, category } = event;
return duration >= this.threshold.value && this.legend.isActive(category);
}
render() {
const events = this.request.events.filter(this.isActive);
const width = this.renderer.width + this.horizontalMargin * 2;
const height = this.eventHeight * events.length;
// Set view box
this.renderer.setViewBox(-this.horizontalMargin, 0, width, height);
// Show 0ms origin
this.renderer.setFullVerticalLine(this.origin, 0);
// Render all events
this.request.events.forEach(event => this.renderEvent(event, events.indexOf(event)));
}
renderEvent(event, index) {
const { name, category, duration, memory, periods, elements } = event;
const { group, label, border, background } = elements;
const visible = index >= 0;
group.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (background) {
background.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (visible) {
const [min, max] = this.getEventLimits(event);
this.renderer.setFullRectangle(background, min * this.scale, max * this.scale);
}
}
if (visible) {
// Position the group
group.setAttribute('transform', `translate(0, ${index * this.eventHeight})`);
// Update top border
this.renderer.setFullHorizontalLine(border, 0);
// render label and ensure it doesn't escape the viewport
this.renderLabel(label, event);
// Update periods
periods.forEach(this.renderPeriod);
}
}
renderLabel(label, event) {
const width = this.getLabelWidth(label);
const [min, max] = this.getEventLimits(event);
const alignLeft = (min * this.scale) + width <= this.renderer.width;
label.setAttribute('x', (alignLeft ? min : max) * this.scale);
label.setAttribute('text-anchor', alignLeft ? 'start' : 'end');
}
renderPeriod(period) {
const { elements, start, duration } = period;
period.draw(elements.timeline, start * this.scale, this.periodY, Math.max(duration * this.scale, 1));
}
getLabelWidth(label) {
if (typeof label.width === 'undefined') {
label.width = label.getBBox().width;
}
return label.width;
}
getEventLimits(event) {
if (typeof event.limits === 'undefined') {
const { periods } = event;
event.limits = [
periods[0].start,
periods[periods.length - 1].end
];
}
return event.limits;
}
getShortName(name) {
const matches = this.FqcnMatcher.exec(name);
if (matches) {
return matches[1];
}
return name;
}
}
class Legend {
constructor(element, theme) {
this.element = element;
this.theme = theme;
this.toggle = this.toggle.bind(this);
this.createCategory = this.createCategory.bind(this);
this.categories = [];
this.theme.getDefaultCategories().forEach(this.createCategory);
}
add(category) {
this.get(category).classList.add('present');
}
createCategory(category) {
const element = document.createElement('button');
element.className = `timeline-category active`;
element.style.borderColor = this.theme.getCategoryColor(category);
element.innerText = category;
element.value = category;
element.type = 'button';
element.addEventListener('click', this.toggle);
this.element.appendChild(element);
this.categories.push(element);
return element;
}
toggle(event) {
event.target.classList.toggle('active');
this.emit('change');
}
isActive(category) {
return this.get(category).classList.contains('active');
}
get(category) {
return this.categories.find(element => element.value === category) || this.createCategory(category);
}
emit(name) {
this.element.dispatchEvent(new Event(name));
}
addEventListener(name, callback) {
this.element.addEventListener(name, callback);
}
removeEventListener(name, callback) {
this.element.removeEventListener(name, callback);
}
}
class SvgRenderer {
/**
* @param {SVGElement} element
*/
constructor(element) {
this.ns = 'http://www.w3.org/2000/svg';
this.width = null;
this.viewBox = {};
this.element = element;
this.add = this.add.bind(this);
this.setViewBox(0, 0, 0, 0);
this.measure();
}
setViewBox(x, y, width, height) {
this.viewBox = { x, y, width, height };
this.element.setAttribute('viewBox', `${x} ${y} ${width} ${height}`);
}
measure() {
this.width = this.element.getBoundingClientRect().width;
}
add(element) {
this.element.appendChild(element);
}
group(elements, className) {
const group = this.create('g', className);
elements.forEach(element => group.appendChild(element));
return group;
}
setHorizontalLine(element, x, y, width) {
element.setAttribute('d', `M${x},${y} h${width}`);
return element;
}
setVerticalLine(element, x, y, height) {
element.setAttribute('d', `M${x},${y} v${height}`);
return element;
}
setFullHorizontalLine(element, y) {
return this.setHorizontalLine(element, this.viewBox.x, y, this.viewBox.width);
}
setFullVerticalLine(element, x) {
return this.setVerticalLine(element, x, this.viewBox.y, this.viewBox.height);
}
setFullRectangle(element, min, max) {
element.setAttribute('x', min);
element.setAttribute('y', this.viewBox.y);
element.setAttribute('width', max - min);
element.setAttribute('height', this.viewBox.height);
}
setSectionLine(element, x, y, width, height = 4, markerSize = 6) {
const totalHeight = height + markerSize;
const maxMarkerWidth = Math.min(markerSize, width / 2);
const widthWithoutMarker = Math.max(0, width - (maxMarkerWidth * 2));
element.setAttribute('d', `M${x},${y + totalHeight} v${-totalHeight} h${width} v${totalHeight} l${-maxMarkerWidth} ${-markerSize} h${-widthWithoutMarker} Z`);
}
setPeriodLine(element, x, y, width, height = 4, markerWidth = 2, markerHeight = 4) {
const totalHeight = height + markerHeight;
const maxMarkerWidth = Math.min(markerWidth, width);
element.setAttribute('d', `M${x + maxMarkerWidth},${y + totalHeight} h${-maxMarkerWidth} v${-totalHeight} h${width} v${height} h${maxMarkerWidth-width}Z`);
}
createText(content, x, y, className) {
const element = this.create('text', className);
element.setAttribute('x', x);
element.setAttribute('y', y);
element.textContent = content;
return element;
}
createTspan(content, className) {
const element = this.create('tspan', className);
element.textContent = content;
return element;
}
createTitle(content) {
const element = this.create('title');
element.textContent = content;
return element;
}
createPath(path = null, className = null, color = null) {
const element = this.create('path', className);
if (path) {
element.setAttribute('d', path);
}
if (color) {
element.setAttribute('fill', color);
}
return element;
}
create(name, className = null) {
const element = document.createElementNS(this.ns, name);
if (className) {
element.setAttribute('class', className);
}
return element;
}
}
class Theme {
constructor(element) {
this.reservedCategoryColors = {
'default': '#777',
'section': '#999',
'event_listener': '#00b8f5',
'template': '#66cc00',
'doctrine': '#ff6633',
'messenger_middleware': '#bdb81e',
'controller.argument_value_resolver': '#8c5de6',
'http_client': '#ffa333',
};
this.customCategoryColors = [
'#dbab09', // dark yellow
'#ea4aaa', // pink
'#964b00', // brown
'#22863a', // dark green
'#0366d6', // dark blue
'#17a2b8', // teal
];
this.getCategoryColor = this.getCategoryColor.bind(this);
this.getDefaultCategories = this.getDefaultCategories.bind(this);
}
getDefaultCategories() {
return Object.keys(this.reservedCategoryColors);
}
getCategoryColor(category) {
return this.reservedCategoryColors[category] || this.getRandomColor(category);
}
getRandomColor(category) {
// instead of pure randomness, colors are assigned deterministically based on the
// category name, to ensure that each custom category always displays the same color
return this.customCategoryColors[this.hash(category) % this.customCategoryColors.length];
}
// copied from https://github.com/darkskyapp/string-hash
hash(string) {
var hash = 5381;
var i = string.length;
while(i) {
hash = (hash * 33) ^ string.charCodeAt(--i);
}
return hash >>> 0;
}
}

View File

@ -0,0 +1,210 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% import _self as helper %}
{% block toolbar %}
{% if collector.messages|length %}
{% set icon %}
{{ include('@WebProfiler/Icon/translation.svg') }}
{% set status_color = collector.countMissings ? 'red' : collector.countFallbacks ? 'yellow' %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="sf-toolbar-value">{{ error_count ?: collector.countDefines }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Default locale</b>
<span class="sf-toolbar-status">
{{ collector.locale|default('-') }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Missing messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countMissings ? 'red' }}">
{{ collector.countMissings }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Fallback messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countFallbacks ? 'yellow' }}">
{{ collector.countFallbacks }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Defined messages</b>
<span class="sf-toolbar-status">{{ collector.countDefines }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.countMissings ? 'error' : collector.countFallbacks ? 'warning' }} {{ collector.messages is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/translation.svg') }}</span>
<strong>Translation</strong>
{% if collector.countMissings or collector.countFallbacks %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="count">
<span>{{ error_count }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Translation</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.locale|default('-') }}</span>
<span class="label">Default locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.fallbackLocales|join(', ')|default('-') }}</span>
<span class="label">Fallback locale{{ collector.fallbackLocales|length != 1 ? 's' }}</span>
</div>
</div>
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty">
<p>No translations have been called.</p>
</div>
{% else %}
{% block messages %}
{# sort translation messages in groups #}
{% set messages_defined, messages_missing, messages_fallback = [], [], [] %}
{% for message in collector.messages %}
{% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_DEFINED') %}
{% set messages_defined = messages_defined|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %}
{% set messages_missing = messages_missing|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %}
{% set messages_fallback = messages_fallback|merge([message]) %}
{% endif %}
{% endfor %}
<div class="sf-tabs">
<div class="tab {{ collector.countMissings == 0 ? 'active' }}">
<h3 class="tab-title">Defined <span class="badge">{{ collector.countDefines }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are correctly translated into the given locale.
</p>
{% if messages_defined is empty %}
<div class="empty">
<p>None of the used translation messages are defined for the given locale.</p>
</div>
{% else %}
{% block defined_messages %}
{{ helper.render_table(messages_defined) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Fallback <span class="badge {{ collector.countFallbacks ? 'status-warning' }}">{{ collector.countFallbacks }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale
but Symfony found them in the fallback locale catalog.
</p>
{% if messages_fallback is empty %}
<div class="empty">
<p>No fallback translation messages were used.</p>
</div>
{% else %}
{% block fallback_messages %}
{{ helper.render_table(messages_fallback, true) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab {{ collector.countMissings > 0 ? 'active' }}">
<h3 class="tab-title">Missing <span class="badge {{ collector.countMissings ? 'status-error' }}">{{ collector.countMissings }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale and cannot
be found in the fallback locales. Add them to the translation
catalogue to avoid Symfony outputting untranslated contents.
</p>
{% if messages_missing is empty %}
<div class="empty">
<p>There are no messages of this category.</p>
</div>
{% else %}
{% block missing_messages %}
{{ helper.render_table(messages_missing) }}
{% endblock %}
{% endif %}
</div>
</div>
</div>
<script>Sfjs.createFilters();</script>
{% endblock messages %}
{% endif %}
{% endblock %}
{% macro render_table(messages, is_fallback) %}
<table data-filters>
<thead>
<tr>
<th data-filter="locale">Locale</th>
{% if is_fallback %}
<th>Fallback locale</th>
{% endif %}
<th data-filter="domain">Domain</th>
<th>Times used</th>
<th>Message ID</th>
<th>Message Preview</th>
</tr>
</thead>
<tbody>
{% for message in messages %}
<tr data-filter-locale="{{ message.locale }}" data-filter-domain="{{ message.domain }}">
<td class="font-normal text-small nowrap">{{ message.locale }}</td>
{% if is_fallback %}
<td class="font-normal text-small nowrap">{{ message.fallbackLocale|default('-') }}</td>
{% endif %}
<td class="font-normal text-small text-bold nowrap">{{ message.domain }}</td>
<td class="font-normal text-small nowrap">{{ message.count }}</td>
<td>
<span class="nowrap">{{ message.id }}</span>
{% if message.transChoiceNumber is not null %}
<small class="newline">(pluralization is used)</small>
{% endif %}
{% if message.parameters|length > 0 %}
<button class="btn-link newline text-small sf-toggle" data-toggle-selector="#parameters-{{ loop.index }}" data-toggle-alt-content="Hide parameters">Show parameters</button>
<div id="parameters-{{ loop.index }}" class="hidden">
{% for parameters in message.parameters %}
{{ profiler_dump(parameters, maxDepth=1) }}
{% endfor %}
</div>
{% endif %}
</td>
<td class="prewrap">{{ message.translation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@ -0,0 +1,115 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set time = collector.templatecount ? '%0.0f'|format(collector.time) : 'n/a' %}
{% set icon %}
{{ include('@WebProfiler/Icon/twig.svg') }}
<span class="sf-toolbar-value">{{ time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Render Time</b>
<span>{{ time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Template Calls</b>
<span class="sf-toolbar-status">{{ collector.templatecount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Block Calls</b>
<span class="sf-toolbar-status">{{ collector.blockcount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Macro Calls</b>
<span class="sf-toolbar-status">{{ collector.macrocount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label {{ 0 == collector.templateCount ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/twig.svg') }}</span>
<strong>Twig</strong>
</span>
{% endblock %}
{% block panel %}
{% if collector.templatecount == 0 %}
<h2>Twig</h2>
<div class="empty">
<p>No Twig templates were rendered for this request.</p>
</div>
{% else %}
<h2>Twig Metrics</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ '%0.0f'|format(collector.time) }} <span class="unit">ms</span></span>
<span class="label">Render time</span>
</div>
<div class="metric">
<span class="value">{{ collector.templatecount }}</span>
<span class="label">Template calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.blockcount }}</span>
<span class="label">Block calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.macrocount }}</span>
<span class="label">Macro calls</span>
</div>
</div>
<p class="help">
Render time includes sub-requests rendering time (if any).
</p>
<h2>Rendered Templates</h2>
<table id="twig-table">
<thead>
<tr>
<th scope="col">Template Name &amp; Path</th>
<th class="num-col" scope="col">Render Count</th>
</tr>
</thead>
<tbody>
{% for template, count in collector.templates %}
<tr>
{%- set file = collector.templatePaths[template]|default(false) -%}
{%- set link = file ? file|file_link(1) : false -%}
<td>
<span class="sf-icon icon-twig">{{ include('@WebProfiler/Icon/twig.svg') }}</span>
{% if link %}
<a href="{{ link }}" title="{{ file }}">{{ template }}</a>
<div>
<a class="text-muted" href="{{ link }}" title="{{ file }}">
{{ file|file_relative|default(file) }}
</a>
</div>
{% else %}
{{ template }}
{% endif %}
</td>
<td class="font-normal num-col">{{ count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>Rendering Call Graph</h2>
<div id="twig-dump">
{{ collector.htmlcallgraph }}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,103 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.violationsCount > 0 or collector.calls|length %}
{% set status_color = collector.violationsCount ? 'red' %}
{% set icon %}
{{ include('@WebProfiler/Icon/validator.svg') }}
<span class="sf-toolbar-value">
{{ collector.violationsCount ?: collector.calls|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Validator calls</b>
<span class="sf-toolbar-status">{{ collector.calls|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of violations</b>
<span class="sf-toolbar-status {{- collector.violationsCount > 0 ? ' sf-toolbar-status-red' }}">{{ collector.violationsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{- collector.violationsCount ? ' label-status-error' }} {{ collector.calls is empty ? 'disabled' }}">
<span class="icon">{{ include('@WebProfiler/Icon/validator.svg') }}</span>
<strong>Validator</strong>
{% if collector.violationsCount > 0 %}
<span class="count">
<span>{{ collector.violationsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Validator calls</h2>
{% for call in collector.calls %}
<div class="sf-validator sf-reset">
<span class="metadata">In
{% set caller = call.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <a class="text-small sf-toggle" data-toggle-selector="#sf-trace-{{ loop.index0 }}">{{ caller.line }}</a> (<a class="text-small sf-toggle" data-toggle-selector="#sf-context-{{ loop.index0 }}">context</a>):
</span>
<div class="sf-validator-compact hidden" id="sf-trace-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
<div class="sf-validator-compact hidden sf-validator-context" id="sf-context-{{ loop.index0 }}">
{{ profiler_dump(call.context, maxDepth=1) }}
</div>
{% if call.violations|length %}
<table>
<thead>
<tr>
<th>Path</th>
<th>Message</th>
<th>Invalid value</th>
<th>Violation</th>
</tr>
</thead>
{% for violation in call.violations %}
<tr>
<td>{{ violation.propertyPath }}</td>
<td>{{ violation.message }}</td>
<td>{{ profiler_dump(violation.seek('invalidValue')) }}</td>
<td>{{ profiler_dump(violation) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No violations
{% endif %}
</div>
{% else %}
<div class="empty">
<p>No calls to the validator were collected during this request.</p>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M9.8 18L6 22.4c-.3.3-.8.4-1.1 0L1 18c-.4-.5-.1-1 .5-1H3V6.4C3 3.8 5.5 2 8.2 2h3.9c1.1 0 2 .9 2 2s-.9 2-2 2H8.2C7.7 6 7 6 7 6.4V17h2.2c.6 0 1 .5.6 1zM23 6l-3.8-4.5a.8.8 0 0 0-1.1 0L14.2 6c-.4.5-.1 1 .5 1H17v10.6c0 .4-.7.4-1.2.4h-3.9c-1.1 0-2 .9-2 2s.9 2 2 2h3.9c2.6 0 5.2-1.8 5.2-4.4V7h1.5c.6 0 .9-.5.5-1z"/></svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M2.3 6l9-4.6a1.5 1.5 0 0 1 1.4 0l9 4.7a1.5 1.5 0 0 1 0 2.6l-9 4.7a1.5 1.5 0 0 1-1.4 0l-9-4.7a1.5 1.5 0 0 1 0-2.6zm18.3 5L12 15.4 3.4 11a1.4 1.4 0 0 0-1.2 2.4l9.2 4.8a1.4 1.4 0 0 0 1.2 0l9.2-4.8a1.4 1.4 0 0 0-1.3-2.4zm0 4.5L12 19.9l-8.6-4.4a1.4 1.4 0 0 0-1.2 2.4l9.2 4.7a1.4 1.4 0 0 0 1.2 0l9.2-4.7a1.4 1.4 0 0 0-1.3-2.5z"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M21.1 18.3c.8.8.8 2 0 2.8-.4.4-.9.6-1.4.6s-1-.2-1.4-.6L12 14.8l-6.3 6.3c-.4.4-.9.6-1.4.6s-1-.2-1.4-.6a2 2 0 0 1 0-2.8L9.2 12 2.9 5.7a2 2 0 0 1 0-2.8 2 2 0 0 1 2.8 0L12 9.2l6.3-6.3a2 2 0 0 1 2.8 0c.8.8.8 2 0 2.8L14.8 12l6.3 6.3z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M11 5.1C11 3.4 9.6 2 7.9 2H5.1A3.1 3.1 0 0 0 2 5.1V18c0 1.6 1.4 3 3.1 3H8c1.7 0 3.1-1.4 3.1-3.1V5.1zM5.2 4h2.7C8.4 4 9 4.8 9 5.3V11H4V5.3C4 4.8 4.6 4 5.2 4zM22 5.1C22 3.4 20.6 2 18.9 2H16c-1.6 0-3 1.4-3 3.1V18c0 1.7 1.4 3.1 3.1 3.1H19c1.7 0 3.1-1.4 3.1-3.1V5.1zM16 4h2.8c.6 0 1.2.8 1.2 1.3V8h-5V5.3c0-.5.5-1.3 1-1.3z"/></svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M19.2 20.8c.4.7.1 1.6-.6 2l-.7.2c-.5 0-1-.3-1.3-.8l-3.7-6.7-1 .1-.9-.1-3.7 6.7c-.4.5-.9.8-1.5.8l-.7-.2c-.7-.4-1-1.3-.6-2l3.8-6.9c-.5-.7-.9-1.6-.9-2.6.1-2.4 2-4.3 4.4-4.3s4.3 1.9 4.3 4.3c0 .9-.3 1.8-.8 2.5l3.9 7zM5.2 11c.6 0 1-.3 1-.8 0-2.1 1.6-3.8 3.7-4.1.5-.1.9-.6.8-1.2-.1-.5-.6-.9-1.1-.9-3.1.5-5.3 3-5.3 6.1-.1.6.4.9.9.9zm8.4-5c2.1.3 3.7 2.1 3.8 4.2 0 .5.5.8 1 .8.6 0 1-.3 1-.8 0-3.1-2.4-5.6-5.5-6.1-.5-.1-1.1.3-1.1.8-.2.6.2 1 .8 1.1zM9 3c.5-.1.9-.6.8-1.1-.1-.6-.6-.9-1.1-.8a9 9 0 0 0-7.4 8.7c0 .6.4 1.2 1 1.2.5 0 1-.6 1-1.2C3.3 6.5 5.7 3.5 9 3zm5.7-2c-.5-.1-1.1.3-1.1.9s.3 1.1.8 1.1c3.3.5 5.8 3.4 5.8 6.8 0 .5.5 1.2 1 1.2.6 0 1-.7 1-1.2A9 9 0 0 0 14.7 1z"/></svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M23.5 9.5c0-.2-1.2.2-1.6.2l.4-.8C23 7.4 22 6.6 21 7.5c-.4.4 0 1.1 0 1.8v.3l-.6-.3c-.5-.8-1.1-.2-1.1 0 0 .3.7.9 1.1.9h.2v.5c0 .7-.8 1.1-1.7 1.2V9.1c0-4.3-3.3-6.4-6.9-6.4-3.5 0-6.9 2-6.9 6.4v2.8c-.9-.2-1.8-.5-1.8-1.2v-.2h.2c.5 0 1.1-.2 1.1-.4.2-1.4-.6-.5-1.1-.5h-.3l.1-.4c0-.5 1.2-1.7-.8-1.9-.4 0-.5.9-.4 1.3l.4 1.2c-.1-.2-.3-.2-.5-.3-.2-.2-1.6-1.9-1.9 0-.1 1.1 1 1.2 1.9 1l.3-.1-.2 1.2c0 1.3 1.5 1.6 2.9 1.7v5.2c0 1.6.5 2.8 2.2 2.8 1.8 0 2.4-1.3 2.4-2.9 0 1.6.6 2.9 2.3 2.9s2.3-2.2 2.3-2.8c0 1.7.7 2.8 2.4 2.8s2.2-1.2 2.2-2.9v-5.1c1.4-.1 2.9-.4 2.9-1.7l-.1-1c.4.5 1.1.8 1.7.5 1.2-.7.2-1.4.2-1.6zM6.8 8.4c0-1.5 1-2.5 2.3-2.5 1.3 0 2.3 1.1 2.3 2.5s-1 2.6-2.2 2.6c.6 0 1.1-.5 1.1-1.2 0-.6-.5-1.2-1.2-1.2-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2-1.3 0-2.3-1.1-2.3-2.6zm5.1 7.5c-2.9-.1-3.1-1.6-3.1-2.5 0-.9 1.7-.3 3.2-.3 1.5 0 3.1-.7 3.1.2 0 1-.8 2.7-3.2 2.6zM15 11c.6-.1 1-.6 1-1.2s-.5-1.2-1.2-1.2c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.1 1.2-1.3 0-2.3-1.2-2.3-2.6 0-1.5 1-2.5 2.3-2.5C16 5.9 17 7 17 8.4c.1 1.4-.8 2.5-2 2.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M20.5 4H18V2.5c0-.8-.7-1.5-1.5-1.5h-9C6.7 1 6 1.7 6 2.5V4H3.5C2.7 4 2 4.7 2 5.5v16c0 .8.7 1.5 1.5 1.5h17c.8 0 1.5-.7 1.5-1.5v-16c0-.8-.7-1.5-1.5-1.5zM9 4h6v1H9V4zm10 16H5V7h1.1c.2.6.8 1 1.4 1h9c.7 0 1.2-.4 1.4-1H19v13zm-2-9c0 .6-.4 1-1 1H8c-.6 0-1-.4-1-1s.4-1 1-1h8c.6 0 1 .4 1 1zm0 3c0 .6-.4 1-1 1H8c-.6 0-1-.4-1-1s.4-1 1-1h8c.6 0 1 .4 1 1zm-4 3c0 .6-.4 1-1 1H8c-.6 0-1-.4-1-1s.4-1 1-1h4c.6 0 1 .4 1 1z"/></svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#aaa" d="M23.6 11l-6.5-6.6a1.2 1.2 0 0 0-2.1.9V9H1.4A1.8 1.8 0 0 0 0 10.8v2.6A1.6 1.6 0 0 0 1.4 15H15v3.7a1.2 1.2 0 0 0 2 1l6.7-6.8a1.3 1.3 0 0 0 0-1.8z"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M20.4 12c-1 0-1.8.6-2.2 1.4l-2.6-.9c.1-.3.1-.5.1-.8 0-1.2-.6-2.2-1.5-2.9l1.5-2.6c.3.1.6.2 1 .2 1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5-2.5 1.1-2.5 2.5c0 .8.4 1.5.9 1.9l-1.5 2.6c-.5-.3-1-.4-1.6-.4-.9 0-1.7.3-2.3.9L7.4 6.6c.3-.4.5-.9.5-1.5 0-1.4-1.1-2.5-2.5-2.5S2.7 3.7 2.7 5.1s1.1 2.5 2.5 2.5c.6 0 1.1-.2 1.5-.5L9 9.4c-.5.6-.8 1.4-.8 2.3 0 .7.2 1.4.6 2l-3.9 3.8c-.4-.3-.9-.5-1.5-.5C2 17 .9 18.1.9 19.5S2.2 22 3.6 22s2.5-1.1 2.5-2.5c0-.5-.2-1-.5-1.5l3.8-3.7c.7.7 1.6 1.1 2.6 1.1h.2l.4 2.4c-1 .3-1.7 1.3-1.7 2.4 0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5l-.4-2.5c1-.3 1.9-1 2.3-2l2.6.9v.4c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5c.1-1.4-1.1-2.5-2.5-2.5z"/></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M21 4v13.8c0 2.7-2.5 5.2-5.2 5.2H6c-.6 0-1-.4-1-1s.4-1 1-1h9.8c1.6 0 3.2-1.7 3.2-3.2V4c0-.6.4-1 1-1s1 .4 1 1zM5.5 20A2.5 2.5 0 0 1 3 17.5v-14C3 2.1 4.1 1 5.5 1h10.1C16.9 1 18 2.1 18 3.5v14.1c0 1.4-1.1 2.5-2.5 2.5h-10zM9 11.4c0 .3.3.6.6.6h1.8c.3 0 .6-.3.6-.6V4.6c0-.3-.3-.6-.6-.6H9.6c-.3 0-.6.3-.6.6v6.8zm0 5c0 .3.3.6.6.6h1.8c.3 0 .6-.3.6-.6v-1.8c0-.3-.3-.6-.6-.6H9.6c-.3 0-.6.3-.6.6v1.8z"/></svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAAAAA" d="M22,4.9C22,3.9,21.1,3,20.1,3H3.9C2.9,3,2,3.9,2,4.9v13.1C2,19.1,2.9,20,3.9,20h16.1c1.1,0,1.9-0.9,1.9-1.9V4.9z M8.3,14.1l-3.1,3.1c-0.2,0.2-0.5,0.3-0.7,0.3S4,17.4,3.8,17.2c-0.4-0.4-0.4-1,0-1.4l3.1-3.1c0.4-0.4,1-0.4,1.4,0S8.7,13.7,8.3,14.1z M20.4,17.2c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3l-3.1-3.1c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l3.1,3.1C20.8,16.2,20.8,16.8,20.4,17.2z M20.4,7.2l-7.6,7.6c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3L3.8,7.2c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6.9,6.9L19,5.8c0.4-0.4,1-0.4,1.4,0S20.8,6.8,20.4,7.2z"/></svg>

After

Width:  |  Height:  |  Size: 643 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M6 18.9V15h12v3.9c0 .7-.2 1.1-1 1.1H7c-.8 0-1-.4-1-1.1zM20 1c-.6 0-1 .5-1 1.1v18c0 .5-.4.9-.9.9H5.9a.9.9 0 0 1-.9-.9v-18C5 1.5 4.6 1 4 1c-.5 0-1 .5-1 1.1v18C3 21.7 4.3 23 5.9 23h12.2c1.6 0 2.9-1.3 2.9-2.9v-18c0-.6-.4-1.1-1-1.1zm-2 8H6v5h12V9z"/></svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M2.6 17.5h18.8c.9 0 1.6.7 1.6 1.6v1.5c0 1-.7 1.6-1.6 1.6H2.6c-.9 0-1.6-.7-1.6-1.6v-1.5c0-.9.7-1.6 1.6-1.6zM1 11.2v1.6c0 .9.7 1.6 1.6 1.6h18.8c.9 0 1.6-.7 1.6-1.6v-1.6c0-.8-.7-1.6-1.6-1.6H2.6A1.6 1.6 0 0 0 1 11.2zm0-7.8v1.5a1.6 1.6 0 0 0 1.6 1.6h18.8c.9 0 1.6-.7 1.6-1.6V3.4c0-1-.7-1.6-1.6-1.6H2.6A1.6 1.6 0 0 0 1 3.4z"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#aaa" d="M16 9a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2h-3V4a1 1 0 0 0-1-1H8a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2h3v6H8a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2h3v9a1 1 0 0 0 2 0v-5h3a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2h-3V9zm2.52-2.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm0 1.63h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.52zm-13-2.82h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.54.48zm0-1.62h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.54.48zm0 9.62h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.54.48zm0-1.62h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.54.48zm13 2.81h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm0 1.63h3a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.52z"/></svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 12 12"><path fill="#999" d="M10.4 8.4L8 6l2.4-2.4c.8-.8.7-1.6.2-2.2-.6-.5-1.4-.6-2.2.2L6 4 3.6 1.6C2.8.8 2 .9 1.4 1.4c-.5.6-.6 1.4.2 2.2L4 6 1.6 8.4c-.8.8-.7 1.6-.2 2.2.6.6 1.4.6 2.2-.2L6 8l2.4 2.4c.8.8 1.6.7 2.2.2.5-.6.6-1.4-.2-2.2z"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 12 12"><path fill="#B0413E" d="M10.4 8.4L8 6l2.4-2.4c.8-.8.7-1.6.2-2.2-.6-.5-1.4-.6-2.2.2L6 4 3.6 1.6C2.8.8 2 .9 1.4 1.4c-.5.6-.6 1.4.2 2.2L4 6 1.6 8.4c-.8.8-.7 1.6-.2 2.2.6.6 1.4.6 2.2-.2L6 8l2.4 2.4c.8.8 1.6.7 2.2.2.5-.6.6-1.4-.2-2.2z"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M11.23,4.8c-3,.83-5.07,3.18-5.07,5.9H7.39c0-2.21,1.77-4,4.17-4.72ZM7.49,0A12.22,12.22,0,0,0,0,11.59H2.07A10.14,10.14,0,0,1,8.23,2Zm8.24,2a10.14,10.14,0,0,1,6.16,9.64H24A12.24,12.24,0,0,0,16.47,0ZM4.41,15.64V10.7a7.57,7.57,0,0,1,15.14,0v4.94l3.3,4.4H1.11Zm4.45,5.3A3.06,3.06,0,0,0,11.92,24H12a3.07,3.07,0,0,0,3.07-3.06Z"/></svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#aaa" d="M23 7.8L14 .4a1.3 1.3 0 0 0-2 .9V4a13.6 13.6 0 0 1-2.2.6l-1.3.5c-.5.1-1 .4-1.4.6l-.7.4-.7.4a10.6 10.6 0 0 0-1.4 1A13.2 13.2 0 0 0 3 8.8a15.3 15.3 0 0 0-1.1 1.5 17.6 17.6 0 0 0-.9 1.6l-.5 1.7c-.2.5 0 1.2 0 1.7a10.2 10.2 0 0 0 0 1.5A5.7 5.7 0 0 0 1 18l.4 1.2 1 2 1 1.4 1 1c.2.2.4.1.3-.2l-.3-1.2-.3-1.6-.1-1.9v-1a3.4 3.4 0 0 1 .2-1 6.4 6.4 0 0 1 .3-.8l.4-.8.6-.6.6-.6.7-.4a7.5 7.5 0 0 1 .8-.2 4.5 4.5 0 0 1 .8-.2h2.5a3.8 3.8 0 0 1 1.2.3v3.1a1.3 1.3 0 0 0 2 1l9-7.5a1.5 1.5 0 0 0 0-2.3z"/></svg>

After

Width:  |  Height:  |  Size: 596 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M15.8 6.4h-1.1s-.1.1-.1 0l.8-.7c.5-.5.5-1.3 0-1.9L14 2.4c-.5-.5-1.4-.5-1.9 0l-.6.8c-.1 0 0 0 0-.1v-1c0-.8-1-1.4-1.8-1.4h-2c-.8 0-1.9.6-1.9 1.4v1.1l.1.1-.8-.8c-.5-.5-1.3-.5-1.9 0L1.8 3.9c-.5.5-.5 1.4 0 1.9l.8.6c0 .1 0 0-.1 0H1.4C.7 6.4 0 7.5 0 8.2v2C0 11 .7 12 1.4 12h1.2l.1-.1-.8.7c-.5.5-.5 1.3 0 1.9L3.3 16c.5.5 1.4.5 1.9 0l.6-.8-.1.1v1.2c0 .8 1.1 1.4 1.9 1.4h2c.8 0 1.8-.6 1.8-1.4v-1.2s-.1-.1 0-.1l.7.8c.5.5 1.3.5 1.9 0l1.4-1.4c.5-.5.5-1.4 0-1.9l-.8-.7.1.1h1.1c.8 0 1.3-1.1 1.3-1.8v-2c0-.8-.6-1.9-1.3-1.9zM8.6 13a3.8 3.8 0 1 1 3.8-3.8A4 4 0 0 1 8.6 13zm13.7 2.6l-.6.2s0 .1 0 0l.3-.5c.2-.4 0-.8-.4-1l-1-.4c-.4-.2-.8 0-1 .4l-.1.5-.2-.6c-.2-.4-.8-.5-1.2-.3l-1.1.4c-.4.2-.8.7-.7 1.1l.2.6h.1l-.5-.3c-.4-.2-.8 0-1 .4l-.4 1c-.2.4 0 .8.4 1l.5.1-.6.2c-.4.2-.5.8-.4 1.2l.4 1.1c.2.4.7.8 1.1.7l.6-.2s0-.1 0 0l-.3.5c-.2.4 0 .8.4 1l1 .4c.4.2.8 0 1-.4l.1-.5.2.6c.2.4.9.5 1.2.3l1.1-.4c.4-.2.8-.7.6-1.1l-.2-.6s-.1 0 0 0l.5.3c.4.2.8 0 1-.4l.4-1c.2-.4 0-.8-.4-1l-.5-.1.6-.2c.4-.2.5-.8.3-1.2l-.4-1.1c-.1-.4-.6-.8-1-.7zm-2.4 4.9a2 2 0 0 1-2.7-1.2 2 2 0 0 1 1.2-2.7 2 2 0 0 1 2.7 1.2 2 2 0 0 1-1.2 2.7z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M13 3v18c0 1.1-.9 2-2 2s-2-.9-2-2V3c0-1.1.9-2 2-2s2 .9 2 2zm10.2 1.6l-1.8-1.4c-.2-.3-.6-.2-1-.2H14v5h6.4c.4 0 .8-.3 1.1-.5l1.8-1.6c.3-.3.3-1-.1-1.3zm-3.7 4.8c-.3-.3-.7-.4-1.1-.4H14v5h4.4a2 2 0 0 0 1.1-.3l1.8-1.5c.4-.3.4-.9 0-1.3l-1.8-1.5zM3.5 7c-.4 0-.7 0-1 .3L.7 8.8c-.4.3-.4.9 0 1.3l1.8 1.6c.3.2.6.3 1 .3H8V7H3.5z"/></svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M11.6.4A8 8 0 0 0 5.2 13L.8 17.5a1.3 1.3 0 0 0-.4.9c0 .3.1.6.4.9s.5.3.9.3c.3 0 .6 0 .9-.3l4.3-4.5a7.9 7.9 0 0 0 4.7 1.5 8 8 0 0 0 0-16zm5.5 8a5.5 5.5 0 0 1-5.5 5.4 5.5 5.5 0 1 1 5.5-5.5z"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M12 .9C5.8.9.9 5.8.9 12a11 11 0 1 0 22.2 0A11 11 0 0 0 12 .9zm6.5 6c-.6 0-.9-.3-.9-.8 0-.2 0-.4.2-.6l.2-.4c0-.3-.5-.4-.6-.4-1.8.1-2.3 2.5-2.7 4.4l-.2 1c1 .2 1.8 0 2.2-.3.6-.4-.2-.7-.1-1.2.1-.3.5-.5.7-.6.5 0 .7.5.7.9 0 .7-1 1.8-3 1.8l-.6-.1-.6 2.4c-.4 1.6-.8 3.8-2.4 5.7-1.4 1.7-2.9 1.9-3.5 1.9-1.2 0-1.9-.6-2-1.5 0-.8.7-1.3 1.2-1.3.6 0 1.1.5 1.1 1s-.2.6-.4.6c-.1.1-.3.2-.3.4 0 .1.1.3.4.3.5 0 .8-.3 1.1-.5 1.2-.9 1.6-2.7 2.2-5.7l.1-.7.7-3.2c-.8-.6-1.3-1.4-2.4-1.7-.6-.1-1.1.1-1.5.5-.4.5-.2 1.1.2 1.5l.7.6c.7.8 1.2 1.6 1 2.5-.3 1.5-2 2.6-4 1.9-1.8-.6-2-1.8-1.8-2.5.2-.6.6-.7 1.1-.6.5.2.6.7.6 1.2l-.1.3c-.2.1-.3.3-.3.4-.1.4.4.6.7.7.7.3 1.6-.2 1.8-.8a1 1 0 0 0-.4-1.1l-.7-.8c-.4-.4-1.1-1.4-.7-2.6.1-.5.4-.9.7-1.3a4 4 0 0 1 2.8-.6c1.2.4 1.8 1.1 2.6 1.8.5-1.2 1-2.4 1.8-3.5.9-.9 1.9-1.6 3.1-1.7 1.3.2 2.2.7 2.2 1.6 0 .4-.2 1.1-.9 1.1z"/></svg>

After

Width:  |  Height:  |  Size: 942 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M15.1 4.3a13 13 0 0 0-6.2 0c-.3 0-.7-.2-.7-.5v-.4c0-1.2 1-2.3 2.3-2.3h3c1.2 0 2.3 1 2.3 2.3v.3c0 .4-.4.6-.7.6zm5.8 9.7a9 9 0 0 1-17.8 0 9 9 0 0 1 17.8 0zm-4.2 1c0-.6-.4-1-1-1H13V8.4c0-.6-.4-1-1-1s-1 .4-1 1v6.2c0 .6.4 1.3 1 1.3h3.7c.5.1 1-.3 1-.9z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M5.4 6H7v.3c0 1.2-.7 1.9-1.7 1.9-1.1 0-1.4-.4-1.4-1.1 0-.9.6-1.1 1.5-1.1zm3.8-6H2.7C1.2 0 0 .9 0 2.4v6.5C0 10.4 1.2 11 2.7 11h1.2l3.3 3.2c.4.2.8.3.8-.1V9.9l.1-1.1h-.4c-.3.1-.7-.1-.7-.4v-.5c0 .7-1 1-1.8 1-1.5 0-2.4-.7-2.4-2s1.1-2 2.6-2H7v-.4c0-1-.4-1.6-1.5-1.6-.7 0-1.1.2-1.5.7l-.3.2a.4.4 0 0 1-.4-.4l.1-.2c.4-.7 1-1.2 2.2-1.2C7.2 2 8 3 8 4.5v3c1-1.4 1.8-2.4 4-2.4V2.4C12 .9 10.7 0 9.2 0zm11.5 6h-8C10.8 6 9 7.2 9 9v8c0 1.8 2 3.3 4 3.3v3c0 .5.5.7.9.3l4-3.7h2.7c1.8 0 3.3-1.2 3.3-3V9c.1-1.8-1.4-3-3.2-3zM13 9h7v.8s-.7.3-1.1.3h-4.8a10 10 0 0 1-1.1-.4V9zm-.5 7.9l-.6-.5c1-.8 1.8-2.1 2.2-3.4l.7.3c-.6 1.5-1.4 2.6-2.3 3.6zM17 12v4.8c0 .7-.2.8-1.2.8l-1.4-.1-.2-.7 1.4.1c.4 0 .4 0 .4-.3V12h-2.9l-1.1.1v-.8s.7-.4 1.1-.4h6.8c.5 0 1.1.4 1.1.4v.8l-1.1-.1H17zm3.7 4.8c-1-1.1-1.6-1.9-2.3-3.6l.6-.2a9 9 0 0 0 2.2 3.3l-.5.5z"/></svg>

After

Width:  |  Height:  |  Size: 922 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M8.932 22.492c.016-6.448-.971-11.295-5.995-11.619 4.69-.352 7.113 2.633 9.298 6.907C12.205 6.354 9.882 1.553 4.8 1.297c7.433.07 10.028 5.9 11.508 14.293 1.171-2.282 3.56-5.553 5.347-1.361-1.594-2.04-3.607-1.617-3.978 8.262H8.933z"/></svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#aaa" d="M19.5 22.5H4.3a2.9 2.9 0 0 1-2.9-2.9V4.4a2.9 2.9 0 0 1 2.9-2.9h14.2a1 1 0 0 1 0 2H4.3a.9.9 0 0 0-.9.9v15.2a.9.9 0 0 0 .9.9h15.2a.9.9 0 0 0 1-.9v-8.3a1 1 0 1 1 2 0v8.3a2.9 2.9 0 0 1-3 2.9zM13 17.3L22.9 6a1.5 1.5 0 1 0-2.3-2L12 14 8 9.1A1.5 1.5 0 0 0 5.7 11l5 6.3a1.5 1.5 0 0 0 1.2.5 1.5 1.5 0 0 0 1.1-.5z"/></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 12 12"><path fill="#5E976E" d="M12 3.1c0 .4-.1.8-.4 1.1L5.9 9.8c-.3.3-.6.4-1 .4s-.7-.1-1-.4L.4 6.3C.1 6 0 5.6 0 5.2c0-.4.2-.7.4-.9.2-.3.6-.4.9-.4.4 0 .8.1 1.1.4l2.5 2.5 4.7-4.7c.3-.3.7-.4 1-.4.4 0 .7.2.9.4.3.3.5.6.5 1z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1 @@
{% block panel '' %}

View File

@ -0,0 +1,20 @@
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col" class="key">{{ labels is defined ? labels[0] : 'Key' }}</th>
<th scope="col">{{ labels is defined ? labels[1] : 'Value' }}</th>
</tr>
</thead>
<tbody>
{% for key in bag.keys|sort %}
<tr>
<th>{{ key }}</th>
<td>{{ profiler_dump(bag.get(key), maxDepth=maxDepth|default(0)) }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">(no data)</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="{{ _charset }}" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{% block title %}Symfony Profiler{% endblock %}</title>
<link rel="icon" type="image/x-icon" sizes="16x16" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFEUlEQVR4AZVXA4wm2RMf27bXDM/+3/+sYBGfrbVtezc6BWtzfPbYXtvDL9906t6v0vWl05me7q1JzXuvvu4yXnvZgJ9hH6bwZYXLFR739vauUGuDwhq1L1N4Uv/tRYUhFjwcg49hn6aYr1V4TiGp86CoP9Oh1tV414KnM6t9fHymKUZ3DAI0hW4b1AyK3lE8phh5OxWeoJgUGhi5mLm95YzBwcHuhIQEV1JSEoWGhoKWHxYWFmenhJ/B5W0GwZpDt5Ovry9lZWWRyWOu5ORk7JsUpogsq5gnmISTU+HKQoLFQv/qq6/os88+I+EVFRUlSsRZ5oRiVmwlXMWShQkahUdERJCfnx/vd+3aRTU1NXTixAmqrq6mK1eu0PTp05mnrmD+QK6XhLO0XP2O2FJAQICRjjMU4P1PP/1EfX19NGfOHM8Z0N7ezueQkBBXYGAgSWIaQ5Em2T5QzFNSUig9PV3OHOe4uDjZ87p//34C7Nm7x/NcRUUFAX799Vec8Y7m7+8Pz92SfBDXr7VwPYRbxn/MmDG8Tps2jQBd3V30/PPPe35/6qmnaPXq1TR69Gg+h4eHiwwosdLT4dBkQDSXWmJiIq/vv/8+/fvvv3ThwgWqr6+n/Px8oyCmAerq6jy03Nxc2Yv7ySSjQzrmi4i92fVpaWlYOZ79/f2MW7dtpSlTptDp06epo6ODPvroI850ASiGdyZOnEjXrl2jyspKT4XA9cgjkaPL/D8UWG62HokieyQQoKSkRGiMs2bNotraWmprayOBNWvWyO+scGdnp5zF/WYvLEb8TwpRykp1MV7feust6uzqJMD169fpueeeY/rDDz/MKzzgdrsJoGkaffvtt/TFF19wQsIDmzZtssojt+6Fo1CgzKiAvAB3DRs2jAULtLS0eErPGB5Ad3c3lZaWUnFxMfeAd955h5+JjY3FaqXAPwhBnRCNySK4b98+Aoilv/z6i/zGggSk1g0opWupAMvGP91yt96zadWqVdTc3Ezz58/31LOAy+US6zgHBP766y+mDR8+HBUgFWSnQI2EAFnqlpcaGxsJIFkMN8L9AnPnzmX6jRs3SACeAi0vL888JwYPgTEJpauhnADo6/LSgQMHCHD37l2Cp15//XXq7eslgKb+Fi1exM9lZmbaCDclIcpQQhATE4OVsrOzuamg+cyePZuzG64Hrlu3jp9ZuWolCdy+fZueeOIJpkdHR1sLHqgM0Yh0bTRz1m7fvp2KiopYkYKCApo8ebLZIwzlFeXSOXEnsLPe2Ij+p5DbYYdOdOtDQ0rNjFya5sTcsGGDcTDZoXTcNoVBMoxWyzDS2yXmOyeUtGSskmDjx4/nRgPAfBDmMpZtUIbRcsi2GsfSD2QYyd2OcdmyZSSwdu1apuXk5GB16v4bak0yX0imyIUEgwNovFTglhMZGcm0srIy43zAVUxuTLbW4xn17Fci23wly9dngUummrTaixcvMpOtW7fiiBwQpqKYU9efHuxDJE5hC9wvL9TW1RLg+PHjPGTQ8wsLC4WpDC5Y5UR4k5qKMSLT6lqeAiX0nuAaMmSI9sMPP9CZM2foyJEj9O677wpTVIuTjidNp0HibvttoH9E5OMqbWKkSaNSlojldoLF7TEP+nUEmKI62y1kOBINbVaNarcI0PuGGUlHyfYvLHg7/jhFSFYqZh0P8KHSptd5ksOPU3tvqAEUot/hFmOIYJLp87wGe9Dwm95eg5xa/R8G6d8U5EcFhwAAAABJRU5ErkJggg==">
{% block head %}
<style{% if csp_style_nonce is defined and csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %}>
{{ include('@WebProfiler/Profiler/profiler.css.twig') }}
</style>
{% endblock %}
</head>
<body>
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
if (null === localStorage.getItem('symfony/profiler/theme') || 'theme-auto' === localStorage.getItem('symfony/profiler/theme')) {
document.body.classList.add((matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light'));
// needed to respond dynamically to OS changes without having to refresh the page
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add(e.matches ? 'theme-dark' : 'theme-light');
});
} else {
document.body.classList.add(localStorage.getItem('symfony/profiler/theme'));
}
document.body.classList.add(localStorage.getItem('symfony/profiler/width') || 'width-normal');
</script>
{% block body '' %}
</body>
</html>

View File

@ -0,0 +1,874 @@
{# This file is partially duplicated in src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js.
If you make any change in this file, verify the same change is needed in the other file. #}
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>/*<![CDATA[*/
{# Caution: the contents of this file are processed by Twig before loading
them as JavaScript source code. Always use '/*' comments instead
of '//' comments to avoid impossible-to-debug side-effects #}
if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') {
Sfjs = (function() {
"use strict";
if ('classList' in document.documentElement) {
var hasClass = function (el, cssClass) { return el.classList.contains(cssClass); };
var removeClass = function(el, cssClass) { el.classList.remove(cssClass); };
var addClass = function(el, cssClass) { el.classList.add(cssClass); };
var toggleClass = function(el, cssClass) { el.classList.toggle(cssClass); };
} else {
var hasClass = function (el, cssClass) { return el.className.match(new RegExp('\\b' + cssClass + '\\b')); };
var removeClass = function(el, cssClass) { el.className = el.className.replace(new RegExp('\\b' + cssClass + '\\b'), ' '); };
var addClass = function(el, cssClass) { if (!hasClass(el, cssClass)) { el.className += " " + cssClass; } };
var toggleClass = function(el, cssClass) { hasClass(el, cssClass) ? removeClass(el, cssClass) : addClass(el, cssClass); };
}
var noop = function() {};
var profilerStorageKey = 'symfony/profiler/';
var addEventListener;
var el = document.createElement('div');
if (!('addEventListener' in el)) {
addEventListener = function (element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
};
} else {
addEventListener = function (element, eventName, callback) {
element.addEventListener(eventName, callback, false);
};
}
if (navigator.clipboard) {
document.querySelectorAll('[data-clipboard-text]').forEach(function(element) {
removeClass(element, 'hidden');
element.addEventListener('click', function() {
navigator.clipboard.writeText(element.getAttribute('data-clipboard-text'));
})
});
}
var request = function(url, onSuccess, onError, payload, options, tries) {
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
options = options || {};
options.retry = options.retry || false;
tries = tries || 1;
/* this delays for 125, 375, 625, 875, and 1000, ... */
var delay = tries < 5 ? (tries - 0.5) * 250 : 1000;
xhr.open(options.method || 'GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function(state) {
if (4 !== xhr.readyState) {
return null;
}
if (xhr.status == 404 && options.retry && !options.stop) {
setTimeout(function() {
if (options.stop) {
return;
}
request(url, onSuccess, onError, payload, options, tries + 1);
}, delay);
return null;
}
if (200 === xhr.status) {
(onSuccess || noop)(xhr);
} else {
(onError || noop)(xhr);
}
};
if (options.onSend) {
options.onSend(tries);
}
xhr.send(payload || '');
};
var getPreference = function(name) {
if (!window.localStorage) {
return null;
}
return localStorage.getItem(profilerStorageKey + name);
};
var setPreference = function(name, value) {
if (!window.localStorage) {
return null;
}
localStorage.setItem(profilerStorageKey + name, value);
};
var requestStack = [];
var extractHeaders = function(xhr, stackElement) {
/* Here we avoid to call xhr.getResponseHeader in order to */
/* prevent polluting the console with CORS security errors */
var allHeaders = xhr.getAllResponseHeaders();
var ret;
if (ret = allHeaders.match(/^x-debug-token:\s+(.*)$/im)) {
stackElement.profile = ret[1];
}
if (ret = allHeaders.match(/^x-debug-token-link:\s+(.*)$/im)) {
stackElement.profilerUrl = ret[1];
}
if (ret = allHeaders.match(/^Symfony-Debug-Toolbar-Replace:\s+(.*)$/im)) {
stackElement.toolbarReplaceFinished = false;
stackElement.toolbarReplace = '1' === ret[1];
}
};
var successStreak = 4;
var pendingRequests = 0;
var renderAjaxRequests = function() {
var requestCounter = document.querySelector('.sf-toolbar-ajax-request-counter');
if (!requestCounter) {
return;
}
requestCounter.textContent = requestStack.length;
var infoSpan = document.querySelector(".sf-toolbar-ajax-info");
if (infoSpan) {
infoSpan.textContent = requestStack.length + ' AJAX request' + (requestStack.length !== 1 ? 's' : '');
}
var ajaxToolbarPanel = document.querySelector('.sf-toolbar-block-ajax');
if (requestStack.length) {
ajaxToolbarPanel.style.display = 'block';
} else {
ajaxToolbarPanel.style.display = 'none';
}
if (pendingRequests > 0) {
addClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
} else if (successStreak < 4) {
addClass(ajaxToolbarPanel, 'sf-toolbar-status-red');
removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
} else {
removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
removeClass(ajaxToolbarPanel, 'sf-toolbar-status-red');
}
};
var startAjaxRequest = function(index) {
var tbody = document.querySelector('.sf-toolbar-ajax-request-list');
if (!tbody) {
return;
}
var nbOfAjaxRequest = tbody.rows.length;
if (nbOfAjaxRequest >= 100) {
tbody.deleteRow(0);
}
var request = requestStack[index];
pendingRequests++;
var row = document.createElement('tr');
request.DOMNode = row;
var requestNumberCell = document.createElement('td');
requestNumberCell.textContent = index + 1;
row.appendChild(requestNumberCell);
var profilerCell = document.createElement('td');
profilerCell.textContent = 'n/a';
row.appendChild(profilerCell);
var methodCell = document.createElement('td');
methodCell.textContent = request.method;
row.appendChild(methodCell);
var typeCell = document.createElement('td');
typeCell.textContent = request.type;
row.appendChild(typeCell);
var statusCodeCell = document.createElement('td');
var statusCode = document.createElement('span');
statusCode.textContent = 'n/a';
statusCodeCell.appendChild(statusCode);
row.appendChild(statusCodeCell);
var pathCell = document.createElement('td');
pathCell.className = 'sf-ajax-request-url';
if ('GET' === request.method) {
var pathLink = document.createElement('a');
pathLink.setAttribute('href', request.url);
pathLink.textContent = request.url;
pathCell.appendChild(pathLink);
} else {
pathCell.textContent = request.url;
}
pathCell.setAttribute('title', request.url);
row.appendChild(pathCell);
var durationCell = document.createElement('td');
durationCell.className = 'sf-ajax-request-duration';
durationCell.textContent = 'n/a';
row.appendChild(durationCell);
request.liveDurationHandle = setInterval(function() {
durationCell.textContent = (new Date() - request.start) + ' ms';
}, 100);
row.className = 'sf-ajax-request sf-ajax-request-loading';
tbody.insertBefore(row, null);
var toolbarInfo = document.querySelector('.sf-toolbar-block-ajax .sf-toolbar-info');
toolbarInfo.scrollTop = toolbarInfo.scrollHeight;
renderAjaxRequests();
};
var finishAjaxRequest = function(index) {
var request = requestStack[index];
clearInterval(request.liveDurationHandle);
if (!request.DOMNode) {
return;
}
if (request.toolbarReplace && !request.toolbarReplaceFinished && request.profile) {
/* Flag as complete because finishAjaxRequest can be called multiple times. */
request.toolbarReplaceFinished = true;
/* Search up through the DOM to find the toolbar's container ID. */
for (var elem = request.DOMNode; elem && elem !== document; elem = elem.parentNode) {
if (elem.id.match(/^sfwdt/)) {
Sfjs.loadToolbar(elem.id.replace(/^sfwdt/, ''), request.profile);
break;
}
}
}
pendingRequests--;
var row = request.DOMNode;
/* Unpack the children from the row */
var profilerCell = row.children[1];
var methodCell = row.children[2];
var statusCodeCell = row.children[4];
var statusCodeElem = statusCodeCell.children[0];
var durationCell = row.children[6];
if (request.error) {
row.className = 'sf-ajax-request sf-ajax-request-error';
methodCell.className = 'sf-ajax-request-error';
successStreak = 0;
} else {
row.className = 'sf-ajax-request sf-ajax-request-ok';
successStreak++;
}
if (request.statusCode) {
if (request.statusCode < 300) {
statusCodeElem.setAttribute('class', 'sf-toolbar-status');
} else if (request.statusCode < 400) {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-yellow');
} else {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red');
}
statusCodeElem.textContent = request.statusCode;
} else {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red');
}
if (request.duration) {
durationCell.textContent = request.duration + ' ms';
}
if (request.profilerUrl) {
profilerCell.textContent = '';
var profilerLink = document.createElement('a');
profilerLink.setAttribute('href', request.profilerUrl);
profilerLink.textContent = request.profile;
profilerCell.appendChild(profilerLink);
}
renderAjaxRequests();
};
{% if excluded_ajax_paths is defined %}
if (window.fetch && window.fetch.polyfill === undefined) {
var oldFetch = window.fetch;
window.fetch = function () {
var promise = oldFetch.apply(this, arguments);
var url = arguments[0];
var params = arguments[1];
var paramType = Object.prototype.toString.call(arguments[0]);
if (paramType === '[object Request]') {
url = arguments[0].url;
params = {
method: arguments[0].method,
credentials: arguments[0].credentials,
headers: arguments[0].headers,
mode: arguments[0].mode,
redirect: arguments[0].redirect
};
} else {
url = String(url);
}
if (!url.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) {
var method = 'GET';
if (params && params.method !== undefined) {
method = params.method;
}
var stackElement = {
error: false,
url: url,
method: method,
type: 'fetch',
start: new Date()
};
var idx = requestStack.push(stackElement) - 1;
promise.then(function (r) {
stackElement.duration = new Date() - stackElement.start;
stackElement.error = r.status < 200 || r.status >= 400;
stackElement.statusCode = r.status;
stackElement.profile = r.headers.get('x-debug-token');
stackElement.profilerUrl = r.headers.get('x-debug-token-link');
stackElement.toolbarReplaceFinished = false;
stackElement.toolbarReplace = '1' === r.headers.get('Symfony-Debug-Toolbar-Replace');
finishAjaxRequest(idx);
}, function (e){
stackElement.error = true;
finishAjaxRequest(idx);
});
startAjaxRequest(idx);
}
return promise;
};
}
if (window.XMLHttpRequest && XMLHttpRequest.prototype.addEventListener) {
var proxied = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
var self = this;
/* prevent logging AJAX calls to static and inline files, like templates */
var path = url;
if (url.slice(0, 1) === '/') {
if (0 === url.indexOf('{{ request.basePath|e('js') }}')) {
path = url.slice({{ request.basePath|length }});
}
}
else if (0 === url.indexOf('{{ (request.schemeAndHttpHost ~ request.basePath)|e('js') }}')) {
path = url.slice({{ (request.schemeAndHttpHost ~ request.basePath)|length }});
}
if (!path.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) {
var stackElement = {
error: false,
url: url,
method: method,
type: 'xhr',
start: new Date()
};
var idx = requestStack.push(stackElement) - 1;
this.addEventListener('readystatechange', function() {
if (self.readyState == 4) {
stackElement.duration = new Date() - stackElement.start;
stackElement.error = self.status < 200 || self.status >= 400;
stackElement.statusCode = self.status;
extractHeaders(self, stackElement);
finishAjaxRequest(idx);
}
}, false);
startAjaxRequest(idx);
}
proxied.apply(this, Array.prototype.slice.call(arguments));
};
}
{% endif %}
return {
hasClass: hasClass,
removeClass: removeClass,
addClass: addClass,
toggleClass: toggleClass,
getPreference: getPreference,
setPreference: setPreference,
addEventListener: addEventListener,
request: request,
renderAjaxRequests: renderAjaxRequests,
getSfwdt: function(token) {
if (!this.sfwdt) {
this.sfwdt = document.getElementById('sfwdt' + token);
}
return this.sfwdt;
},
load: function(selector, url, onSuccess, onError, options) {
var el = document.getElementById(selector);
if (el && el.getAttribute('data-sfurl') !== url) {
request(
url,
function(xhr) {
el.innerHTML = xhr.responseText;
el.setAttribute('data-sfurl', url);
removeClass(el, 'loading');
var pending = pendingRequests;
for (var i = 0; i < requestStack.length; i++) {
startAjaxRequest(i);
if (requestStack[i].duration) {
finishAjaxRequest(i);
}
}
/* Revert the pending state in case there was a start called without a finish above. */
pendingRequests = pending;
(onSuccess || noop)(xhr, el);
},
function(xhr) { (onError || noop)(xhr, el); },
'',
options
);
}
return this;
},
showToolbar: function(token) {
var sfwdt = this.getSfwdt(token);
removeClass(sfwdt, 'sf-display-none');
if (getPreference('toolbar/displayState') == 'none') {
document.getElementById('sfToolbarMainContent-' + token).style.display = 'none';
document.getElementById('sfToolbarClearer-' + token).style.display = 'none';
document.getElementById('sfMiniToolbar-' + token).style.display = 'block';
} else {
document.getElementById('sfToolbarMainContent-' + token).style.display = 'block';
document.getElementById('sfToolbarClearer-' + token).style.display = 'block';
document.getElementById('sfMiniToolbar-' + token).style.display = 'none';
}
},
hideToolbar: function(token) {
var sfwdt = this.getSfwdt(token);
addClass(sfwdt, 'sf-display-none');
},
initToolbar: function(token) {
this.showToolbar(token);
var hideButton = document.getElementById('sfToolbarHideButton-' + token);
var hideButtonSvg = hideButton.querySelector('svg');
hideButtonSvg.setAttribute('aria-hidden', 'true');
hideButtonSvg.setAttribute('focusable', 'false');
addEventListener(hideButton, 'click', function (event) {
event.preventDefault();
var p = this.parentNode;
p.style.display = 'none';
(p.previousElementSibling || p.previousSibling).style.display = 'none';
document.getElementById('sfMiniToolbar-' + token).style.display = 'block';
setPreference('toolbar/displayState', 'none');
});
var showButton = document.getElementById('sfToolbarMiniToggler-' + token);
var showButtonSvg = showButton.querySelector('svg');
showButtonSvg.setAttribute('aria-hidden', 'true');
showButtonSvg.setAttribute('focusable', 'false');
addEventListener(showButton, 'click', function (event) {
event.preventDefault();
var elem = this.parentNode;
if (elem.style.display == 'none') {
document.getElementById('sfToolbarMainContent-' + token).style.display = 'none';
document.getElementById('sfToolbarClearer-' + token).style.display = 'none';
elem.style.display = 'block';
} else {
document.getElementById('sfToolbarMainContent-' + token).style.display = 'block';
document.getElementById('sfToolbarClearer-' + token).style.display = 'block';
elem.style.display = 'none'
}
setPreference('toolbar/displayState', 'block');
});
},
loadToolbar: function(token, newToken) {
var that = this;
var triesCounter = document.getElementById('sfLoadCounter-' + token);
var options = {
retry: true,
onSend: function (count) {
if (count === 3) {
that.initToolbar(token);
}
if (triesCounter) {
triesCounter.textContent = count;
}
},
};
var cancelButton = document.getElementById('sfLoadCancel-' + token);
if (cancelButton) {
addEventListener(cancelButton, 'click', function (event) {
event.preventDefault();
options.stop = true;
that.hideToolbar(token);
});
}
newToken = (newToken || token);
this.load(
'sfwdt' + token,
'{{ url("_wdt", { "token": "xxxxxx" })|escape('js') }}'.replace(/xxxxxx/, newToken),
function(xhr, el) {
/* Do nothing in the edge case where the toolbar has already been replaced with a new one */
if (!document.getElementById('sfToolbarMainContent-' + newToken)) {
return;
}
/* Evaluate in global scope scripts embedded inside the toolbar */
var i, scripts = [].slice.call(el.querySelectorAll('script'));
for (i = 0; i < scripts.length; ++i) {
eval.call({}, scripts[i].firstChild.nodeValue);
}
el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none';
if (el.style.display == 'none') {
return;
}
that.initToolbar(newToken);
/* Handle toolbar-info position */
var toolbarBlocks = [].slice.call(el.querySelectorAll('.sf-toolbar-block'));
for (i = 0; i < toolbarBlocks.length; ++i) {
toolbarBlocks[i].onmouseover = function () {
var toolbarInfo = this.querySelectorAll('.sf-toolbar-info')[0];
var pageWidth = document.body.clientWidth;
var elementWidth = toolbarInfo.offsetWidth;
var leftValue = (elementWidth + this.offsetLeft) - pageWidth;
var rightValue = (elementWidth + (pageWidth - this.offsetLeft)) - pageWidth;
/* Reset right and left value, useful on window resize */
toolbarInfo.style.right = '';
toolbarInfo.style.left = '';
if (elementWidth > pageWidth) {
toolbarInfo.style.left = 0;
}
else if (leftValue > 0 && rightValue > 0) {
toolbarInfo.style.right = (rightValue * -1) + 'px';
} else if (leftValue < 0) {
toolbarInfo.style.left = 0;
} else {
toolbarInfo.style.right = '0px';
}
};
}
renderAjaxRequests();
addEventListener(document.querySelector('.sf-toolbar-ajax-clear'), 'click', function() {
requestStack = [];
renderAjaxRequests();
successStreak = 4;
document.querySelector('.sf-toolbar-ajax-request-list').innerHTML = '';
});
addEventListener(document.querySelector('.sf-toolbar-block-ajax'), 'mouseenter', function (event) {
var elem = document.querySelector('.sf-toolbar-block-ajax .sf-toolbar-info');
elem.scrollTop = elem.scrollHeight;
});
addEventListener(document.querySelector('.sf-toolbar-block-ajax > .sf-toolbar-icon'), 'click', function (event) {
event.preventDefault();
toggleClass(this.parentNode, 'hover');
});
var dumpInfo = document.querySelector('.sf-toolbar-block-dump .sf-toolbar-info');
if (null !== dumpInfo) {
addEventListener(dumpInfo, 'sfbeforedumpcollapse', function () {
dumpInfo.style.minHeight = dumpInfo.getBoundingClientRect().height+'px';
});
addEventListener(dumpInfo, 'mouseleave', function () {
dumpInfo.style.minHeight = '';
});
}
},
function(xhr) {
if (xhr.status !== 0 && !options.stop) {
var sfwdt = that.getSfwdt(token);
sfwdt.innerHTML = '\
<div class="sf-toolbarreset">\
<div class="sf-toolbar-icon"><svg width="26" height="28" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 26 28" enable-background="new 0 0 26 28" xml:space="preserve"><path fill="#FFFFFF" d="M13 0C5.8 0 0 5.8 0 13c0 7.2 5.8 13 13 13c7.2 0 13-5.8 13-13C26 5.8 20.2 0 13 0z M20 7.5 c-0.6 0-1-0.3-1-0.9c0-0.2 0-0.4 0.2-0.6c0.1-0.3 0.2-0.3 0.2-0.4c0-0.3-0.5-0.4-0.7-0.4c-2 0.1-2.5 2.7-2.9 4.8l-0.2 1.1 c1.1 0.2 1.9 0 2.4-0.3c0.6-0.4-0.2-0.8-0.1-1.3C18 9.2 18.4 9 18.7 8.9c0.5 0 0.8 0.5 0.8 1c0 0.8-1.1 2-3.3 1.9 c-0.3 0-0.5 0-0.7-0.1L15 14.1c-0.4 1.7-0.9 4.1-2.6 6.2c-1.5 1.8-3.1 2.1-3.8 2.1c-1.3 0-2.1-0.6-2.2-1.6c0-0.9 0.8-1.4 1.3-1.4 c0.7 0 1.2 0.5 1.2 1.1c0 0.5-0.2 0.6-0.4 0.7c-0.1 0.1-0.3 0.2-0.3 0.4c0 0.1 0.1 0.3 0.4 0.3c0.5 0 0.9-0.3 1.2-0.5 c1.3-1 1.7-2.9 2.4-6.2l0.1-0.8c0.2-1.1 0.5-2.3 0.8-3.5c-0.9-0.7-1.4-1.5-2.6-1.8c-0.8-0.2-1.3 0-1.7 0.4C8.4 10 8.6 10.7 9 11.1 l0.7 0.7c0.8 0.9 1.3 1.7 1.1 2.7c-0.3 1.6-2.1 2.8-4.3 2.1c-1.9-0.6-2.2-1.9-2-2.7c0.2-0.6 0.7-0.8 1.2-0.6 c0.5 0.2 0.7 0.8 0.6 1.3c0 0.1 0 0.1-0.1 0.3C6 15 5.9 15.2 5.9 15.3c-0.1 0.4 0.4 0.7 0.8 0.8c0.8 0.3 1.7-0.2 1.9-0.9 c0.2-0.6-0.2-1.1-0.4-1.2l-0.8-0.9c-0.4-0.4-1.2-1.5-0.8-2.8c0.2-0.5 0.5-1 0.9-1.4c1-0.7 2-0.8 3-0.6c1.3 0.4 1.9 1.2 2.8 1.9 c0.5-1.3 1.1-2.6 2-3.8c0.9-1 2-1.7 3.3-1.8C20 4.8 21 5.4 21 6.3C21 6.7 20.8 7.5 20 7.5z"/></svg></div>\
An error occurred while loading the web debug toolbar. <a href="{{ url("_profiler_home")|escape('js') }}' + newToken + '>Open the web profiler.</a>\
</div>\
';
sfwdt.setAttribute('class', 'sf-toolbar sf-error-toolbar');
}
},
options
);
return this;
},
toggle: function(selector, elOn, elOff) {
var tmp = elOn.style.display,
el = document.getElementById(selector);
elOn.style.display = elOff.style.display;
elOff.style.display = tmp;
if (el) {
el.style.display = 'none' === tmp ? 'none' : 'block';
}
return this;
},
createTabs: function() {
var tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
/* create the tab navigation for each group of tabs */
for (var i = 0; i < tabGroups.length; i++) {
var tabs = tabGroups[i].querySelectorAll(':scope > .tab');
var tabNavigation = document.createElement('ul');
tabNavigation.className = 'tab-navigation';
var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */
for (var j = 0; j < tabs.length; j++) {
var tabId = 'tab-' + i + '-' + j;
var tabTitle = tabs[j].querySelector('.tab-title').innerHTML;
var tabNavigationItem = document.createElement('li');
tabNavigationItem.setAttribute('data-tab-id', tabId);
if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; }
if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); }
tabNavigationItem.innerHTML = tabTitle;
tabNavigation.appendChild(tabNavigationItem);
var tabContent = tabs[j].querySelector('.tab-content');
tabContent.parentElement.setAttribute('id', tabId);
}
tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild);
addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active');
}
/* display the active tab and add the 'click' event listeners */
for (i = 0; i < tabGroups.length; i++) {
tabNavigation = tabGroups[i].querySelectorAll(':scope > .tab-navigation li');
for (j = 0; j < tabNavigation.length; j++) {
tabId = tabNavigation[j].getAttribute('data-tab-id');
document.getElementById(tabId).querySelector('.tab-title').className = 'hidden';
if (hasClass(tabNavigation[j], 'active')) {
document.getElementById(tabId).className = 'block';
} else {
document.getElementById(tabId).className = 'hidden';
}
tabNavigation[j].addEventListener('click', function(e) {
var activeTab = e.target || e.srcElement;
/* needed because when the tab contains HTML contents, user can click */
/* on any of those elements instead of their parent '<li>' element */
while (activeTab.tagName.toLowerCase() !== 'li') {
activeTab = activeTab.parentNode;
}
/* get the full list of tabs through the parent of the active tab element */
var tabNavigation = activeTab.parentNode.children;
for (var k = 0; k < tabNavigation.length; k++) {
var tabId = tabNavigation[k].getAttribute('data-tab-id');
document.getElementById(tabId).className = 'hidden';
removeClass(tabNavigation[k], 'active');
}
addClass(activeTab, 'active');
var activeTabId = activeTab.getAttribute('data-tab-id');
document.getElementById(activeTabId).className = 'block';
});
}
tabGroups[i].setAttribute('data-processed', 'true');
}
},
createToggles: function() {
var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
for (var i = 0; i < toggles.length; i++) {
var elementSelector = toggles[i].getAttribute('data-toggle-selector');
var element = document.querySelector(elementSelector);
addClass(element, 'sf-toggle-content');
if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') {
addClass(toggles[i], 'sf-toggle-on');
addClass(element, 'sf-toggle-visible');
} else {
addClass(toggles[i], 'sf-toggle-off');
addClass(element, 'sf-toggle-hidden');
}
addEventListener(toggles[i], 'click', function(e) {
e.preventDefault();
if ('' !== window.getSelection().toString()) {
/* Don't do anything on text selection */
return;
}
var toggle = e.target || e.srcElement;
/* needed because when the toggle contains HTML contents, user can click */
/* on any of those elements instead of their parent '.sf-toggle' element */
while (!hasClass(toggle, 'sf-toggle')) {
toggle = toggle.parentNode;
}
var element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
toggleClass(toggle, 'sf-toggle-on');
toggleClass(toggle, 'sf-toggle-off');
toggleClass(element, 'sf-toggle-hidden');
toggleClass(element, 'sf-toggle-visible');
/* the toggle doesn't change its contents when clicking on it */
if (!toggle.hasAttribute('data-toggle-alt-content')) {
return;
}
if (!toggle.hasAttribute('data-toggle-original-content')) {
toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
}
var currentContent = toggle.innerHTML;
var originalContent = toggle.getAttribute('data-toggle-original-content');
var altContent = toggle.getAttribute('data-toggle-alt-content');
toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
});
/* Prevents from disallowing clicks on links inside toggles */
var toggleLinks = toggles[i].querySelectorAll('a');
for (var j = 0; j < toggleLinks.length; j++) {
addEventListener(toggleLinks[j], 'click', function(e) {
e.stopPropagation();
});
}
/* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]');
for (var k = 0; k < copyToClipboardElements.length; k++) {
addEventListener(copyToClipboardElements[k], 'click', function(e) {
e.stopPropagation();
});
}
toggles[i].setAttribute('data-processed', 'true');
}
},
initializeLogsTable: function() {
Sfjs.updateLogsTable();
document.querySelectorAll('.log-filter input').forEach((input) => {
input.addEventListener('change', () => { Sfjs.updateLogsTable(); });
});
document.querySelectorAll('.filter-select-all-or-none button').forEach((link) => {
link.addEventListener('click', () => {
const selectAll = link.classList.contains('select-all');
link.closest('.log-filter-content').querySelectorAll('input').forEach((input) => {
input.checked = selectAll;
});
Sfjs.updateLogsTable();
});
});
document.body.addEventListener('click', (event) => {
document.querySelectorAll('details.log-filter').forEach((filterElement) => {
if (!filterElement.contains(event.target) && filterElement.open) {
filterElement.open = false;
}
});
});
},
updateLogsTable: function() {
const selectedType = document.querySelector('#log-filter-type input:checked').value;
const priorities = document.querySelectorAll('#log-filter-priority input');
const allPriorities = Array.from(priorities).map((input) => input.value);
const selectedPriorities = Array.from(priorities).filter((input) => input.checked).map((input) => input.value);
const channels = document.querySelectorAll('#log-filter-channel input');
const selectedChannels = Array.from(channels).filter((input) => input.checked).map((input) => input.value);
const logs = document.querySelector('table.logs');
if (null === logs) {
return;
}
/* hide rows that don't match the current filters */
let numVisibleRows = 0;
logs.querySelectorAll('tbody tr').forEach((row) => {
if ('all' !== selectedType && selectedType !== row.getAttribute('data-type')) {
row.style.display = 'none';
return;
}
const priority = row.getAttribute('data-priority');
if (false === selectedPriorities.includes(priority) && true === allPriorities.includes(priority)) {
row.style.display = 'none';
return;
}
if ('' !== row.getAttribute('data-channel') && false === selectedChannels.includes(row.getAttribute('data-channel'))) {
row.style.display = 'none';
return;
}
row.style.display = 'table-row';
numVisibleRows++;
});
document.querySelector('table.logs').style.display = 0 === numVisibleRows ? 'none' : 'table';
document.querySelector('.no-logs-message').style.display = 0 === numVisibleRows ? 'block' : 'none';
/* update the selected totals of all filters */
document.querySelector('#log-filter-priority .filter-active-num').innerText = (priorities.length === selectedPriorities.length) ? 'All' : selectedPriorities.length;
document.querySelector('#log-filter-channel .filter-active-num').innerText = (channels.length === selectedChannels.length) ? 'All' : selectedChannels.length;
/* update the currently selected "log type" tab */
document.querySelectorAll('#log-filter-type li').forEach((tab) => tab.classList.remove('active'));
document.querySelector(`#log-filter-type input[value="${selectedType}"]`).parentElement.classList.add('active');
},
};
})();
Sfjs.addEventListener(document, 'DOMContentLoaded', function() {
Sfjs.createTabs();
Sfjs.createToggles();
});
}
/*]]>*/</script>

View File

@ -0,0 +1,25 @@
{% block toolbar %}
{% set icon %}
{{ include('@WebProfiler/Icon/symfony.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">
Loading&hellip;
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Loading the web debug toolbar&hellip;</b>
</div>
<div class="sf-toolbar-info-piece">
Attempt #<span id="sfLoadCounter-{{ token }}"></span>
</div>
<div class="sf-toolbar-info-piece">
<b>
<button class="sf-cancel-button" type="button" id="sfLoadCancel-{{ token }}" title="Cancel loading">Cancel</button>
</b>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}

View File

@ -0,0 +1,14 @@
<div id="header">
<div class="container">
<h1>{{ include('@WebProfiler/Icon/symfony.svg') }} Symfony <span>Profiler</span></h1>
<div class="search">
<form method="get" action="https://symfony.com/search" target="_blank">
<div class="form-row">
<input name="q" id="search-id" type="search" placeholder="search on symfony.com">
<button type="submit" class="btn">Search</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% set messages = {
'no_token' : {
status: 'error',
title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found',
message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.'
}
} %}
{% block summary %}
<div class="status status-{{ messages[about].status }}">
<div class="container">
<h2>{{ messages[about].status|title }}</h2>
</div>
</div>
{% endblock %}
{% block panel %}
<h2>{{ messages[about].title }}</h2>
<p>{{ messages[about].message }}</p>
{% endblock %}

View File

@ -0,0 +1,153 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block body %}
{{ include('@WebProfiler/Profiler/header.html.twig', with_context = false) }}
<div id="summary">
{% block summary %}
{% if profile is defined %}
{% set request_collector = profile.collectors.request|default(false) %}
{% set status_code = request_collector ? request_collector.statuscode|default(0) : 0 %}
{% set css_class = status_code > 399 ? 'status-error' : status_code > 299 ? 'status-warning' : 'status-success' %}
<div class="status {{ css_class }}">
<div class="container">
<h2 class="break-long-words">
{% if profile.method|upper in ['GET', 'HEAD'] %}
<a href="{{ profile.url }}">{{ profile.url }}</a>
{% else %}
{{ profile.url }}
{% set referer = request_collector ? request_collector.requestheaders.get('referer') : null %}
{% if referer %}
<a href="{{ referer }}" class="referer">Return to referer URL</a>
{% endif %}
{% endif %}
</h2>
{% if request_collector and request_collector.redirect -%}
{%- set redirect = request_collector.redirect -%}
{%- set controller = redirect.controller -%}
{%- set redirect_route = '@' ~ redirect.route %}
<dl class="metadata">
<dt>
<span class="label">{{ redirect.status_code }}</span>
Redirect from
</dt>
<dd>
{{ 'GET' != redirect.method ? redirect.method }}
{% if redirect.controller.class is defined -%}
{%- set link = controller.file|file_link(controller.line) -%}
{% if link %}<a href="{{ link }}" title="{{ controller.file }}">{% endif -%}
{{ redirect_route }}
{%- if link %}</a>{% endif -%}
{%- else -%}
{{ redirect_route }}
{%- endif %}
(<a href="{{ path('_profiler', { token: redirect.token, panel: request.query.get('panel', 'request') }) }}">{{ redirect.token }}</a>)
</dd>
</dl>
{%- endif %}
{% if request_collector and request_collector.forwardtoken -%}
{% set forward_profile = profile.childByToken(request_collector.forwardtoken) %}
{% set controller = forward_profile ? forward_profile.collector('request').controller : 'n/a' %}
<dl class="metadata">
<dt>Forwarded to</dt>
<dd>
{% set link = controller.file is defined ? controller.file|file_link(controller.line) : null -%}
{%- if link %}<a href="{{ link }}" title="{{ controller.file }}">{% endif -%}
{% if controller.class is defined %}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{% else %}
{{- controller -}}
{% endif %}
{%- if link %}</a>{% endif %}
(<a href="{{ path('_profiler', { token: request_collector.forwardtoken }) }}">{{ request_collector.forwardtoken }}</a>)
</dd>
</dl>
{%- endif %}
<dl class="metadata">
<dt>Method</dt>
<dd>{{ profile.method|upper }}</dd>
<dt>HTTP Status</dt>
<dd>{{ status_code }}</dd>
<dt>IP</dt>
<dd>
<a href="{{ path('_profiler_search_results', { token: token, limit: 10, ip: profile.ip }) }}">{{ profile.ip }}</a>
</dd>
<dt>Profiled on</dt>
<dd><time datetime="{{ profile.time|date('c') }}">{{ profile.time|date('r') }}</time></dd>
<dt>Token</dt>
<dd>{{ profile.token }}</dd>
</dl>
</div>
</div>
{% endif %}
{% endblock %}
</div>
<div id="content" class="container">
<div id="main">
<div id="sidebar">
<div id="sidebar-shortcuts">
<div class="shortcuts">
<a href="#" id="sidebarShortcutsMenu" class="visible-small">
<span class="icon">{{ include('@WebProfiler/Icon/menu.svg') }}</span>
</a>
<a class="btn btn-sm" href="{{ path('_profiler_search', { limit: 10 }) }}">Last 10</a>
<a class="btn btn-sm" href="{{ path('_profiler', { token: 'latest' }|merge(request.query.all)) }}">Latest</a>
<a class="sf-toggle btn btn-sm" data-toggle-selector="#sidebar-search" {% if tokens is defined or about is defined %}data-toggle-initial="display"{% endif %}>
{{ include('@WebProfiler/Icon/search.svg') }} <span class="hidden-small">Search</span>
</a>
{{ render(controller('web_profiler.controller.profiler::searchBarAction', request.query.all)) }}
</div>
</div>
{% if templates is defined %}
<ul id="menu-profiler">
{% for name, template in templates %}
{% set menu -%}
{%- if block('menu', template) is defined -%}
{% with { collector: profile.getcollector(name), profiler_markup_version: profiler_markup_version } %}
{{- block('menu', template) -}}
{% endwith %}
{%- endif -%}
{%- endset %}
{% if menu is not empty %}
<li class="{{ name }} {{ name == panel ? 'selected' }}">
<a href="{{ path('_profiler', { token: token, panel: name }) }}">{{ menu|raw }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{{ include('@WebProfiler/Profiler/settings.html.twig') }}
</div>
<div id="collector-wrapper">
<div id="collector-content">
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
{% block panel '' %}
</div>
</div>
</div>
</div>
<script>
(function () {
Sfjs.addEventListener(document.getElementById('sidebarShortcutsMenu'), 'click', function (event) {
event.preventDefault();
Sfjs.toggleClass(document.getElementById('sidebar'), 'expanded');
})
}());
</script>
{% endblock %}

View File

@ -0,0 +1,79 @@
{# Mixins
========================================================================= #}
{% set mixins = {
'break_long_words': '-ms-word-break: break-all; word-break: break-all; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto;',
'monospace_font': 'font-family: monospace; font-size: 13px; font-size-adjust: 0.5;',
'sans_serif_font': 'font-family: Helvetica, Arial, sans-serif;',
'subtle_border_and_shadow': 'background: #FFF; border: 1px solid #E0E0E0; box-shadow: 0px 0px 1px rgba(128, 128, 128, .2);'
} %}
{# Normalization
(normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css)
========================================================================= #}
html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}
{# Basic styles
========================================================================= #}
html, body {
height: 100%;
width: 100%;
}
body {
background-color: #F9F9F9;
color: #aaa;
display: flex;
flex-direction: column;
{{ mixins.sans_serif_font|raw }}
font-size: 14px;
line-height: 1.4;
}
.header {
background-color: #222;
position: fixed;
top: 0;
display: flex;
width: 100%;
}
.header h1 {
color: #FFF;
font-weight: normal;
font-size: 21px;
margin: 0;
padding: 10px 10px 8px;
word-break: break-all;
}
a.doc {
color: #FFF;
text-decoration: none;
margin: auto;
margin-right: 10px;
}
a.doc:hover {
text-decoration: underline;
}
.empty {
padding: 10px;
color: #555;
}
.source {
margin-top: 41px;
}
.source li code {
color: #555;
}
.source li.selected {
background: rgba(255, 255, 153, 0.5);
}
.anchor {
position: relative;
display: inline-block;
top: -7em;
visibility: hidden;
}

View File

@ -0,0 +1,22 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block head %}
<style>
{{ include('@WebProfiler/Profiler/open.css.twig') }}
</style>
{% endblock %}
{% block body %}
{% set source = filename|file_excerpt(line, -1) %}
<div class="header">
<h1>{{ file }}{% if 0 < line %} <small>line {{ line }}</small>{% endif %}</h1>
<a class="doc" href="https://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}/reference/configuration/framework.html#ide" rel="help">Open in your IDE?</a>
</div>
<div class="source">
{% if source is null %}
<p class="empty">The file is not readable.</p>
{% else %}
{{ source|raw }}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,1477 @@
{# This file is partially duplicated in TwigBundle/Resources/views/exceotion.css.twig.
If you make any change in this file, verify the same change is needed in the other file. #}
{# Normalization
(normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css)
========================================================================= #}
*{-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}
:root {
--font-sans-serif: Helvetica, Arial, sans-serif;
--page-background: #f9f9f9;
--color-text: #222;
--color-muted: #999;
--color-link: #218BC3;
/* when updating any of these colors, do the same in toolbar.css.twig */
--color-success: #4f805d;
--color-warning: #a46a1f;
--color-error: #b0413e;
--badge-background: #f5f5f5;
--badge-color: #666;
--badge-warning-background: #FEF3C7;
--badge-warning-color: #B45309;
--badge-danger-background: #FEE2E2;
--badge-danger-color: #B91C1C;
--tab-background: #fff;
--tab-color: #444;
--tab-active-background: #666;
--tab-active-color: #fafafa;
--tab-disabled-background: #f5f5f5;
--tab-disabled-color: #999;
--log-filter-button-background: #fff;
--log-filter-button-border: #999;
--log-filter-button-color: #555;
--log-filter-active-num-color: #2563EB;
--log-timestamp-color: #555;
--metric-value-background: #fff;
--metric-value-color: inherit;
--metric-unit-color: #999;
--metric-label-background: #e0e0e0;
--metric-label-color: inherit;
--trace-selected-background: #F7E5A1;
--table-border: #e0e0e0;
--table-background: #fff;
--table-header: #e0e0e0;
--info-background: #ddf;
--tree-active-background: #F7E5A1;
--exception-title-color: var(--base-2);
--shadow: 0px 0px 1px rgba(128, 128, 128, .2);
--border: 1px solid #e0e0e0;
--background-error: var(--color-error);
--highlight-comment: #969896;
--highlight-default: #222222;
--highlight-keyword: #a71d5d;
--highlight-string: #183691;
--base-0: #fff;
--base-1: #f5f5f5;
--base-2: #e0e0e0;
--base-3: #ccc;
--base-4: #666;
--base-5: #444;
--base-6: #222;
--card-label-background: #eee;
--card-label-color: var(--base-6);
}
.theme-dark {
--page-background: #36393e;
--color-text: #e0e0e0;
--color-muted: #777;
--color-link: #93C5FD;
--color-error: #d43934;
--badge-background: #555;
--badge-color: #ddd;
--badge-warning-background: #B45309;
--badge-warning-color: #FEF3C7;
--badge-danger-background: #B91C1C;
--badge-danger-color: #FEE2E2;
--tab-background: #555;
--tab-color: #ccc;
--tab-active-background: #888;
--tab-active-color: #fafafa;
--tab-disabled-background: var(--page-background);
--tab-disabled-color: #777;
--log-filter-button-background: #555;
--log-filter-button-border: #999;
--log-filter-button-color: #ccc;
--log-filter-active-num-color: #93C5FD;
--log-timestamp-color: #ccc;
--metric-value-background: #555;
--metric-value-color: inherit;
--metric-unit-color: #999;
--metric-label-background: #777;
--metric-label-color: #e0e0e0;
--trace-selected-background: #71663acc;
--table-border: #444;
--table-background: #333;
--table-header: #555;
--info-background: rgba(79, 148, 195, 0.5);
--tree-active-background: var(--metric-label-background);
--exception-title-color: var(--base-2);
--shadow: 0px 0px 1px rgba(32, 32, 32, .2);
--border: 1px solid #666;
--background-error: #b0413e;
--highlight-comment: #dedede;
--highlight-default: var(--base-6);
--highlight-keyword: #ff413c;
--highlight-string: #70a6fd;
--base-0: #2e3136;
--base-1: #444;
--base-2: #666;
--base-3: #666;
--base-4: #666;
--base-5: #e0e0e0;
--base-6: #f5f5f5;
--card-label-background: var(--tab-active-background);
--card-label-color: var(--tab-active-color);
}
{# Basic styles
========================================================================= #}
html, body {
height: 100%;
width: 100%;
}
body {
background-color: var(--page-background);
color: var(--base-6);
display: flex;
flex-direction: column;
font-family: var(--font-sans-serif);
font-size: 14px;
line-height: 1.4;
}
h2, h3, h4 {
font-weight: 500;
margin: 1.5em 0 .5em;
}
h2 + h3,
h3 + h4 {
margin-top: 1em;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 21px;
}
h4 {
font-size: 18px;
}
h2 span, h3 span, h4 span,
h2 small, h3 small, h4 small {
color: var(--color-muted);
}
li {
margin-bottom: 10px;
}
p {
font-size: 16px;
margin-bottom: 1em;
}
a {
color: var(--color-link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a.link-inverse {
text-decoration: underline;
}
a.link-inverse:hover {
text-decoration: none;
}
a:active,
a:hover {
outline: 0;
}
h2 a,
h3 a,
h4 a {
text-decoration: underline;
}
h2 a:hover,
h3 a:hover,
h4 a:hover {
text-decoration: none;
}
abbr {
border-bottom: 1px dotted var(--base-5);
cursor: help;
}
code, pre {
font-family: monospace;
font-size: 13px;
}
{# Buttons (the colors of this element don't change based on the selected theme)
------------------------------------------------------------------------- #}
button {
font-family: var(--font-sans-serif);
}
.btn {
background: #777;
border-radius: 2px;
border: 0;
color: #f5f5f5;
display: inline-block;
padding: .5em .75em;
}
.btn:hover {
cursor: pointer;
opacity: 0.8;
text-decoration: none;
}
.btn-sm {
font-size: 12px;
}
.btn-sm svg {
height: 16px;
width: 16px;
vertical-align: middle;
}
.btn-link {
border-color: transparent;
color: var(--color-link);
text-decoration: none;
background-color: transparent;
outline: none;
border: 0;
padding: 0;
cursor: pointer;
}
.btn-link:hover {
text-decoration: underline;
}
{# Tables
------------------------------------------------------------------------- #}
table, tr, th, td {
background: var(--table-background);
border-collapse: collapse;
line-height: 1.5;
vertical-align: top;
}
table {
background: var(--base-0);
border: var(--border);
box-shadow: var(--shadow);
margin: 1em 0;
width: 100%;
}
table th, table td {
padding: 8px 10px;
}
table th {
font-weight: bold;
text-align: left;
}
table thead th {
background-color: var(--table-header);
}
table thead th.key {
width: 19%;
}
table thead.small th {
font-size: 12px;
padding: 4px 10px;
}
table tbody th,
table tbody td {
border: 1px solid var(--base-2);
border-width: 1px 0;
font-family: monospace;
font-size: 13px;
}
table tbody div {
margin: .25em 0;
}
table tbody ul {
margin: 0;
padding: 0 0 0 1em;
}
table thead th.num-col,
table tbody td.num-col {
text-align: center;
}
{# Utility classes
========================================================================= #}
.block {
display: block;
}
.full-width {
width: 100%;
}
.hidden {
display: none;
}
.nowrap {
white-space: pre;
}
.prewrap {
white-space: pre-wrap;
}
.newline {
display: block;
}
.break-long-words {
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
.text-small {
font-size: 12px !important;
}
.text-muted {
color: var(--color-muted);
}
.text-danger {
color: var(--color-error);
}
.text-bold {
font-weight: bold;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.font-normal {
font-family: var(--font-sans-serif);
font-size: 14px;
}
.help {
color: var(--color-muted);
font-size: 14px;
margin-bottom: .5em;
}
.empty {
border: 4px dashed var(--base-2);
color: var(--color-muted);
margin: 1em 0;
padding: .5em 2em;
}
.label {
background-color: var(--base-4);
color: #FAFAFA;
display: inline-block;
font-size: 12px;
font-weight: bold;
padding: 3px 7px;
white-space: nowrap;
}
.label.same-width {
min-width: 70px;
text-align: center;
}
.label.status-success { background: var(--color-success); color: #FFF; }
.label.status-warning { background: var(--color-warning); color: #FFF; }
.label.status-error { background: var(--background-error); color: #FFF; }
{# Metrics
------------------------------------------------------------------------- #}
.metrics {
margin: 1em 0 0;
overflow: auto;
}
.metrics .metric {
float: left;
margin: 0 1em 1em 0;
}
.metric {
background: var(--metric-value-background);
border: 1px solid var(--table-border);
box-shadow: var(--shadow);
color: var(--metric-value-color);
min-width: 100px;
min-height: 70px;
}
.metric .value {
display: block;
font-size: 28px;
padding: 8px 15px 4px;
text-align: center;
}
.metric .value svg {
margin: 5px 0 -5px;
}
.metric .unit {
color: var(--metric-unit-color);
font-size: 18px;
margin-left: -4px;
}
.metric .label {
background: var(--metric-label-background);
color: var(--metric-label-color);
display: block;
font-size: 12px;
padding: 5px;
text-align: center;
}
.metrics-horizontal .metric {
min-height: 0;
min-width: 0;
}
.metrics-horizontal .metric .value,
.metrics-horizontal .metric .label {
display: inline;
padding: 2px 6px;
}
.metrics-horizontal .metric .label {
display: inline-block;
padding: 6px;
}
.metrics-horizontal .metric .value {
font-size: 16px;
}
.metrics-horizontal .metric .value svg {
max-height: 14px;
line-height: 10px;
margin: 0;
padding-left: 4px;
vertical-align: middle;
}
.metric-divider {
float: left;
margin: 0 1em;
min-height: 1px; {# required to apply 'margin' to an empty 'div' #}
}
{# Cards
------------------------------------------------------------------------- #}
.card {
background: var(--base-0);
border: var(--border);
box-shadow: var(--shadow);
margin: 1em 0;
padding: 10px;
}
.card-block + .card-block {
border-top: 1px solid var(--base-2);
padding-top: 10px;
}
.card *:first-child,
.card-block *:first-child {
margin-top: 0;
}
.card .label {
background-color: var(--card-label-background);
color: var(--card-label-color);
}
{# Status
------------------------------------------------------------------------- #}
.status-success {
background: rgba(94, 151, 110, 0.3);
}
.status-warning {
background: rgba(240, 181, 24, 0.3);
}
.status-error {
background: rgba(176, 65, 62, 0.2);
}
.status-success td,
.status-warning td,
.status-error td {
background: transparent;
}
tr.status-error td,
tr.status-warning td {
border-bottom: 1px solid var(--base-2);
border-top: 1px solid var(--base-2);
}
.status-warning .colored {
color: var(--color-warning);
}
.status-error .colored {
color: var(--color-error);
}
{# Syntax highlighting
========================================================================= #}
.highlight pre {
margin: 0;
white-space: pre-wrap;
}
.highlight .keyword { color: #8959A8; font-weight: bold; }
.highlight .word { color: var(--color-text); }
.highlight .variable { color: #916319; }
.highlight .symbol { color: var(--color-text); }
.highlight .comment { color: #999999; }
.highlight .backtick { color: #718C00; }
.highlight .string { color: #718C00; }
.highlight .number { color: #F5871F; font-weight: bold; }
.highlight .error { color: #C82829; }
{# Icons
========================================================================= #}
.sf-icon {
vertical-align: middle;
background-repeat: no-repeat;
background-size: contain;
width: 16px;
height: 16px;
display: inline-block;
}
.sf-icon svg {
width: 16px;
height: 16px;
}
.sf-icon.sf-medium,
.sf-icon.sf-medium svg {
width: 24px;
height: 24px;
}
.sf-icon.sf-large,
.sf-icon.sf-large svg {
width: 32px;
height: 32px;
}
{# Layout
========================================================================= #}
.container {
max-width: 1300px;
padding-right: 15px;
}
#header {
flex: 0 0 auto;
}
#header .container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
#summary {
flex: 0 0 auto;
}
#content {
height: 100%;
}
#main {
display: flex;
flex-direction: row;
min-height: 100%;
}
#sidebar {
flex: 0 0 220px;
}
#collector-wrapper {
flex: 0 1 100%;
min-width: 0;
}
#collector-content {
margin: 0 0 30px 0;
padding: 14px 0 14px 20px;
}
#main h2:first-of-type {
margin-top: 0;
}
{# Header (the colors of this element don't change based on the selected theme)
========================================================================= #}
#header {
background-color: #222;
overflow: hidden;
}
#header h1 {
color: #fff;
flex: 1;
font-weight: normal;
font-size: 21px;
margin: 0;
padding: 10px 10px 8px;
}
#header h1 span {
color: #ccc;
}
#header h1 svg {
height: 40px;
width: 40px;
margin-top: -4px;
vertical-align: middle;
}
#header h1 svg path,
#header h1 svg .sf-svg-path {
fill: #fff;
}
#header .search {
padding-top: 11px;
}
#header .search input {
border: 1px solid #ddd;
margin-right: 4px;
padding: 7px 8px;
width: 200px;
}
{# Summary
========================================================================= #}
#summary .status {
background: var(--base-2);
border: solid rgba(0, 0, 0, 0.1);
border-width: 2px 0;
padding: 10px;
}
#summary h2,
#summary h2 a {
color: var(--base-6);
font-size: 21px;
margin: 0;
text-decoration: none;
vertical-align: middle;
}
#summary h2 a:hover {
text-decoration: underline;
}
#summary h2 a.referer {
margin-left: .5em;
font-size: 75%;
color: rgba(255, 255, 255, 0.5);
}
#summary h2 a.referer:before {
content: '\1F503\00a0';
}
#summary .status-success { background: var(--color-success); }
#summary .status-warning { background: var(--color-warning); }
#summary .status-error { background: var(--background-error); }
#summary .status-success h2,
#summary .status-success a,
#summary .status-warning h2,
#summary .status-warning a,
#summary .status-error h2,
#summary .status-error a {
color: #FFF;
}
#summary dl.metadata,
#summary dl.metadata a {
margin: 5px 0 0;
color: rgba(255, 255, 255, 0.75);
}
#summary dl.metadata dt,
#summary dl.metadata dd {
display: inline-block;
font-size: 13px;
}
#summary dl.metadata dt {
font-weight: bold;
}
#summary dl.metadata dt:after {
content: ':';
}
#summary dl.metadata dd {
margin: 0 1.5em 0 0;
}
#summary dl.metadata .label {
background: rgba(255, 255, 255, 0.2);
}
{# Sidebar
========================================================================= #}
#sidebar {
background: #444;
color: #ccc;
padding-bottom: 30px;
position: relative;
width: 220px;
z-index: 9999;
}
#sidebar .module {
padding: 10px;
width: 220px;
}
{# Sidebar Shortcuts
------------------------------------------------------------------------- #}
#sidebar #sidebar-shortcuts {
background: #333;
width: 220px;
}
#sidebar #sidebar-shortcuts .shortcuts {
position: relative;
padding: 16px 10px;
}
#sidebar-shortcuts .icon {
display: block;
float: left;
width: 50px;
margin: 2px 0 0 -10px;
text-align: center;
}
#sidebar #sidebar-shortcuts .btn {
color: #f5f5f5;
}
#sidebar #sidebar-shortcuts .btn + .btn {
margin-left: 5px;
}
#sidebar #sidebar-shortcuts .btn {
padding: .5em;
}
{# Sidebar Search (the colors of this element don't change based on the selected theme)
------------------------------------------------------------------------- #}
#sidebar-search .form-group:first-of-type {
padding-top: 20px;
}
#sidebar-search .form-group {
clear: both;
overflow: hidden;
padding-bottom: 10px;
}
#sidebar-search .form-group label {
float: left;
font-size: 13px;
line-height: 24px;
width: 60px;
}
#sidebar-search .form-group input,
#sidebar-search .form-group select {
float: left;
font-size: 13px;
padding: 3px 6px;
}
#sidebar-search .form-group input {
background: #ccc;
border: 1px solid var(--color-muted);
color: #222;
width: 120px;
}
#sidebar-search .form-group select {
color: #222;
}
#sidebar-search .form-group .btn {
float: right;
margin-right: 10px;
}
{# Sidebar Menu (the colors of this element don't change based on the selected theme)
------------------------------------------------------------------------- #}
#menu-profiler {
margin: 0;
padding: 0;
list-style-type: none;
}
#menu-profiler li {
position: relative;
margin-bottom: 0;
}
#menu-profiler li a {
border: solid transparent;
border-width: 2px 0;
color: var(--base-3);
display: block;
}
#menu-profiler li a:hover {
text-decoration: none;
}
#menu-profiler li a .label {
background: transparent;
color: #EEE;
display: block;
padding: 8px 10px 8px 50px;
overflow: hidden;
white-space: nowrap;
}
#menu-profiler li a .label .icon {
display: block;
position: absolute;
left: 0;
top: 8px;
width: 50px;
text-align: center;
}
#menu-profiler .label .icon img,
#menu-profiler .label .icon svg {
height: 24px;
max-width: 24px;
}
#menu-profiler li a .label .icon svg path,
#menu-profiler li a .label .icon svg .sf-svg-path {
fill: #DDD;
}
#menu-profiler li a .label strong {
font-size: 16px;
font-weight: normal;
}
#menu-profiler li a .label.disabled {
opacity: .25;
}
#menu-profiler li a:hover .label.disabled,
#menu-profiler li.selected a .label.disabled {
opacity: 1;
}
#menu-profiler li.selected a,
#menu-profiler:hover li.selected a:hover,
#menu-profiler li a:hover {
background: #666;
border: solid #555;
border-width: 2px 0;
}
#menu-profiler li.selected a .label,
#menu-profiler li a:hover .label {
color: #FFF;
}
#menu-profiler li.selected a .icon svg path,
#menu-profiler li.selected a .icon svg .sf-svg-path,
#menu-profiler li a:hover .icon svg path,
#menu-profiler li a:hover .icon svg .sf-svg-path {
fill: #fff;
}
#menu-profiler li a .count {
background-color: #666;
color: #fff;
display: inline-block;
font-weight: bold;
min-width: 10px;
padding: 2px 6px;
position: absolute;
right: 10px;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}
#menu-profiler li a span.count span {
font-size: 12px;
}
#menu-profiler li a span.count span + span::before {
content: " / ";
color: #AAA;
}
#menu-profiler .label-status-warning .count {
background: var(--color-warning);
}
#menu-profiler .label-status-error .count {
background: var(--background-error);
}
{# Timeline panel
========================================================================= #}
#timeline-control {
background: var(--table-background);
box-shadow: var(--shadow);
margin: 1em 0;
padding: 10px;
}
#timeline-control label {
font-weight: bold;
margin-right: 1em;
}
#timeline-control input {
background: var(--metric-value-background);
border: 1px solid var(--table-border);
font-size: 16px;
padding: 4px;
text-align: right;
width: 5em;
}
#timeline-control .help {
margin-left: 1em;
}
.sf-profiler-timeline .legends {
font-size: 12px;
line-height: 1.5em;
}
.sf-profiler-timeline + p.help {
margin-top: 0;
}
{# Tabbed navigation
========================================================================= #}
.tab-navigation {
margin: 0 0 1em 0;
padding: 0;
}
.tab-navigation li {
background: var(--tab-background);
border: 1px solid var(--table-border);
color: var(--tab-color);
cursor: pointer;
display: inline-block;
font-size: 16px;
margin: 0 0 0 -1px;
padding: .5em .75em;
z-index: 1;
}
.tab-navigation li .badge {
background-color: var(--base-1);
color: var(--base-4);
display: inline-block;
font-size: 14px;
font-weight: bold;
margin-left: 8px;
min-width: 10px;
padding: 1px 6px;
text-align: center;
white-space: nowrap;
}
.tab-navigation li.disabled {
background: var(--tab-disabled-background);
color: var(--tab-disabled-color);
}
.tab-navigation li.active {
background: var(--tab-active-background);
color: var(--tab-active-color);
z-index: 1100;
}
.tab-navigation li.active .badge {
background-color: var(--base-5);
color: var(--base-2);
}
.tab-content > *:first-child {
margin-top: 0;
}
.tab-navigation li .badge.status-warning { background: var(--color-warning); color: #FFF; }
.tab-navigation li .badge.status-error { background: var(--background-error); color: #FFF; }
.sf-tabs .tab:not(:first-child) { display: none; }
{# Toggles
========================================================================= #}
.sf-toggle-content {
-moz-transition: display .25s ease;
-webkit-transition: display .25s ease;
transition: display .25s ease;
}
.sf-toggle-content.sf-toggle-hidden {
display: none;
}
.sf-toggle-content.sf-toggle-visible {
display: block;
}
{# Filters
========================================================================= #}
[data-filters] { position: relative; }
[data-filtered] { cursor: pointer; }
[data-filtered]:after { content: '\00a0\25BE'; }
[data-filtered]:hover .filter-list li { display: inline-flex; }
[class*="filter-hidden-"] { display: none; }
.filter-list { position: absolute; border: var(--border); box-shadow: var(--shadow); margin: 0; padding: 0; display: flex; flex-direction: column; }
.filter-list :after { content: ''; }
.filter-list li {
background: var(--tab-disabled-background);
border-bottom: var(--border);
color: var(--tab-disabled-color);
display: none;
list-style: none;
margin: 0;
padding: 5px 10px;
text-align: left;
font-weight: normal;
}
.filter-list li.active {
background: var(--tab-background);
color: var(--tab-color);
}
.filter-list li.last-active {
background: var(--tab-active-background);
color: var(--tab-active-color);
}
.filter-list-level li { cursor: s-resize; }
.filter-list-level li.active { cursor: n-resize; }
.filter-list-level li.last-active { cursor: default; }
.filter-list-level li.last-active:before { content: '\2714\00a0'; }
.filter-list-choice li:before { content: '\2714\00a0'; color: transparent; }
.filter-list-choice li.active:before { color: unset; }
{# Twig panel
========================================================================= #}
#twig-dump pre {
font-size: 12px;
line-height: 1.7;
background-color: var(--base-0);
border: var(--border);
padding: 15px;
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
}
#twig-dump span {
border-radius: 2px;
padding: 1px 2px;
}
#twig-dump .status-error { background: transparent; color: var(--color-error); }
#twig-dump .status-warning { background: rgba(240, 181, 24, 0.3); }
#twig-dump .status-success { background: rgba(100, 189, 99, 0.2); }
#twig-dump .status-info { background: var(--info-background); }
#twig-table tbody td {
vertical-align: middle;
}
#twig-table tbody td > a {
margin-left: -5px;
}
#twig-table tbody td div {
margin: 0;
}
.icon-twig {
vertical-align: text-bottom;
}
.icon-twig svg path {
fill: #7eea12;
}
{# Logger panel
========================================================================= #}
.badge {
background: var(--badge-background);
border-radius: 4px;
color: var(--badge-color);
font-size: 12px;
font-weight: bold;
padding: 1px 4px;
}
.badge-warning {
background: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.log-filters {
display: flex;
}
.log-filters .log-filter {
position: relative;
}
.log-filters .log-filter + .log-filter {
margin-left: 15px;
}
.log-filters .log-filter summary {
align-items: center;
background: var(--log-filter-button-background);
border-radius: 2px;
border: 1px solid var(--log-filter-button-border);
color: var(--log-filter-button-color);
cursor: pointer;
display: flex;
padding: 5px 8px;
}
.log-filters .log-filter summary .icon {
height: 18px;
width: 18px;
margin: 0 7px 0 0;
}
.log-filters .log-filter summary svg {
height: 18px;
width: 18px;
opacity: 0.7;
}
.log-filters .log-filter summary .filter-active-num {
color: var(--log-filter-active-num-color);
font-weight: bold;
padding: 0 1px;
}
.log-filter .tab-navigation {
margin-bottom: 0;
}
.log-filter .tab-navigation li:first-child {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
.log-filter .tab-navigation li:last-child {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.log-filter .tab-navigation li {
border-color: var(--log-filter-button-border);
padding: 0;
}
.log-filter .tab-navigation li + li {
margin-left: -5px;
}
.log-filter .tab-navigation li .badge {
font-size: 13px;
padding: 0 6px;
}
.log-filter .tab-navigation li input {
display: none;
}
.log-filter .tab-navigation li label {
align-items: center;
cursor: pointer;
padding: 5px 10px;
display: inline-flex;
font-size: 14px;
}
.log-filters .log-filter .log-filter-content {
background: var(--base-0);
border: 1px solid var(--table-border);
border-radius: 2px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
padding: 15px;
position: absolute;
left: 0;
top: 36px;
max-width: 400px;
min-width: 200px;
z-index: 9999;
}
.log-filters .log-filter .log-filter-content .log-filter-option {
align-items: center;
display: flex;
}
.log-filter .filter-select-all-or-none {
margin-bottom: 10px;
}
.log-filter .filter-select-all-or-none button + button {
margin-left: 15px;
}
.log-filters .log-filter .log-filter-content .log-filter-option + .log-filter-option {
margin: 7px 0 0;
}
.log-filters .log-filter .log-filter-content .log-filter-option label {
cursor: pointer;
flex: 1;
padding-left: 10px;
}
table.logs .metadata {
display: block;
font-size: 12px;
}
.theme-dark tr.status-error td,
.theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; }
table.logs .log-timestamp {
color: var(--log-timestamp-color);
}
table.logs .log-metadata {
margin: 8px 0 0;
}
table.logs .log-metadata > span {
display: inline-block;
}
table.logs .log-metadata > span + span {
margin-left: 10px;
}
table.logs .log-metadata .log-channel {
color: var(--base-1);
font-size: 13px;
font-weight: bold;
}
table.logs .log-metadata .log-num-occurrences {
color: var(--color-muted);
font-size: 13px;
}
.log-type-badge {
display: inline-block;
font-family: var(--font-sans-serif);
margin-top: 5px;
}
.log-type-badge.badge-deprecation {
background: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.log-type-badge.badge-error {
background: var(--badge-danger-background);
color: var(--badge-danger-color);
}
.log-type-badge.badge-silenced {
background: #EDE9FE;
color: #6D28D9;
}
.theme-dark .log-type-badge.badge-silenced {
background: #5B21B6;
color: #EDE9FE;
}
tr.log-status-warning {
border-left: 4px solid #F59E0B;
}
tr.log-status-error {
border-left: 4px solid #EF4444;
}
tr.log-status-silenced {
border-left: 4px solid #A78BFA;
}
.container-compilation-logs {
background: var(--table-background);
border: 1px solid var(--base-2);
margin-top: 30px;
padding: 15px;
}
.container-compilation-logs summary {
cursor: pointer;
}
.container-compilation-logs summary h4 {
margin: 0 0 5px;
}
.container-compilation-logs summary p {
margin: 0;
}
{# Doctrine panel
========================================================================= #}
.sql-runnable {
background: var(--base-1);
margin: .5em 0;
padding: 1em;
}
.sql-explain {
overflow-x: auto;
max-width: 920px;
}
.sql-explain table td, .sql-explain table tr {
word-break: normal;
}
.queries-table pre {
margin: 0;
white-space: pre-wrap;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
{# Security panel
========================================================================= #}
#collector-content .decision-log .voter_result td {
border-top-width: 1px;
border-bottom-width: 0;
padding-bottom: 0;
}
#collector-content .decision-log .voter_details td {
border-top-width: 0;
border-bottom-width: 1px;
padding-bottom: 0;
}
#collector-content .decision-log .voter_details table {
border: 0;
margin: 0;
box-shadow: unset;
}
#collector-content .decision-log .voter_details table td {
border: 0;
padding: 0 0 8px 0;
}
{# Validator panel
========================================================================= #}
#collector-content .sf-validator {
margin-bottom: 2em;
}
#collector-content .sf-validator .sf-validator-context,
#collector-content .sf-validator .trace {
border: var(--border);
background: var(--base-0);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .sf-validator .trace {
font-size: 12px;
}
#collector-content .sf-validator .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .sf-validator .trace li.selected {
background: rgba(255, 255, 153, 0.5);
}
{# Messenger panel
========================================================================= #}
#collector-content .message-bus .trace {
border: 1px solid #DDD;
background: #FFF;
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .message-bus .trace {
font-size: 12px;
}
#collector-content .message-bus .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .message-bus .trace li.selected {
background: rgba(255, 255, 153, 0.5);
}
{# Dump panel
========================================================================= #}
pre.sf-dump, pre.sf-dump .sf-dump-default {
z-index: 1000 !important;
}
#collector-content .sf-dump {
margin-bottom: 2em;
}
#collector-content pre.sf-dump,
#collector-content .sf-dump code,
#collector-content .sf-dump samp {
font-family: monospace;
font-size: 13px;
}
#collector-content .sf-dump a {
cursor: pointer;
}
#collector-content .sf-dump pre.sf-dump,
#collector-content .sf-dump .trace {
border: var(--border);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content pre.sf-dump,
#collector-content .sf-dump-default {
background: none;
}
#collector-content .sf-dump-ellipsis { max-width: 100em; }
#collector-content .sf-dump {
margin: 0;
padding: 0;
line-height: 1.4;
}
#collector-content .dump-inline .sf-dump {
display: inline;
white-space: normal;
font-size: inherit;
line-height: inherit;
}
#collector-content .dump-inline .sf-dump:after {
display: none;
}
#collector-content .sf-dump .trace {
font-size: 12px;
}
#collector-content .sf-dump .trace li {
margin-bottom: 0;
padding: 0;
}
{# Search Results page
========================================================================= #}
#search-results td {
font-family: var(--font-sans-serif);
vertical-align: middle;
}
#search-results .sf-search {
visibility: hidden;
margin-left: 2px;
}
#search-results tr:hover .sf-search {
visibility: visible;
}
{# Small screens
========================================================================= #}
.visible-small {
display: none;
}
.hidden-small {
display: inherit;
}
@media (max-width: 768px) {
#sidebar {
flex-basis: 50px;
overflow-x: hidden;
transition: flex-basis 200ms ease-out;
}
#sidebar:hover, #sidebar.expanded {
flex-basis: 220px;
}
#sidebar-search {
display: none;
}
#sidebar:hover #sidebar-search.sf-toggle-visible, #sidebar.expanded #sidebar-search.sf-toggle-visible {
display: block;
}
#sidebar .module {
display: none;
}
#sidebar:hover .module, #sidebar.expanded .module {
display: block;
}
#sidebar:not(:hover):not(.expanded) .label .count {
border-radius: 50%;
border: 1px solid #eee;
height: 8px;
min-width: 0;
padding: 0;
right: 4px;
text-indent: -9999px;
top: 50%;
width: 8px;
}
.visible-small {
display: inherit;
}
.hidden-small {
display: none;
}
.btn-sm svg {
margin-left: 2px;
}
}
{# Config Options
========================================================================= #}
body.width-full .container {
max-width: 100%;
}
body.theme-light #collector-content .sf-dump pre.sf-dump,
body.theme-light #collector-content .sf-dump .trace {
background: #FFF;
}
body.theme-light #collector-content pre.sf-dump,
body.theme-light #collector-content .sf-dump-default {
color: #CC7832;
}
body.theme-light #collector-content .sf-dump-str { color: #629755; }
body.theme-light #collector-content .sf-dump-private,
body.theme-light #collector-content .sf-dump-protected,
body.theme-light #collector-content .sf-dump-public { color: #262626; }
body.theme-light #collector-content .sf-dump-note { color: #6897BB; }
body.theme-light #collector-content .sf-dump-key { color: #789339; }
body.theme-light #collector-content .sf-dump-ref { color: #6E6E6E; }
body.theme-light #collector-content .sf-dump-ellipsis { color: #CC7832; max-width: 100em; }
body.theme-light #collector-content .sf-dump-ellipsis-path { max-width: 5em; }
body.theme-light #collector-content .sf-dump .trace li.selected {
background: rgba(255, 255, 153, 0.5);
}

View File

@ -0,0 +1,67 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% macro profile_search_filter(request, result, property) %}
{%- if request.hasSession -%}
<a href="{{ path('_profiler_search_results', request.query.all|merge({token: result.token})|merge({ (property): result[property] })) }}" title="Search"><span title="Search" class="sf-icon sf-search">{{ include('@WebProfiler/Icon/search.svg') }}</span></a>
{%- endif -%}
{% endmacro %}
{% import _self as helper %}
{% block summary %}
<div class="status">
<div class="container">
<h2>Profile Search</h2>
</div>
</div>
{% endblock %}
{% block panel %}
<h2>{{ tokens ? tokens|length : 'No' }} results found</h2>
{% if tokens %}
<table id="search-results">
<thead>
<tr>
<th scope="col" class="text-center">Status</th>
<th scope="col">IP</th>
<th scope="col">Method</th>
<th scope="col">URL</th>
<th scope="col">Time</th>
<th scope="col">Token</th>
</tr>
</thead>
<tbody>
{% for result in tokens %}
{% set css_class = result.status_code|default(0) > 399 ? 'status-error' : result.status_code|default(0) > 299 ? 'status-warning' : 'status-success' %}
<tr>
<td class="text-center">
<span class="label {{ css_class }}">{{ result.status_code|default('n/a') }}</span>
</td>
<td>
<span class="nowrap">{{ result.ip }} {{ helper.profile_search_filter(request, result, 'ip') }}</span>
</td>
<td>
<span class="nowrap">{{ result.method }} {{ helper.profile_search_filter(request, result, 'method') }}</span>
</td>
<td class="break-long-words">
{{ result.url }}
{{ helper.profile_search_filter(request, result, 'url') }}
</td>
<td class="text-small">
<span class="nowrap">{{ result.time|date('d-M-Y') }}</span>
<span class="nowrap newline">{{ result.time|date('H:i:s') }}</span>
</td>
<td class="nowrap"><a href="{{ path('_profiler', { token: result.token }) }}">{{ result.token }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>The query returned no result.</p>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,56 @@
<div id="sidebar-search" class="hidden">
<form action="{{ path('_profiler_search') }}" method="get">
<div class="form-group">
<label for="ip">IP</label>
<input type="text" name="ip" id="ip" value="{{ ip }}">
</div>
<div class="form-group">
<label for="method">Method</label>
<select name="method" id="method">
<option value="">Any</option>
{% for m in ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'] %}
<option {{ m == method ? 'selected="selected"' }}>{{ m }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="status_code">Status</label>
<input type="number" name="status_code" id="status_code" min="100" max="599" value="{{ status_code }}">
</div>
<div class="form-group">
<label for="url">URL</label>
<input type="text" name="url" id="url" value="{{ url }}">
</div>
<div class="form-group">
<label for="token">Token</label>
<input type="text" name="token" id="token" value="{{ token }}">
</div>
<div class="form-group">
<label for="start">From</label>
<input type="date" name="start" id="start" value="{{ start }}">
</div>
<div class="form-group">
<label for="end">Until</label>
<input type="date" name="end" id="end" value="{{ end }}">
</div>
<div class="form-group">
<label for="limit">Results</label>
<select name="limit" id="limit">
{% for l in [10, 50, 100] %}
<option {{ l == limit ? 'selected="selected"' }}>{{ l }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<button type="submit" class="btn btn-sm">Search</button>
</div>
</form>
</div>

View File

@ -0,0 +1,193 @@
<style>
#open-settings {
color: var(--color-muted);
display: block;
margin: 15px 15px 5px;
}
.modal-wrap {
-webkit-transition: all 0.3s ease-in-out;
align-items: center;
background: rgba(0, 0, 0, 0.8);
display: flex;
height: 100%;
justify-content: center;
left: 0;
opacity: 1;
overflow: auto;
position: fixed;
top: 0;
transition: all 0.3s ease-in-out;
visibility: hidden;
width: 100%;
z-index: 100000;
}
.modal-wrap.visible {
opacity: 1;
visibility: visible;
}
.modal-wrap .modal-container {
box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 0.5);
color: var(--base-6);
margin: 1em;
max-width: 94%;
width: 600px;
}
.modal-wrap .modal-header {
align-items: center;
background: var(--table-header);
display: flex;
justify-content: space-between;
padding: 15px 30px;
}
.modal-wrap .modal-header h3 {
color: var(--base-6);
margin: 0;
}
.modal-wrap .modal-header .close-modal {
background: transparent;
border: 0;
color: var(--base-4);
cursor: pointer;
font-size: 28px;
line-height: 1;
}
.modal-wrap .modal-header .close-modal:hover { opacity: 1; }
.modal-wrap .modal-content {
background: var(--base-1);
margin: 0;
padding: 15px 30px;
width: 100%;
z-index: 100000;
}
.modal-content h4 {
border-bottom: var(--border);
margin: 0 0 15px;
padding: 0 0 5px;
}
.modal-content input, .modal-content .form-help {
margin-left: 15px;
}
.modal-content label {
cursor: pointer;
font-size: 16px;
margin-left: 3px;
}
.modal-content .form-help {
color: var(--color-muted);
font-size: 14px;
margin: 5px 0 15px 33px;
}
.modal-content .form-help + h4 {
margin-top: 45px;
}
@media (max-width: 768px) {
#open-settings {
color: transparent;
}
#sidebar:hover #open-settings, #sidebar.expanded #open-settings {
color: var(--color-muted);
}
#open-settings:before {
content: '\2699';
font-weight: bold;
font-size: 25px;
color: var(--color-muted);
}
#sidebar:hover #open-settings:before, #sidebar.expanded #open-settings:before {
content: '';
}
}
</style>
<a href="#" id="open-settings">Settings</a>
<div class="modal-wrap" id="profiler-settings">
<div class="modal-container">
<div class="modal-header">
<h3>Configuration Settings</h3>
<button class="close-modal">&times;</button>
</div>
<div class="modal-content">
<h4>Theme</h4>
<input class="config-option" type="radio" name="theme" value="auto" id="settings-theme-auto">
<label for="settings-theme-auto">Auto</label>
<p class="form-help"><strong>Default theme</strong>. It switches between Light and Dark automatically to match the operating system theme.</p>
<input class="config-option" type="radio" name="theme" value="light" id="settings-theme-light">
<label for="settings-theme-light">Light</label>
<p class="form-help">Provides greatest readability, but requires a well-lit environment.</p>
<input class="config-option" type="radio" name="theme" value="dark" id="settings-theme-dark">
<label for="settings-theme-dark">Dark</label>
<p class="form-help">Reduces eye fatigue. Ideal for low light environments.</p>
<h4>Page Width</h4>
<input class="config-option" type="radio" name="width" value="normal" id="settings-width-normal">
<label for="settings-width-normal">Normal</label>
<p class="form-help">Fixed page width. Improves readability.</p>
<input class="config-option" type="radio" name="width" value="full" id="settings-width-full">
<label for="settings-width-full">Full-page</label>
<p class="form-help">Dynamic page width. As wide as the browser window.</p>
</div>
</div>
</div>
<script>
(function() {
const configOptions = document.querySelectorAll('.config-option');
const allSettingValues = ['theme-auto', 'theme-ligh', 'theme-dark', 'width-normal', 'width-full'];
[...configOptions].forEach(option => {
option.addEventListener('change', function (event) {
const optionName = option.name;
const optionValue = option.value;
const settingName = 'symfony/profiler/' + optionName;
const settingValue = optionName + '-' + optionValue;
localStorage.setItem(settingName, settingValue);
document.body.classList.forEach((cssClass) => {
if (cssClass.startsWith(optionName)) {
document.body.classList.remove(cssClass);
}
});
if ('theme-auto' === settingValue) {
document.body.classList.add((matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light'));
} else {
document.body.classList.add(settingValue);
}
});
});
const openModalButton = document.getElementById('open-settings');
const modalWindow = document.getElementById('profiler-settings');
const closeModalButton = document.getElementsByClassName('close-modal')[0];
const modalWrapper = document.getElementsByClassName('modal-wrap')[0]
openModalButton.addEventListener('click', function(event) {
document.getElementById('settings-' + (localStorage.getItem('symfony/profiler/theme') || 'theme-auto')).checked = 'checked';
document.getElementById('settings-' + (localStorage.getItem('symfony/profiler/width') || 'width-normal')).checked = 'checked';
modalWindow.classList.toggle('visible');
event.preventDefault();
});
closeModalButton.addEventListener('click', function() {
modalWindow.classList.remove('visible');
});
modalWrapper.addEventListener('click', function(event) {
if (event.target == event.currentTarget) {
modalWindow.classList.remove('visible');
}
});
})();
</script>

View File

@ -0,0 +1,16 @@
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col" class="key">{{ labels is defined ? labels[0] : 'Key' }}</th>
<th scope="col">{{ labels is defined ? labels[1] : 'Value' }}</th>
</tr>
</thead>
<tbody>
{% for key in data|keys|sort %}
<tr>
<th scope="row">{{ key }}</th>
<td>{{ profiler_dump(data[key]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,562 @@
{# when updating any of these colors, do the same in profiler.css.twig #}
{% set colors = { 'success': '#4F805D', 'warning': '#A46A1F', 'error': '#B0413E' } %}
.sf-minitoolbar {
background-color: #222;
border-top-left-radius: 4px;
bottom: 0;
box-sizing: border-box;
display: none;
height: 36px;
padding: 6px;
position: fixed;
right: 0;
z-index: 99999;
}
.sf-minitoolbar button {
background-color: transparent;
padding: 0;
border: none;
}
.sf-minitoolbar svg,
.sf-minitoolbar img {
max-height: 24px;
max-width: 24px;
display: inline;
}
.sf-toolbar-clearer {
clear: both;
height: 36px;
}
.sf-display-none {
display: none;
}
.sf-toolbarreset * {
box-sizing: content-box;
vertical-align: baseline;
letter-spacing: normal;
width: auto;
}
.sf-toolbarreset {
background-color: #222;
bottom: 0;
box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
color: #EEE;
font: 11px Arial, sans-serif;
left: 0;
margin: 0;
padding: 0 36px 0 0;
position: fixed;
right: 0;
text-align: left;
text-transform: none;
z-index: 99999;
direction: ltr;
/* neutralize the aliasing defined by external CSS styles */
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
}
.sf-toolbarreset abbr {
border: dashed #777;
border-width: 0 0 1px;
}
.sf-toolbarreset svg,
.sf-toolbarreset img {
height: 20px;
width: 20px;
display: inline-block;
}
.sf-toolbarreset .sf-cancel-button {
color: #444;
}
.sf-toolbarreset .hide-button {
background: #444;
display: block;
position: absolute;
top: 0;
right: 0;
width: 36px;
height: 36px;
cursor: pointer;
text-align: center;
border: none;
margin: 0;
padding: 0;
}
.sf-toolbarreset .hide-button svg {
max-height: 18px;
margin-top: 1px;
}
.sf-toolbar-block {
cursor: default;
display: block;
float: left;
height: 36px;
margin-right: 0;
white-space: nowrap;
max-width: 15%;
}
.sf-toolbar-block > a,
.sf-toolbar-block > a:hover {
display: block;
text-decoration: none;
background-color: transparent;
color: inherit;
}
.sf-toolbar-block span {
display: inline-block;
}
.sf-toolbar-block .sf-toolbar-value {
color: #F5F5F5;
font-size: 13px;
line-height: 36px;
padding: 0;
}
.sf-toolbar-block .sf-toolbar-label,
.sf-toolbar-block .sf-toolbar-class-separator {
color: #AAA;
font-size: 12px;
}
.sf-toolbar-block .sf-toolbar-info {
border-collapse: collapse;
display: table;
z-index: 100000;
}
.sf-toolbar-block hr {
border-top: 1px solid #777;
margin: 4px 0;
padding-top: 4px;
}
.sf-toolbar-block .sf-toolbar-info-piece {
/* this 'border-bottom' trick is needed because 'margin-bottom' doesn't work for table rows */
border-bottom: solid transparent 3px;
display: table-row;
}
.sf-toolbar-block .sf-toolbar-info-piece-additional,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
display: none;
}
.sf-toolbar-block .sf-toolbar-info-group {
margin-bottom: 4px;
padding-bottom: 2px;
border-bottom: 1px solid #333333;
}
.sf-toolbar-block .sf-toolbar-info-group:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status {
padding: 2px 5px;
margin-bottom: 0;
}
.sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status + .sf-toolbar-status {
margin-left: 4px;
}
.sf-toolbar-block .sf-toolbar-info-piece:last-child {
margin-bottom: 0;
}
div.sf-toolbar .sf-toolbar-block .sf-toolbar-info-piece a {
color: #99CDD8;
text-decoration: underline;
}
div.sf-toolbar .sf-toolbar-block a:hover {
text-decoration: none;
}
.sf-toolbar-block .sf-toolbar-info-piece b {
color: #AAA;
display: table-cell;
font-size: 11px;
padding: 4px 8px 4px 0;
}
.sf-toolbar-block:not(.sf-toolbar-block-dump) .sf-toolbar-info-piece span {
color: #F5F5F5;
}
.sf-toolbar-block .sf-toolbar-info-piece span {
font-size: 12px;
}
.sf-toolbar-block .sf-toolbar-info {
background-color: #444;
bottom: 36px;
color: #F5F5F5;
display: none;
padding: 9px 0;
position: absolute;
}
.sf-toolbar-block .sf-toolbar-info:empty {
visibility: hidden;
}
.sf-toolbar-block .sf-toolbar-status {
display: inline-block;
color: #FFF;
background-color: #666;
padding: 3px 6px;
margin-bottom: 2px;
vertical-align: middle;
min-width: 15px;
min-height: 13px;
text-align: center;
}
.sf-toolbar-block .sf-toolbar-status-green {
background-color: {{ colors.success|raw }};
}
.sf-toolbar-block .sf-toolbar-status-red {
background-color: {{ colors.error|raw }};
}
.sf-toolbar-block .sf-toolbar-status-yellow {
background-color: {{ colors.warning|raw }};
}
.sf-toolbar-block.sf-toolbar-status-green {
background-color: {{ colors.success|raw }};
color: #FFF;
}
.sf-toolbar-block.sf-toolbar-status-red {
background-color: {{ colors.error|raw }};
color: #FFF;
}
.sf-toolbar-block.sf-toolbar-status-yellow {
background-color: {{ colors.warning|raw }};
color: #FFF;
}
.sf-toolbar-block-request .sf-toolbar-status {
color: #FFF;
display: inline-block;
font-size: 14px;
height: 36px;
line-height: 36px;
padding: 0 10px;
}
.sf-toolbar-block-request .sf-toolbar-info-piece a {
background-color: transparent;
text-decoration: none;
}
.sf-toolbar-block-request .sf-toolbar-info-piece a:hover {
text-decoration: underline;
}
.sf-toolbar-block-request .sf-toolbar-redirection-status {
font-weight: normal;
padding: 2px 4px;
line-height: 18px;
}
.sf-toolbar-block-request .sf-toolbar-info-piece span.sf-toolbar-redirection-method {
font-size: 12px;
height: 17px;
line-height: 17px;
margin-right: 5px;
}
.sf-toolbar-block-ajax .sf-toolbar-icon {
cursor: pointer;
}
.sf-toolbar-status-green .sf-toolbar-label,
.sf-toolbar-status-yellow .sf-toolbar-label,
.sf-toolbar-status-red .sf-toolbar-label {
color: #FFF;
}
.sf-toolbar-status-green svg path,
.sf-toolbar-status-green svg .sf-svg-path,
.sf-toolbar-status-red svg path,
.sf-toolbar-status-red svg .sf-svg-path,
.sf-toolbar-status-yellow svg path,
.sf-toolbar-status-yellow svg .sf-svg-path {
fill: #FFF;
}
.sf-toolbar-block-config svg path,
.sf-toolbar-block-config svg .sf-svg-path {
fill: #FFF;
}
.sf-toolbar-block .sf-toolbar-icon {
display: block;
height: 36px;
padding: 0 7px;
overflow: hidden;
text-overflow: ellipsis;
}
.sf-toolbar-block-request .sf-toolbar-icon {
padding-left: 0;
padding-right: 0;
}
.sf-toolbar-block .sf-toolbar-icon img,
.sf-toolbar-block .sf-toolbar-icon svg {
border-width: 0;
position: relative;
top: 8px;
vertical-align: baseline;
}
.sf-toolbar-block .sf-toolbar-icon img + span,
.sf-toolbar-block .sf-toolbar-icon svg + span {
margin-left: 4px;
}
.sf-toolbar-block-config .sf-toolbar-icon .sf-toolbar-value {
margin-left: 4px;
}
.sf-toolbar-block:hover,
.sf-toolbar-block.hover {
position: relative;
}
.sf-toolbar-block:hover .sf-toolbar-icon,
.sf-toolbar-block.hover .sf-toolbar-icon {
background-color: #444;
position: relative;
z-index: 10002;
}
.sf-toolbar-block-ajax.hover .sf-toolbar-info {
z-index: 10001;
}
.sf-toolbar-block:hover .sf-toolbar-info,
.sf-toolbar-block.hover .sf-toolbar-info {
display: block;
padding: 10px;
max-width: 480px;
max-height: 480px;
word-wrap: break-word;
overflow: hidden;
overflow-y: auto;
}
.sf-toolbar-info-piece b.sf-toolbar-ajax-info {
color: #F5F5F5;
}
.sf-toolbar-ajax-requests {
table-layout: auto;
width: 100%;
}
.sf-toolbar-ajax-requests td {
background-color: #444;
border-bottom: 1px solid #777;
color: #F5F5F5;
font-size: 12px;
padding: 4px;
}
.sf-toolbar-ajax-requests tr:last-child td {
border-bottom: 0;
}
.sf-toolbar-ajax-requests th {
background-color: #222;
border-bottom: 0;
color: #AAA;
font-size: 11px;
padding: 4px;
}
.sf-ajax-request-url {
max-width: 250px;
line-height: 9px;
overflow: hidden;
text-overflow: ellipsis;
}
.sf-toolbar-ajax-requests .sf-ajax-request-url a {
text-decoration: none;
}
.sf-toolbar-ajax-requests .sf-ajax-request-url a:hover {
text-decoration: underline;
}
.sf-ajax-request-duration {
text-align: right;
}
.sf-ajax-request-loading {
animation: sf-blink .5s ease-in-out infinite;
}
@keyframes sf-blink {
0% { background: #222; }
50% { background: #444; }
100% { background: #222; }
}
.sf-toolbar-block.sf-toolbar-block-dump .sf-toolbar-info {
max-width: none;
width: 100%;
position: fixed;
box-sizing: border-box;
left: 0;
}
.sf-toolbar-block-dump pre.sf-dump {
background-color: #222;
border-color: #777;
border-radius: 0;
margin: 6px 0 12px 0;
}
.sf-toolbar-block-dump pre.sf-dump:last-child {
margin-bottom: 0;
}
.sf-toolbar-block-dump pre.sf-dump .sf-dump-search-wrapper {
margin-bottom: 5px;
}
.sf-toolbar-block-dump pre.sf-dump span.sf-dump-search-count {
color: #333;
font-size: 12px;
}
.sf-toolbar-block-dump .sf-toolbar-info-piece {
display: block;
}
.sf-toolbar-block-dump .sf-toolbar-info-piece .sf-toolbar-file-line {
color: #AAA;
margin-left: 4px;
}
.sf-toolbar-block-dump .sf-toolbar-info img {
display: none;
}
/* Responsive Design */
.sf-toolbar-icon .sf-toolbar-label,
.sf-toolbar-icon .sf-toolbar-value {
display: none;
}
.sf-toolbar-block-config .sf-toolbar-icon .sf-toolbar-label {
display: inline-block;
}
/* Legacy Design - these styles are maintained to make old panels look
a bit better on the new toolbar */
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
color: #AAA;
font-size: 12px;
}
.sf-toolbar-status-green .sf-toolbar-info-piece-additional-detail,
.sf-toolbar-status-yellow .sf-toolbar-info-piece-additional-detail,
.sf-toolbar-status-red .sf-toolbar-info-piece-additional-detail {
color: #FFF;
}
@media (min-width: 768px) {
.sf-toolbar-icon .sf-toolbar-label,
.sf-toolbar-icon .sf-toolbar-value {
display: inline;
}
.sf-toolbar-block .sf-toolbar-icon img,
.sf-toolbar-block .sf-toolbar-icon svg {
top: 6px;
}
.sf-toolbar-block-time .sf-toolbar-icon svg,
.sf-toolbar-block-memory .sf-toolbar-icon svg {
display: none;
}
.sf-toolbar-block-time .sf-toolbar-icon svg + span,
.sf-toolbar-block-memory .sf-toolbar-icon svg + span {
margin-left: 0;
}
.sf-toolbar-block .sf-toolbar-icon {
padding: 0 10px;
}
.sf-toolbar-block-time .sf-toolbar-icon {
padding-right: 5px;
}
.sf-toolbar-block-memory .sf-toolbar-icon {
padding-left: 5px;
}
.sf-toolbar-block-request .sf-toolbar-icon {
padding-left: 0;
padding-right: 0;
}
.sf-toolbar-block-request .sf-toolbar-label {
margin-left: 5px;
}
.sf-toolbar-block-request .sf-toolbar-status + svg {
margin-left: 5px;
}
.sf-toolbar-block-request .sf-toolbar-icon svg + .sf-toolbar-label {
margin-left: 0;
}
.sf-toolbar-block-request .sf-toolbar-label + .sf-toolbar-value {
margin-right: 10px;
}
.sf-toolbar-block-request:hover .sf-toolbar-info {
max-width: none;
}
.sf-toolbar-block .sf-toolbar-info-piece b {
font-size: 12px;
}
.sf-toolbar-block .sf-toolbar-info-piece span {
font-size: 13px;
}
.sf-toolbar-block-right {
float: right;
margin-left: 0;
margin-right: 0;
}
}
@media (min-width: 1024px) {
.sf-toolbar-block .sf-toolbar-info-piece-additional,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
display: inline;
}
.sf-toolbar-block .sf-toolbar-info-piece-additional:empty,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail:empty {
display: none;
}
}
/***** Error Toolbar *****/
.sf-error-toolbar .sf-toolbarreset {
background: #222;
color: #f5f5f5;
font: 13px/36px Arial, sans-serif;
height: 36px;
padding: 0 15px;
text-align: left;
}
.sf-error-toolbar .sf-toolbarreset svg {
height: auto;
}
.sf-error-toolbar .sf-toolbarreset a {
color: #99cdd8;
margin-left: 5px;
text-decoration: underline;
}
.sf-error-toolbar .sf-toolbarreset a:hover {
text-decoration: none;
}
.sf-error-toolbar .sf-toolbarreset .sf-toolbar-icon {
float: left;
padding: 5px 0;
margin-right: 10px;
}
.sf-full-stack {
left: 0px;
font-size: 12px;
}
/***** Media query print: Do not print the Toolbar. *****/
@media print {
.sf-toolbar {
display: none !important;
}
}

View File

@ -0,0 +1,46 @@
<!-- START of Symfony Web Debug Toolbar -->
<div id="sfMiniToolbar-{{ token }}" class="sf-minitoolbar" data-no-turbolink>
<button type="button" title="Show Symfony toolbar" id="sfToolbarMiniToggler-{{ token }}" accesskey="D" aria-expanded="false" aria-controls="sfToolbarMainContent-{{ token }}">
{{ include('@WebProfiler/Icon/symfony.svg') }}
</button>
</div>
<div id="sfToolbarClearer-{{ token }}" class="sf-toolbar-clearer"></div>
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset clear-fix" data-no-turbolink>
{% for name, template in templates %}
{% if block('toolbar', template) is defined %}
{% with {
collector: profile ? profile.getcollector(name) : null,
profiler_url: profiler_url,
token: token ?? (profile ? profile.token : null),
name: name,
profiler_markup_version: profiler_markup_version,
csp_script_nonce: csp_script_nonce,
csp_style_nonce: csp_style_nonce
} %}
{{ block('toolbar', template) }}
{% endwith %}
{% endif %}
{% endfor %}
{% if full_stack %}
<div class="sf-full-stack sf-toolbar-block sf-toolbar-block-full-stack sf-toolbar-status-red sf-toolbar-block-right">
<div class="sf-toolbar-icon">
<span class="sf-toolbar-value">Using symfony/symfony is NOT supported</span>
</div>
<div class="sf-toolbar-info sf-toolbar-status-red">
<p>This project is using Symfony via the "symfony/symfony" package.</p>
<p>This is NOT supported anymore since Symfony 4.0.</p>
<p>Even if it seems to work well, it has some important limitations with no workarounds.</p>
<p>Using this package also makes your project slower.</p>
<strong>Please, stop using this package and replace it with individual packages instead.</strong>
</div>
<div></div>
</div>
{% endif %}
<button class="hide-button" type="button" id="sfToolbarHideButton-{{ token }}" title="Close Toolbar" accesskey="D" aria-expanded="true" aria-controls="sfToolbarMainContent-{{ token }}">
{{ include('@WebProfiler/Icon/close.svg') }}
</button>
</div>
<!-- END of Symfony Web Debug Toolbar -->

View File

@ -0,0 +1,6 @@
<div class="sf-toolbar-block sf-toolbar-block-{{ name }} sf-toolbar-status-{{ status|default('normal') }} {{ additional_classes|default('') }}" {{ block_attrs|default('')|raw }}>
{% if link is not defined or link %}<a href="{{ url('_profiler', { token: token, panel: name }) }}">{% endif %}
<div class="sf-toolbar-icon">{{ icon|default('') }}</div>
{% if link|default(false) %}</a>{% endif %}
<div class="sf-toolbar-info">{{ text|default('') }}</div>
</div>

View File

@ -0,0 +1,21 @@
<div id="sfwdt{{ token }}" class="sf-toolbar sf-display-none" role="region" aria-label="Symfony Web Debug Toolbar">
{% include('@WebProfiler/Profiler/toolbar.html.twig') with {
templates: {
'request': '@WebProfiler/Profiler/cancel.html.twig'
},
profile: null,
profiler_url: url('_profiler', {token: token}),
profiler_markup_version: 2,
} %}
</div>
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
<style{% if csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %}>
{{ include('@WebProfiler/Profiler/toolbar.css.twig') }}
</style>
<script{% if csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>/*<![CDATA[*/
(function () {
Sfjs.loadToolbar('{{ token }}');
})();
/*]]>*/</script>

View File

@ -0,0 +1,18 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block title 'Redirection Intercepted' %}
{% block body %}
<div class="sf-reset">
<div class="block-exception">
<h1>This request redirects to <a href="{{ location }}">{{ location }}</a>.</h1>
<p>
<small>
The redirect was intercepted by the web debug toolbar to help debugging.
For more information, see the "intercept-redirects" option of the Profiler.
</small>
</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,71 @@
<h2>Routing</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ request.route ?: '(none)' }}</span>
<span class="label">Matched route</span>
</div>
</div>
{% if request.route %}
<h3>Route Parameters</h3>
{% if request.routeParams is empty %}
<div class="empty">
<p>No parameters.</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: request.routeParams, labels: ['Name', 'Value'] }, with_context = false) }}
{% endif %}
{% endif %}
{% if router.redirect %}
<h3>Route Redirection</h3>
<p>This page redirects to:</p>
<div class="card break-long-words">
{{ router.targetUrl }}
{% if router.targetRoute %}<span class="text-muted">(route: "{{ router.targetRoute }}")</span>{% endif %}
</div>
{% endif %}
<h3>Route Matching Logs</h3>
<div class="card">
<strong>Path to match:</strong> <code>{{ request.pathinfo }}</code>
</div>
<table id="router-logs">
<thead>
<tr>
<th>#</th>
<th>Route name</th>
<th>Path</th>
<th>Log</th>
</tr>
</thead>
<tbody>
{% for trace in traces %}
<tr class="{{ trace.level == 1 ? 'status-warning' : trace.level == 2 ? 'status-success' }}">
<td class="font-normal text-muted nowrap">{{ loop.index }}</td>
<td class="break-long-words">{{ trace.name }}</td>
<td class="break-long-words">{{ trace.path }}</td>
<td class="font-normal">
{% if trace.level == 1 %}
Path almost matches, but
<span class="newline">{{ trace.log }}</span>
{% elseif trace.level == 2 %}
{{ trace.log }}
{% else %}
Path does not match
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="help">
Note: These matching logs are based on the current router configuration,
which might differ from the configuration used when profiling this request.
</p>

View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1408 960V832q0-26-19-45t-45-19H448q-26 0-45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45-19t19-45zm256-544v960q0 119-84.5 203.5T1376 1664H416q-119 0-203.5-84.5T128 1376V416q0-119 84.5-203.5T416 128h960q119 0 203.5 84.5T1664 416z"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@ -0,0 +1 @@
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1408 960V832q0-26-19-45t-45-19h-320V448q0-26-19-45t-45-19H832q-26 0-45 19t-19 45v320H448q-26 0-45 19t-19 45v128q0 26 19 45t45 19h320v320q0 26 19 45t45 19h128q26 0 45-19t19-45v-320h320q26 0 45-19t19-45zm256-544v960q0 119-84.5 203.5T1376 1664H416q-119 0-203.5-84.5T128 1376V416q0-119 84.5-203.5T416 128h960q119 0 203.5 84.5T1664 416z"/></svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -0,0 +1,113 @@
<?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\Bundle\WebProfilerBundle\Twig;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Twig\Environment;
use Twig\Extension\ProfilerExtension;
use Twig\Profiler\Profile;
use Twig\TwigFunction;
/**
* Twig extension for the profiler.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class WebProfilerExtension extends ProfilerExtension
{
/**
* @var HtmlDumper
*/
private $dumper;
/**
* @var resource
*/
private $output;
/**
* @var int
*/
private $stackLevel = 0;
public function __construct(HtmlDumper $dumper = null)
{
$this->dumper = $dumper ?? new HtmlDumper();
$this->dumper->setOutput($this->output = fopen('php://memory', 'r+'));
}
public function enter(Profile $profile): void
{
++$this->stackLevel;
}
public function leave(Profile $profile): void
{
if (0 === --$this->stackLevel) {
$this->dumper->setOutput($this->output = fopen('php://memory', 'r+'));
}
}
/**
* {@inheritdoc}
*/
public function getFunctions(): array
{
return [
new TwigFunction('profiler_dump', [$this, 'dumpData'], ['is_safe' => ['html'], 'needs_environment' => true]),
new TwigFunction('profiler_dump_log', [$this, 'dumpLog'], ['is_safe' => ['html'], 'needs_environment' => true]),
];
}
public function dumpData(Environment $env, Data $data, int $maxDepth = 0)
{
$this->dumper->setCharset($env->getCharset());
$this->dumper->dump($data, null, [
'maxDepth' => $maxDepth,
]);
$dump = stream_get_contents($this->output, -1, 0);
rewind($this->output);
ftruncate($this->output, 0);
return str_replace("\n</pre", '</pre', rtrim($dump));
}
public function dumpLog(Environment $env, string $message, Data $context = null)
{
$message = twig_escape_filter($env, $message);
$message = preg_replace('/&quot;(.*?)&quot;/', '&quot;<b>$1</b>&quot;', $message);
if (null === $context || !str_contains($message, '{')) {
return '<span class="dump-inline">'.$message.'</span>';
}
$replacements = [];
foreach ($context as $k => $v) {
$k = '{'.twig_escape_filter($env, $k).'}';
$replacements['&quot;<b>'.$k.'</b>&quot;'] = $replacements['&quot;'.$k.'&quot;'] = $replacements[$k] = $this->dumpData($env, $v);
}
return '<span class="dump-inline">'.strtr($message, $replacements).'</span>';
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'profiler';
}
}

View File

@ -0,0 +1,27 @@
<?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\Bundle\WebProfilerBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class WebProfilerBundle extends Bundle
{
public function boot()
{
if ('prod' === $this->container->getParameter('kernel.environment')) {
@trigger_error('Using WebProfilerBundle in production is not supported and puts your project at risk, disable it.', \E_USER_WARNING);
}
}
}

View File

@ -0,0 +1,47 @@
{
"name": "symfony/web-profiler-bundle",
"type": "symfony-bundle",
"description": "Provides a development tool that gives detailed information about the execution of any request",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/framework-bundle": "^5.3|^6.0",
"symfony/http-kernel": "^5.3|^6.0",
"symfony/polyfill-php80": "^1.16",
"symfony/routing": "^4.4|^5.0|^6.0",
"symfony/twig-bundle": "^4.4|^5.0|^6.0",
"twig/twig": "^2.13|^3.0.4"
},
"require-dev": {
"symfony/browser-kit": "^4.4|^5.0|^6.0",
"symfony/console": "^4.4|^5.0|^6.0",
"symfony/css-selector": "^4.4|^5.0|^6.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0"
},
"conflict": {
"symfony/form": "<4.4",
"symfony/mailer": "<5.4",
"symfony/messenger": "<4.4",
"symfony/dependency-injection": "<5.2"
},
"autoload": {
"psr-4": { "Symfony\\Bundle\\WebProfilerBundle\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}