From 5d76eb7b8623ccec8677c560ca42d5e5b2c07c8b Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Mon, 22 Jul 2019 12:33:01 -0400 Subject: [PATCH] [ErrorRenderer] Improving the exception page provided by HtmlErrorRenderer --- .../Resources/config/error_renderer.xml | 3 + .../Tests/ExceptionHandlerTest.php | 9 +- .../ErrorRenderer/HtmlErrorRenderer.php | 411 +++++++++--------- .../Resources/assets/css/error.css | 4 + .../Resources/assets/css/exception.css | 209 +++++++++ .../Resources/assets/css/exception_full.css | 128 ++++++ .../Resources/assets/images/chevron-right.svg | 1 + .../assets/images/favicon.png.base64 | 1 + .../Resources/assets/images/icon-book.svg | 1 + .../assets/images/icon-minus-square-o.svg | 1 + .../assets/images/icon-minus-square.svg | 1 + .../assets/images/icon-plus-square-o.svg | 1 + .../assets/images/icon-plus-square.svg | 1 + .../Resources/assets/images/icon-support.svg | 1 + .../assets/images/symfony-ghost.svg.php | 1 + .../Resources/assets/images/symfony-logo.svg | 1 + .../Resources/assets/js/exception.js | 279 ++++++++++++ .../Resources/views/error.html.php | 20 + .../Resources/views/exception.html.php | 116 +++++ .../Resources/views/exception_full.html.php | 41 ++ .../Resources/views/logs.html.php | 42 ++ .../Resources/views/trace.html.php | 40 ++ .../Resources/views/traces.html.php | 42 ++ .../Resources/views/traces_text.html.php | 43 ++ .../ErrorRenderer/HtmlErrorRendererTest.php | 2 +- 25 files changed, 1183 insertions(+), 216 deletions(-) create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/css/error.css create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception.css create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception_full.css create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/chevron-right.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/favicon.png.base64 create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-book.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square-o.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square-o.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-support.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-ghost.svg.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-logo.svg create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/assets/js/exception.js create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/error.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/exception.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/exception_full.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/logs.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/trace.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/traces.html.php create mode 100644 src/Symfony/Component/ErrorRenderer/Resources/views/traces_text.html.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml index a6f7b099b7211..206e399f5f459 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml @@ -14,6 +14,9 @@ %kernel.debug% %kernel.charset% %debug.file_link_format% + %kernel.project_dir% + + diff --git a/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php index 694177f91e43d..db01374b9170c 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php @@ -52,16 +52,15 @@ public function testDebug() $response = ob_get_clean(); $this->assertContains('

Foo

', $response); - $this->assertContains('
', $response); + $this->assertContains('
', $response); // taken from https://www.owasp.org/index.php/Cross-site_Scripting_(XSS) - $htmlWithXss = ' click me! '; + $htmlWithXss = ' click me! '; ob_start(); $handler->sendPhpResponse(new \RuntimeException($htmlWithXss)); $response = ob_get_clean(); - $this->assertContains(sprintf('

%s

', htmlspecialchars($htmlWithXss, ENT_COMPAT | ENT_SUBSTITUTE, 'UTF-8')), $response); + $this->assertContains(sprintf('

%s

', htmlspecialchars($htmlWithXss, ENT_COMPAT | ENT_SUBSTITUTE, 'UTF-8')), $response); } public function testStatusCode() @@ -106,7 +105,7 @@ public function testNestedExceptions() $handler->sendPhpResponse(new \RuntimeException('Foo', 0, new \RuntimeException('Bar'))); $response = ob_get_clean(); - $this->assertStringMatchesFormat('%A

Foo

%A

Bar

%A', $response); + $this->assertStringMatchesFormat('%A

Foo

%A

Bar

%A', $response); } public function testHandle() diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php index a1f222607524c..59d8504d4149b 100644 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php @@ -11,8 +11,11 @@ namespace Symfony\Component\ErrorRenderer\ErrorRenderer; +use Psr\Log\LoggerInterface; use Symfony\Component\ErrorRenderer\Exception\FlattenException; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; /** * @author Yonel Ceruto @@ -32,12 +35,18 @@ class HtmlErrorRenderer implements ErrorRendererInterface private $debug; private $charset; private $fileLinkFormat; + private $projectDir; + private $requestStack; + private $logger; - public function __construct(bool $debug = false, string $charset = null, $fileLinkFormat = null) + public function __construct(bool $debug = false, string $charset = null, $fileLinkFormat = null, string $projectDir = null, RequestStack $requestStack = null, LoggerInterface $logger = null) { $this->debug = $debug; $this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8'); - $this->fileLinkFormat = $fileLinkFormat; + $this->fileLinkFormat = $fileLinkFormat ?: (ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')); + $this->projectDir = $projectDir; + $this->requestStack = $requestStack; + $this->logger = $logger; } /** @@ -53,276 +62,249 @@ public static function getFormat(): string */ public function render(FlattenException $exception): string { - $css = $this->getStylesheet(); - $body = $this->getBody($exception); - $charset = $this->escapeHtml($this->charset); - $title = $this->escapeHtml($exception->getTitle()); - - return << - - - - - {$title} - - - - $body - - -EOF; + return $this->renderException($exception); } /** - * Sets the format for links to source files. - * - * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files + * Gets the HTML content associated with the given exception. * - * @return string The previous file link format + * @internal */ - public function setFileLinkFormat($fileLinkFormat) + public function getBody(FlattenException $exception): string { - $old = $this->fileLinkFormat; - $this->fileLinkFormat = $fileLinkFormat; - - return $old; + return $this->renderException($exception, 'views/exception.html.php'); } /** - * Gets the HTML content associated with the given exception. + * Gets the stylesheet associated with the given exception. * - * @return string The content as a string + * @internal */ - public function getBody(FlattenException $exception) + public function getStylesheet(): string { - $statusCode = $this->escapeHtml($exception->getStatusCode()); - $title = $this->escapeHtml($exception->getTitle()); - if (!$this->debug) { - return << -

Oops! An Error Occurred

-

The server returned a "{$statusCode} {$title}".

-

- Something is broken. Please let us know what you were doing when this error occurred. - We will fix it as soon as possible. Sorry for any inconvenience caused. -

-
-EOF; + return $this->include('assets/css/error.css'); } - if (404 === $exception->getStatusCode()) { - $exceptionMessage = 'Sorry, the page you are looking for could not be found.'; - } else { - $exceptionMessage = $this->escapeHtml($exception->getMessage()); - } + return $this->include('assets/css/exception.css'); + } - $content = ''; - try { - $count = \count($exception->getAllPrevious()); - $total = $count + 1; - foreach ($exception->toArray() as $position => $e) { - $ind = $count - $position + 1; - $class = $this->formatClass($e['class']); - $message = nl2br($this->escapeHtml($e['message'])); - $content .= sprintf(<<<'EOF' -
- - - -EOF - , $ind, $total, $class, $message); - foreach ($e['trace'] as $trace) { - $content .= '\n"; - } + private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string + { + $statusText = $this->escape($exception->getTitle()); + $statusCode = $this->escape($exception->getStatusCode()); - $content .= "\n
-

- (%d/%d) - %s -

-

%s

-
'; - if ($trace['function']) { - $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); - } - if (isset($trace['file'], $trace['line'])) { - $content .= $this->formatPath($trace['file'], $trace['line']); - } - $content .= "
\n
\n"; - } - } catch (\Exception $e) { - // something nasty happened and we cannot throw an exception anymore - if ($this->debug) { - $e = FlattenException::createFromThrowable($e); - $exceptionMessage = sprintf('Exception thrown when handling an exception (%s: %s)', $e->getClass(), $this->escapeHtml($e->getMessage())); - } else { - $exceptionMessage = 'Whoops, looks like something went wrong.'; - } + if (!$this->debug) { + return $this->include('views/error.html.php', [ + 'statusText' => $statusText, + 'statusCode' => $statusCode, + ]); } - $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); - - return << -
-
-

$exceptionMessage

-
$symfonyGhostImageContents
-
-
-
- -
- $content -
-EOF; + $exceptionMessage = $this->escape($exception->getMessage()); + $request = $this->requestStack ? $this->requestStack->getCurrentRequest() : null; + + return $this->include($debugTemplate, [ + 'exception' => $exception, + 'exceptionMessage' => $exceptionMessage, + 'statusText' => $statusText, + 'statusCode' => $statusCode, + 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, + 'currentContent' => $request ? $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level')) : null, + ]); } - /** - * Gets the stylesheet associated with the given exception. - * - * @return string The stylesheet as a string - */ - public function getStylesheet(): string + private function getAndCleanOutputBuffering(int $startObLevel): string { - if (!$this->debug) { - return <<<'EOF' - body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; } - .container { margin: 30px; max-width: 600px; } - h1 { color: #dc3545; font-size: 24px; } - h2 { font-size: 18px; } -EOF; + if (ob_get_level() <= $startObLevel) { + return ''; } - return <<<'EOF' - body { background-color: #F9F9F9; color: #222; font: 14px/1.4 Helvetica, Arial, sans-serif; margin: 0; padding-bottom: 45px; } - - a { cursor: pointer; text-decoration: none; } - a:hover { text-decoration: underline; } - abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } + Response::closeOutputBuffers($startObLevel + 1, true); - code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } + return ob_get_clean(); + } - table, tr, th, td { background: #FFF; border-collapse: collapse; vertical-align: top; } - table { background: #FFF; border: 1px solid #E0E0E0; box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } - table th, table td { border: solid #E0E0E0; border-width: 1px 0; padding: 8px 10px; } - table th { background-color: #E0E0E0; font-weight: bold; text-align: left; } + /** + * Formats an array as a string. + */ + private function formatArgs(array $args): string + { + $result = []; + foreach ($args as $key => $item) { + if ('object' === $item[0]) { + $formattedValue = sprintf('object(%s)', $this->abbrClass($item[1])); + } elseif ('array' === $item[0]) { + $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); + } elseif ('null' === $item[0]) { + $formattedValue = 'null'; + } elseif ('boolean' === $item[0]) { + $formattedValue = ''.strtolower(var_export($item[1], true)).''; + } elseif ('resource' === $item[0]) { + $formattedValue = 'resource'; + } else { + $formattedValue = str_replace("\n", '', $this->escape(var_export($item[1], true))); + } - .hidden-xs-down { display: none; } - .block { 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-muted { color: #999; } + $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escape($key), $formattedValue); + } - .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } - .container::after { content: ""; display: table; clear: both; } + return implode(', ', $result); + } - .exception-summary { background: #B0413E; border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, .3); flex: 0 0 auto; margin-bottom: 30px; } + private function formatArgsAsText($args) + { + return strip_tags($this->formatArgs($args)); + } - .exception-message-wrapper { display: flex; align-items: center; min-height: 70px; } - .exception-message { flex-grow: 1; padding: 30px 0; } - .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } - .exception-message.long { font-size: 18px; } - .exception-message a { border-bottom: 1px solid rgba(255, 255, 255, 0.5); font-size: inherit; text-decoration: none; } - .exception-message a:hover { border-bottom-color: #ffffff; } + private function escape(string $string): string + { + return htmlspecialchars($string, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + } - .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } + private function abbrClass(string $class): string + { + $parts = explode('\\', $class); + $short = array_pop($parts); - .trace + .trace { margin-top: 30px; } - .trace-head .trace-class { color: #222; font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } + return sprintf('%s', $class, $short); + } - .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } + private function getFileRelative(string $file): ?string + { + $file = str_replace('\\', '/', $file); - .trace-file-path, .trace-file-path a { color: #222; margin-top: 3px; font-size: 13px; } - .trace-class { color: #B0413E; } - .trace-type { padding: 0 2px; } - .trace-method { color: #B0413E; font-weight: bold; } - .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } + if (null !== $this->projectDir && 0 === strpos($file, $this->projectDir)) { + return ltrim(substr($file, \strlen($this->projectDir)), '/'); + } - @media (min-width: 575px) { - .hidden-xs-down { display: initial; } - } -EOF; + return null; } - private function formatClass($class): string + /** + * Returns the link for a given file/line pair. + * + * @return string|false A link or false + */ + private function getFileLink(string $file, int $line) { - $parts = explode('\\', $class); + if ($fmt = $this->fileLinkFormat) { + return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); + } - return sprintf('%s', $class, array_pop($parts)); + return false; } - private function formatPath(string $path, int $line): string + /** + * Formats a file path. + * + * @param string $file An absolute file path + * @param int $line The line number + * @param string $text Use this text for the link rather than the file path + */ + private function formatFile(string $file, int $line, string $text = null): string { - $file = $this->escapeHtml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path); - $fmt = $this->fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $file = trim($file); - if (!$fmt) { - return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); + if (null === $text) { + $text = $file; + if (null !== $rel = $this->getFileRelative($text)) { + $rel = explode('/', $rel, 2); + $text = sprintf('%s%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? '')); + } } - if (\is_string($fmt)) { - $i = strpos($f = $fmt, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f); - $fmt = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, PREG_SPLIT_DELIM_CAPTURE); - - for ($i = 1; isset($fmt[$i]); ++$i) { - if (0 === strpos($path, $k = $fmt[$i++])) { - $path = substr_replace($path, $fmt[$i], 0, \strlen($k)); - break; - } - } + if (0 < $line) { + $text .= ' at line '.$line; + } - $link = strtr($fmt[0], ['%f' => $path, '%l' => $line]); - } else { - try { - $link = $fmt->format($path, $line); - } catch (\Exception $e) { - return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); - } + if (false !== $link = $this->getFileLink($file, $line)) { + return sprintf('%s', $this->escape($link), $text); } - return sprintf('in %s%s', $this->escapeHtml($link), $file, 0 < $line ? ' line '.$line : ''); + return $text; } /** - * Formats an array as a string. + * Returns an excerpt of a code file around the given line number. + * + * @param string $file A file path + * @param int $line The selected line number + * @param int $srcContext The number of displayed lines around or -1 for the whole file + * + * @return string An HTML string */ - private function formatArgs(array $args): string + private function fileExcerpt(string $file, int $line, int $srcContext = 3): string { - $result = []; - foreach ($args as $key => $item) { - if ('object' === $item[0]) { - $formattedValue = sprintf('object(%s)', $this->formatClass($item[1])); - } elseif ('array' === $item[0]) { - $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); - } elseif ('null' === $item[0]) { - $formattedValue = 'null'; - } elseif ('boolean' === $item[0]) { - $formattedValue = ''.strtolower(var_export($item[1], true)).''; - } elseif ('resource' === $item[0]) { - $formattedValue = 'resource'; - } else { - $formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true))); + if (is_file($file) && is_readable($file)) { + // highlight_file could throw warnings + // see https://bugs.php.net/bug.php?id=25725 + $code = @highlight_file($file, true); + // remove main code/span tags + $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); + // split multiline spans + $code = preg_replace_callback('#]++)>((?:[^<]*+
)++[^<]*+)
#', function ($m) { + return "".str_replace('
', "

", $m[2]).''; + }, $code); + $content = explode('
', $code); + + $lines = []; + if (0 > $srcContext) { + $srcContext = \count($content); } - $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue); + for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) { + $lines[] = ''.$this->fixCodeMarkup($content[$i - 1]).''; + } + + return '
    '.implode("\n", $lines).'
'; } - return implode(', ', $result); + return ''; } - /** - * HTML-encodes a string. - */ - private function escapeHtml(string $str): string + private function fixCodeMarkup($line) + { + // ending tag from previous line + $opening = strpos($line, ''); + if (false !== $closing && (false === $opening || $closing < $opening)) { + $line = substr_replace($line, '', $closing, 7); + } + + // missing tag at the end of line + $opening = strpos($line, ''); + if (false !== $opening && (false === $closing || $closing > $opening)) { + $line .= ''; + } + + return trim($line); + } + + private function formatFileFromText($text) { - return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) { + return 'in '.$this->formatFile($match[2], $match[3]); + }, $text); } - private function getSymfonyGhostAsSvg(): string + private function formatLogMessage(string $message, array $context) { - return ''.$this->addElementToGhost().''; + if ($context && false !== strpos($message, '{')) { + $replacements = []; + foreach ($context as $key => $val) { + if (is_scalar($val)) { + $replacements['{'.$key.'}'] = $val; + } + } + + if ($replacements) { + $message = strtr($message, $replacements); + } + } + + return $this->escape($message); } private function addElementToGhost(): string @@ -333,4 +315,13 @@ private function addElementToGhost(): string return ''; } + + private function include(string $name, array $context = []): string + { + extract($context, EXTR_SKIP); + ob_start(); + include __DIR__.'/../Resources/'.$name; + + return trim(ob_get_clean()); + } } diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/css/error.css b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/error.css new file mode 100644 index 0000000000000..332d81876caf0 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/error.css @@ -0,0 +1,4 @@ +body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; } +.container { margin: 30px; max-width: 600px; } +h1 { color: #dc3545; font-size: 24px; } +h2 { font-size: 18px; } diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception.css b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception.css new file mode 100644 index 0000000000000..952c66d2fc936 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception.css @@ -0,0 +1,209 @@ +/* This file is based on WebProfilerBundle/Resources/views/Profiler/profiler.css.twig. + If you make any change in this file, verify the same change is needed in the other file. */ +:root { + --font-sans-serif: Helvetica, Arial, sans-serif; + --page-background: #f9f9f9; + --color-text: #222; + /* when updating any of these colors, do the same in toolbar.css.twig */ + --color-success: #4f805d; + --color-warning: #a46a1f; + --color-error: #b0413e; + --color-muted: #999; + --tab-background: #fff; + --tab-color: #444; + --tab-active-background: #666; + --tab-active-color: #fafafa; + --tab-disabled-background: #f5f5f5; + --tab-disabled-color: #999; + --metric-value-background: #fff; + --metric-value-color: inherit; + --metric-unit-color: #999; + --metric-label-background: #e0e0e0; + --metric-label-color: inherit; + --table-border: #e0e0e0; + --table-background: #fff; + --table-header: #e0e0e0; + --trace-selected-background: #F7E5A1; + --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; +} + +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} + +html { + /* always display the vertical scrollbar to avoid jumps when toggling contents */ + overflow-y: scroll; +} +body { background-color: #F9F9F9; color: var(--base-6); font: 14px/1.4 Helvetica, Arial, sans-serif; padding-bottom: 45px; } + +a { cursor: pointer; text-decoration: none; } +a:hover { text-decoration: underline; } +abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } + +code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } + +table, tr, th, td { background: #FFF; border-collapse: collapse; vertical-align: top; } +table { background: #FFF; border: var(--border); box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } +table th, table td { border: solid var(--base-2); border-width: 1px 0; padding: 8px 10px; } +table th { background-color: var(--base-2); font-weight: bold; text-align: left; } + +.m-t-5 { margin-top: 5px; } +.hidden-xs-down { display: none; } +.block { display: block; } +.full-width { width: 100%; } +.hidden { display: none; } +.prewrap { white-space: pre-wrap; } +.nowrap { white-space: nowrap; } +.newline { display: block; } +.break-long-words { word-wrap: break-word; overflow-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; min-width: 0; } +.text-small { font-size: 12px !important; } +.text-muted { color: #999; } +.text-bold { font-weight: bold; } +.empty { border: 4px dashed var(--base-2); color: #999; margin: 1em 0; padding: .5em 2em; } + +.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 #FAFAFA; border-top: 1px solid #FAFAFA; } +.status-warning .colored { color: #A46A1F; } +.status-error .colored { color: var(--color-error); } + +.sf-toggle { cursor: pointer; } +.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; } +thead.sf-toggle-content.sf-toggle-visible, tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; } +.sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; } +.sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; } + +.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; } + +[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; } + +.container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } +.container::after { content: ""; display: table; clear: both; } + +header { background-color: var(--base-6); color: rgba(255, 255, 255, 0.75); font-size: 13px; height: 33px; line-height: 33px; padding: 0; } +header .container { display: flex; justify-content: space-between; } +.logo { flex: 1; font-size: 13px; font-weight: normal; margin: 0; padding: 0; } +.logo svg { height: 18px; width: 18px; opacity: .8; vertical-align: -5px; } + +.help-link { margin-left: 15px; } +.help-link a { color: inherit; } +.help-link .icon svg { height: 15px; width: 15px; opacity: .7; vertical-align: -2px; } +.help-link a:hover { color: #EEE; text-decoration: none; } +.help-link a:hover svg { opacity: .9; } + +.exception-summary { background: var(--background-error); border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, .3); flex: 0 0 auto; margin-bottom: 15px; } +.exception-metadata { background: rgba(0, 0, 0, 0.1); padding: 7px 0; } +.exception-metadata .container { display: flex; flex-direction: row; justify-content: space-between; } +.exception-metadata h2, .exception-metadata h2 > a { color: rgba(255, 255, 255, 0.8); font-size: 13px; font-weight: 400; margin: 0; } +.exception-http small { font-size: 13px; opacity: .7; } +.exception-hierarchy { flex: 1; } +.exception-hierarchy .icon { margin: 0 3px; opacity: .7; } +.exception-hierarchy .icon svg { height: 13px; width: 13px; vertical-align: -2px; } + +.exception-without-message .exception-message-wrapper { display: none; } +.exception-message-wrapper .container { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 15px 8px; } +.exception-message { flex-grow: 1; } +.exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } +.exception-message.long { font-size: 18px; } +.exception-message a { border-bottom: 1px solid rgba(255, 255, 255, 0.5); font-size: inherit; text-decoration: none; } +.exception-message a:hover { border-bottom-color: #ffffff; } + +.exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } + +.trace + .trace { margin-top: 30px; } +.trace-head { background-color: var(--base-2); padding: 10px; position: relative; } +.trace-head .trace-class { color: var(--base-6); font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } +.trace-head .trace-namespace { color: #999; display: block; font-size: 13px; } +.trace-head .icon { position: absolute; right: 0; top: 0; } +.trace-head .icon svg { height: 24px; width: 24px; } + +.trace-details { background: var(--base-0); border: var(--border); box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; table-layout: fixed; } + +.trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } + +.trace-line { position: relative; padding-top: 8px; padding-bottom: 8px; } +.trace-line + .trace-line { border-top: var(--border); } +.trace-line:hover { background: var(--base-1); } +.trace-line a { color: var(--base-6); } +.trace-line .icon { opacity: .4; position: absolute; left: 10px; top: 11px; } +.trace-line .icon svg { height: 16px; width: 16px; } +.trace-line-header { padding-left: 36px; padding-right: 10px; } + +.trace-file-path, .trace-file-path a { color: var(--base-6); font-size: 13px; } +.trace-class { color: var(--color-error); } +.trace-type { padding: 0 2px; } +.trace-method { color: var(--color-error); font-weight: bold; } +.trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } + +.trace-code { background: var(--base-0); font-size: 12px; margin: 10px 10px 2px 10px; padding: 10px; overflow-x: auto; white-space: nowrap; } +.trace-code ol { margin: 0; float: left; } +.trace-code li { color: #969896; margin: 0; padding-left: 10px; float: left; width: 100%; } +.trace-code li + li { margin-top: 5px; } +.trace-code li.selected { background: var(--trace-selected-background); margin-top: 2px; } +.trace-code li code { color: var(--base-6); white-space: nowrap; } + +.trace-as-text .stacktrace { line-height: 1.8; margin: 0 0 15px; white-space: pre-wrap; } + +@media (min-width: 575px) { + .hidden-xs-down { display: initial; } + .help-link { margin-left: 30px; } +} diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception_full.css b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception_full.css new file mode 100644 index 0000000000000..fa77cb324951c --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/css/exception_full.css @@ -0,0 +1,128 @@ +.sf-reset .traces { + padding-bottom: 14px; +} +.sf-reset .traces li { + font-size: 12px; + color: #868686; + padding: 5px 4px; + list-style-type: decimal; + margin-left: 20px; +} +.sf-reset #logs .traces li.error { + font-style: normal; + color: #AA3333; + background: #f9ecec; +} +.sf-reset #logs .traces li.warning { + font-style: normal; + background: #ffcc00; +} +/* fix for Opera not liking empty
  • */ +.sf-reset .traces li:after { + content: "\00A0"; +} +.sf-reset .trace { + border: 1px solid #D3D3D3; + padding: 10px; + overflow: auto; + margin: 10px 0 20px; +} +.sf-reset .block-exception { + -moz-border-radius: 16px; + -webkit-border-radius: 16px; + border-radius: 16px; + margin-bottom: 20px; + background-color: #f6f6f6; + border: 1px solid #dfdfdf; + padding: 30px 28px; + word-wrap: break-word; + overflow: hidden; +} +.sf-reset .block-exception div { + color: #313131; + font-size: 10px; +} +.sf-reset .block-exception-detected .illustration-exception, +.sf-reset .block-exception-detected .text-exception { + float: left; +} +.sf-reset .block-exception-detected .illustration-exception { + width: 152px; +} +.sf-reset .block-exception-detected .text-exception { + width: 670px; + padding: 30px 44px 24px 46px; + position: relative; +} +.sf-reset .text-exception .open-quote, +.sf-reset .text-exception .close-quote { + font-family: Arial, Helvetica, sans-serif; + position: absolute; + color: #C9C9C9; + font-size: 8em; +} +.sf-reset .open-quote { + top: 0; + left: 0; +} +.sf-reset .close-quote { + bottom: -0.5em; + right: 50px; +} +.sf-reset .block-exception p { + font-family: Arial, Helvetica, sans-serif; +} +.sf-reset .block-exception p a, +.sf-reset .block-exception p a:hover { + color: #565656; +} +.sf-reset .logs h2 { + float: left; + width: 654px; +} +.sf-reset .error-count, .sf-reset .support { + float: right; + width: 170px; + text-align: right; +} +.sf-reset .error-count span { + display: inline-block; + background-color: #aacd4e; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; + padding: 4px; + color: white; + margin-right: 2px; + font-size: 11px; + font-weight: bold; +} + +.sf-reset .support a { + display: inline-block; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; + padding: 4px; + color: #000000; + margin-right: 2px; + font-size: 11px; + font-weight: bold; +} + +.sf-reset .toggle { + vertical-align: middle; +} +.sf-reset .linked ul, +.sf-reset .linked li { + display: inline; +} +.sf-reset #output-content { + color: #000; + font-size: 12px; +} +.sf-reset #traces-text pre { + white-space: pre; + font-size: 12px; + font-family: monospace; +} diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/chevron-right.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/chevron-right.svg new file mode 100644 index 0000000000000..6837aff18bd20 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/chevron-right.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/favicon.png.base64 b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/favicon.png.base64 new file mode 100644 index 0000000000000..fb076ed16d0ae --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/favicon.png.base64 @@ -0,0 +1 @@ +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAgCAYAAAABtRhCAAADVUlEQVRIx82XX0jTURTHLYPyqZdefQx66CEo80+aYpoIkqzUikz6Z5klQoWUWYRIJYEUGpQ+lIr9U5dOTLdCtkmWZis3rbnC5fw/neYW002307mX/cZvP3/7o1PwwOdh95x7vnf39zvnd29AgBer2xO6DclAXiMqZAqxIiNIN/IYSUS2BPhjmGATchUxI+ADWiRhpWK7HKuHFVBFdmU5YvnI4grFGCaReF/EBH4KsZlGgj2JBTuCYBWRIYF8YoEOJ6wBt/gEs7mBbyOjQXruPLSdOgPCiEiPSUUHDoL8Ug5IUo9B/d5wrt+G7OAKNrODPuVdB6vRCIzN6SdBlpW9RIgk/1FeAXabzRlrUPVCS/JhbmwudztnGeeH9AyXBIwtmM3wLinZJZHifjHw2V+NBoRh+9ixQrbgbnaSIcl7cGea6hoXQbNe7za241oeO5Z0p42M4BV2EqP2D50wo+6HzvwC6C4sApNOR8cmOrtcnhtj2kYRyC9eBvXzKrBZrXSs72kFd1t3MoKVbMekQkEnSNKOO8fac3LpmK6l1TlGtsxmsdKFsecPYgwxst0cwROMYDXboSotg0WLBRqjY51jLYcENElXwW2XJKPydvoI2GN9T8rBtrAArYIUruBJXkFheCQYlCpQP6uk5dAQFQNaUROMSGVQFxLmkoQsxDJrhLbTZ+nvVsERME9MgPJRKV/58AsyomTSzE813WLFvWK++qI0xSfQl8k8Pg46sYRuv5t6dS+4RqxDwaa4BGjYH+NTQvKScIp9+YL/hoZh3jDtLRHtt2C3g6bmhX+CpsFBWg7ilDSPgj0lD2ncr5ev/BP8VvyAJhqVyZeUhPOrEhEFxgEtjft846Z/guQTNT89Q5P9flMLoth4F7808wKtWWKzAwNQHxrh/1vaid2F+XpYTSbQf1XA2McOmOpROnvpvMEA4tSjq1cW0sws2gCYxswY6TKkvzYnJq1NHZLnRU4BX+4U0uburvusu8Kv8iHY7qefkM4IFngJHEOUXmLEPgiGsI8YnlZILit3vSSLRTQe/MPIZva5pshNIEmyFQlCvruJKXPkCEfmePzkphXHdzZNQdoRI9KPlBAxlj/I8U97ERPS5bjGbWDFbEdqHVe5caTBeZZx2H/IMvzeN15yoQAAAABJRU5ErkJggg== diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-book.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-book.svg new file mode 100644 index 0000000000000..498a74f401e32 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-book.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square-o.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square-o.svg new file mode 100644 index 0000000000000..be534ad44037c --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square-o.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square.svg new file mode 100644 index 0000000000000..471c2741c7fd7 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-minus-square.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square-o.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square-o.svg new file mode 100644 index 0000000000000..b2593a9fe79e9 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square-o.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square.svg new file mode 100644 index 0000000000000..2f5c3b3583076 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-plus-square.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-support.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-support.svg new file mode 100644 index 0000000000000..03fd8e7678baf --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/icon-support.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-ghost.svg.php b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-ghost.svg.php new file mode 100644 index 0000000000000..1a245e3f98656 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-ghost.svg.php @@ -0,0 +1 @@ +addElementToGhost() ?> diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-logo.svg b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-logo.svg new file mode 100644 index 0000000000000..f10824ae96f6a --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/images/symfony-logo.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/assets/js/exception.js b/src/Symfony/Component/ErrorRenderer/Resources/assets/js/exception.js new file mode 100644 index 0000000000000..8cc7b5318449a --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/assets/js/exception.js @@ -0,0 +1,279 @@ +/* This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. + If you make any change in this file, verify the same change is needed in the other file. */ +/* .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 '
  • ' 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(); + }); + } + + toggles[i].setAttribute('data-processed', 'true'); + } + }, + + createFilters: function() { + document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { + var filters = filter.closest('[data-filters]'), + type = 'choice', + name = filter.dataset.filter, + ucName = name.charAt(0).toUpperCase()+name.slice(1), + list = document.createElement('ul'), + values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), + labels = {}, + defaults = null, + indexed = {}, + processed = {}; + if (typeof values === 'string') { + type = 'level'; + labels = values.split(','); + values = values.toLowerCase().split(','); + defaults = values.length - 1; + } + addClass(list, 'filter-list'); + addClass(list, 'filter-list-'+type); + values.forEach(function (value, i) { + if (value instanceof HTMLElement) { + value = value.dataset['filter'+ucName]; + } + if (value in processed) { + return; + } + var option = document.createElement('li'), + label = i in labels ? labels[i] : value, + active = false, + matches; + if ('' === label) { + option.innerHTML = '(none)'; + } else { + option.innerText = label; + } + option.dataset.filter = value; + option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); + indexed[value] = i; + list.appendChild(option); + addEventListener(option, 'click', function () { + if ('choice' === type) { + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (option.dataset.filter === row.dataset['filter'+ucName]) { + toggleClass(row, 'filter-hidden-'+name); + } + }); + toggleClass(option, 'active'); + } else if ('level' === type) { + if (i === this.parentNode.querySelectorAll('.active').length - 1) { + return; + } + this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { + if (j <= i) { + addClass(currentOption, 'active'); + if (i === j) { + addClass(currentOption, 'last-active'); + } else { + removeClass(currentOption, 'last-active'); + } + } else { + removeClass(currentOption, 'active'); + removeClass(currentOption, 'last-active'); + } + }); + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (i < indexed[row.dataset['filter'+ucName]]) { + addClass(row, 'filter-hidden-'+name); + } else { + removeClass(row, 'filter-hidden-'+name); + } + }); + } + }); + if ('choice' === type) { + active = null === defaults || 0 <= defaults.indexOf(value); + } else if ('level' === type) { + active = i <= defaults; + if (active && i === defaults) { + addClass(option, 'last-active'); + } + } + if (active) { + addClass(option, 'active'); + } else { + filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { + toggleClass(row, 'filter-hidden-'+name); + }); + } + processed[value] = true; + }); + + if (1 < list.childNodes.length) { + filter.appendChild(list); + filter.dataset.filtered = ''; + } + }); + } + }; +})(); + +Sfjs.addEventListener(document, 'DOMContentLoaded', function() { + Sfjs.createTabs(); + Sfjs.createToggles(); + Sfjs.createFilters(); +}); + +/*]]>*/ diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/error.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/error.html.php new file mode 100644 index 0000000000000..8981b1739941f --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/error.html.php @@ -0,0 +1,20 @@ + + + + + + An Error Occurred: <?= $statusText ?> + + + +
    +

    Oops! An Error Occurred

    +

    The server returned a " ".

    + +

    + Something is broken. Please let us know what you were doing when this error occurred. + We will fix it as soon as possible. Sorry for any inconvenience caused. +

    +
    + + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/exception.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/exception.html.php new file mode 100644 index 0000000000000..1dd6bd2b9c899 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/exception.html.php @@ -0,0 +1,116 @@ +
    + + +
    +
    +

    formatFileFromText(nl2br($exceptionMessage)) ?>

    + +
    + include('assets/images/symfony-ghost.svg.php') ?> +
    +
    +
    +
    + +
    +
    +
    + toArray(); + $exceptionWithUserCode = []; + $exceptionAsArrayCount = \count($exceptionAsArray); + $last = \count($exceptionAsArray) - 1; + foreach ($exceptionAsArray as $i => $e) { + foreach ($e['trace'] as $trace) { + if ($trace['file'] && false === mb_strpos($trace['file'], '/vendor/') && false === mb_strpos($trace['file'], '/var/cache/') && $i < $last) { + $exceptionWithUserCode[] = $i; + } + } + } + ?> +

    + 1) { ?> + Exceptions + + Exception + +

    + +
    + $e) { + echo $this->include('views/traces.html.php', [ + 'exception' => $e, + 'index' => $i + 1, + 'expand' => \in_array($i, $exceptionWithUserCode, true) || ([] === $exceptionWithUserCode && 0 === $i), + ]); + } + ?> +
    +
    + + +
    +

    + Logs + countErrors()) { ?>countErrors() ?> +

    + +
    + getLogs()) { ?> + include('views/logs.html.php', ['logs' => $logger->getLogs()]) ?> + +
    +

    No log messages

    +
    + +
    +
    + + +
    +

    + 1) { ?> + Stack Traces + + Stack Trace + +

    + +
    + $e) { + echo $this->include('views/traces_text.html.php', [ + 'exception' => $e, + 'index' => $i + 1, + 'numExceptions' => $exceptionAsArrayCount, + ]); + } + ?> +
    +
    + + +
    +

    Output content

    + +
    + +
    +
    + +
    +
    diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/exception_full.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/exception_full.html.php new file mode 100644 index 0000000000000..09ed0cb57ec71 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/exception_full.html.php @@ -0,0 +1,41 @@ + + + + + + + + <?= $_message ?> + + + + + +
    + +
    + + include('views/exception.html.php', $context) ?> + + + + + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/logs.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/logs.html.php new file mode 100644 index 0000000000000..ccd64f7f14b47 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/logs.html.php @@ -0,0 +1,42 @@ + + + + + + + + + + + + = 400) { + $status = 'error'; + } elseif ($log['priority'] >= 300) { + $status = 'warning'; + } else { + $severity = $log['context']['exception']['severity'] ?? false; + $status = E_DEPRECATED === $severity || E_USER_DEPRECATED === $severity ? 'warning' : 'normal'; + } ?> + data-filter-channel="escape($log['channel']) ?>" + + + + + + + + +
    LevelChannelMessage
    + escape($log['priorityName']) ?> + format('H:i:s') ?> + + escape($log['channel']) ?> + + formatLogMessage($log['message'], $log['context']) ?> + +
    + +
    diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/trace.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/trace.html.php new file mode 100644 index 0000000000000..08aa0d2bde2eb --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/trace.html.php @@ -0,0 +1,40 @@ +
    + + include('assets/images/icon-minus-square.svg') ?> + include('assets/images/icon-plus-square.svg') ?> + + + + abbrClass($trace['class']) ?>(formatArgs($trace['args']) ?>) + + + + getFileLink($trace['file'], $lineNumber); + $filePath = strtr(strip_tags($this->formatFile($trace['file'], $lineNumber)), [' at line '.$lineNumber => '']); + $filePathParts = explode(DIRECTORY_SEPARATOR, $filePath); + ?> + + in + + + + + + + + (line ) + + +
    + +
    + fileExcerpt($trace['file'], $trace['line'], 5), [ + '#DD0000' => 'var(--highlight-string)', + '#007700' => 'var(--highlight-keyword)', + '#0000BB' => 'var(--highlight-default)', + '#FF8000' => 'var(--highlight-comment)', + ]) ?> +
    + diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/traces.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/traces.html.php new file mode 100644 index 0000000000000..40b5207c7dfa2 --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/traces.html.php @@ -0,0 +1,42 @@ +
    +
    +
    + +

    + include('assets/images/icon-minus-square-o.svg') ?> + include('assets/images/icon-plus-square-o.svg') ?> + + + 1 ? '\\' : '' ?> + + +

    + + 1) { ?> +

    escape($exception['message']) ?>

    + +
    +
    + +
    + $trace) { + $isVendorTrace = $trace['file'] && (false !== mb_strpos($trace['file'], '/vendor/') || false !== mb_strpos($trace['file'], '/var/cache/')); + $displayCodeSnippet = $isFirstUserCode && !$isVendorTrace; + if ($displayCodeSnippet) { + $isFirstUserCode = false; + } ?> +
    + include('views/trace.html.php', [ + 'prefix' => $index, + 'i' => $i, + 'trace' => $trace, + 'style' => $isVendorTrace ? 'compact' : ($displayCodeSnippet ? 'expanded' : ''), + ]) ?> +
    + +
    +
    +
    diff --git a/src/Symfony/Component/ErrorRenderer/Resources/views/traces_text.html.php b/src/Symfony/Component/ErrorRenderer/Resources/views/traces_text.html.php new file mode 100644 index 0000000000000..a247ad5fd35db --- /dev/null +++ b/src/Symfony/Component/ErrorRenderer/Resources/views/traces_text.html.php @@ -0,0 +1,43 @@ + + + + + + + + + + + + +
    +

    + 1) { ?> + [/] + + + include('assets/images/icon-minus-square-o.svg') ?> + include('assets/images/icon-plus-square-o.svg') ?> +

    +
    + +
    +formatArgsAsText($trace['args']).')';
    +                        }
    +                        if ($trace['file'] && $trace['line']) {
    +                            echo ($trace['function'] ? "\n     (" : 'at ').strtr(strip_tags($this->formatFile($trace['file'], $trace['line'])), [' at line '.$trace['line'] => '']).':'.$trace['line'].($trace['function'] ? ')' : '');
    +                        }
    +                    }
    +?>
    +                
    + +
    diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php index 6b9a53b04aa0b..e1c55d395780d 100644 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php +++ b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php @@ -20,7 +20,7 @@ class HtmlErrorRendererTest extends TestCase public function testRender() { $exception = FlattenException::createFromThrowable(new \RuntimeException('Foo')); - $expected = '%A%A%AInternal Server Error%A

    Foo

    %ARuntimeException%A'; + $expected = '%A%A%AFoo (500 Internal Server Error)%A'; $this->assertStringMatchesFormat($expected, (new HtmlErrorRenderer(true))->render($exception)); } pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy