* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\Debug\Exception\OutOfMemoryException; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** * ExceptionHandler converts an exception to a Response object. * * It is mostly useful in debug mode to replace the default PHP/XDebug * output with something prettier and more useful. * * As this class is mainly used during Kernel boot, where nothing is yet * available, the Response content is always HTML. * * @author Fabien Potencier * @author Nicolas Grekas */ class ExceptionHandler { private $debug; private $charset; private $handler; private $caughtBuffer; private $caughtLength; private $fileLinkFormat; public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null) { $this->debug = $debug; $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; $this->fileLinkFormat = $fileLinkFormat; } /** * Registers the exception handler. * * @param bool $debug Enable/disable debug mode, where the stack trace is displayed * @param string|null $charset The charset used by exception messages * @param string|null $fileLinkFormat The IDE link template * * @return static */ public static function register($debug = true, $charset = null, $fileLinkFormat = null) { $handler = new static($debug, $charset, $fileLinkFormat); $prev = set_exception_handler([$handler, 'handle']); if (\is_array($prev) && $prev[0] instanceof ErrorHandler) { restore_exception_handler(); $prev[0]->setExceptionHandler([$handler, 'handle']); } return $handler; } /** * Sets a user exception handler. * * @param callable $handler An handler that will be called on Exception * * @return callable|null The previous exception handler if any */ public function setHandler(callable $handler = null) { $old = $this->handler; $this->handler = $handler; return $old; } /** * Sets the format for links to source files. * * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files * * @return string The previous file link format */ public function setFileLinkFormat($fileLinkFormat) { $old = $this->fileLinkFormat; $this->fileLinkFormat = $fileLinkFormat; return $old; } /** * Sends a response for the given Exception. * * To be as fail-safe as possible, the exception is first handled * by our simple exception handler, then by the user exception handler. * The latter takes precedence and any output from the former is cancelled, * if and only if nothing bad happens in this handling path. */ public function handle(\Exception $exception) { if (null === $this->handler || $exception instanceof OutOfMemoryException) { $this->sendPhpResponse($exception); return; } $caughtLength = $this->caughtLength = 0; ob_start(function ($buffer) { $this->caughtBuffer = $buffer; return ''; }); $this->sendPhpResponse($exception); while (null === $this->caughtBuffer && ob_end_flush()) { // Empty loop, everything is in the condition } if (isset($this->caughtBuffer[0])) { ob_start(function ($buffer) { if ($this->caughtLength) { // use substr_replace() instead of substr() for mbstring overloading resistance $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); if (isset($cleanBuffer[0])) { $buffer = $cleanBuffer; } } return $buffer; }); echo $this->caughtBuffer; $caughtLength = ob_get_length(); } $this->caughtBuffer = null; try { ($this->handler)($exception); $this->caughtLength = $caughtLength; } catch (\Exception $e) { if (!$caughtLength) { // All handlers failed. Let PHP handle that now. throw $exception; } } } /** * Sends the error associated with the given Exception as a plain PHP response. * * This method uses plain PHP functions like header() and echo to output * the response. * * @param \Exception|FlattenException $exception An \Exception or FlattenException instance */ public function sendPhpResponse($exception) { if (!$exception instanceof FlattenException) { $exception = FlattenException::create($exception); } if (!headers_sent()) { header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); foreach ($exception->getHeaders() as $name => $value) { header($name.': '.$value, false); } header('Content-Type: text/html; charset='.$this->charset); } echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); } /** * Gets the full HTML content associated with the given exception. * * @param \Exception|FlattenException $exception An \Exception or FlattenException instance * * @return string The HTML content as a string */ public function getHtml($exception) { if (!$exception instanceof FlattenException) { $exception = FlattenException::create($exception); } return $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); } /** * Gets the HTML content associated with the given exception. * * @return string The content as a string */ public function getContent(FlattenException $exception) { switch ($exception->getStatusCode()) { case 404: $title = 'Sorry, the page you are looking for could not be found.'; break; default: $title = 'Whoops, looks like something went wrong.'; } if (!$this->debug) { return <<

$title

EOF; } $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"; } $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']) && isset($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::create($e); $title = sprintf('Exception thrown when handling an exception (%s: %s)', $e->getClass(), $this->escapeHtml($e->getMessage())); } else { $title = 'Whoops, looks like something went wrong.'; } } $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); return <<

$title

$symfonyGhostImageContents
$content
EOF; } /** * Gets the stylesheet associated with the given exception. * * @return string The stylesheet as a string */ public function getStylesheet(FlattenException $exception) { 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; } EOF; } 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; } 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: 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; } .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; } .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } .container::after { content: ""; display: table; clear: both; } .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; } .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; } .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } .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; } .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } .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; } @media (min-width: 575px) { .hidden-xs-down { display: initial; } } EOF; } private function decorate($content, $css) { return << $content EOF; } private function formatClass($class) { $parts = explode('\\', $class); return sprintf('%s', $class, array_pop($parts)); } private function formatPath($path, $line) { $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'); if (!$fmt) { return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); } 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; } } $link = strtr($fmt[0], ['%f' => $path, '%l' => $line]); } else { $link = $fmt->format($path, $line); } return sprintf('in %s%s', $this->escapeHtml($link), $file, 0 < $line ? ' line '.$line : ''); } /** * Formats an array as a string. * * @param array $args The argument array * * @return string */ private function formatArgs(array $args) { $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))); } $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue); } return implode(', ', $result); } /** * HTML-encodes a string. */ private function escapeHtml($str) { return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); } private function getSymfonyGhostAsSvg() { return ''; } }