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

$title

Chris@17: Chris@17: EOF; Chris@17: } Chris@17: Chris@0: $content = ''; Chris@17: try { Chris@17: $count = \count($exception->getAllPrevious()); Chris@17: $total = $count + 1; Chris@17: foreach ($exception->toArray() as $position => $e) { Chris@17: $ind = $count - $position + 1; Chris@17: $class = $this->formatClass($e['class']); Chris@17: $message = nl2br($this->escapeHtml($e['message'])); Chris@17: $content .= sprintf(<<<'EOF' Chris@17:
Chris@17: Chris@17: Chris@17: Chris@0: EOF Chris@17: , $ind, $total, $class, $message); Chris@17: foreach ($e['trace'] as $trace) { Chris@17: $content .= '\n"; Chris@17: } Chris@0: Chris@17: $content .= "\n
Chris@17:

Chris@17: (%d/%d) Chris@17: %s Chris@17:

Chris@17:

%s

Chris@17:
'; Chris@17: if ($trace['function']) { Chris@17: $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); Chris@0: } Chris@17: if (isset($trace['file']) && isset($trace['line'])) { Chris@17: $content .= $this->formatPath($trace['file'], $trace['line']); Chris@17: } Chris@17: $content .= "
\n
\n"; Chris@17: } Chris@17: } catch (\Exception $e) { Chris@17: // something nasty happened and we cannot throw an exception anymore Chris@17: if ($this->debug) { Chris@17: $title = sprintf('Exception thrown when handling an exception (%s: %s)', \get_class($e), $this->escapeHtml($e->getMessage())); Chris@17: } else { Chris@17: $title = 'Whoops, looks like something went wrong.'; Chris@0: } Chris@0: } Chris@0: Chris@12: $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); Chris@12: Chris@0: return << Chris@12:
Chris@12:
Chris@12:

$title

Chris@12:
$symfonyGhostImageContents
Chris@12:
Chris@12:
Chris@12: Chris@12: Chris@12:
Chris@0: $content Chris@0:
Chris@0: EOF; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the stylesheet associated with the given exception. Chris@0: * Chris@0: * @return string The stylesheet as a string Chris@0: */ Chris@0: public function getStylesheet(FlattenException $exception) Chris@0: { Chris@17: if (!$this->debug) { Chris@17: return <<<'EOF' Chris@17: body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; } Chris@17: .container { margin: 30px; max-width: 600px; } Chris@17: h1 { color: #dc3545; font-size: 24px; } Chris@17: EOF; Chris@17: } Chris@17: Chris@0: return <<<'EOF' Chris@12: body { background-color: #F9F9F9; color: #222; font: 14px/1.4 Helvetica, Arial, sans-serif; margin: 0; padding-bottom: 45px; } Chris@12: Chris@12: a { cursor: pointer; text-decoration: none; } Chris@12: a:hover { text-decoration: underline; } Chris@12: abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } Chris@12: Chris@12: code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } Chris@12: Chris@12: table, tr, th, td { background: #FFF; border-collapse: collapse; vertical-align: top; } Chris@12: table { background: #FFF; border: 1px solid #E0E0E0; box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } Chris@12: table th, table td { border: solid #E0E0E0; border-width: 1px 0; padding: 8px 10px; } Chris@12: table th { background-color: #E0E0E0; font-weight: bold; text-align: left; } Chris@12: Chris@12: .hidden-xs-down { display: none; } Chris@12: .block { display: block; } Chris@12: .break-long-words { -ms-word-break: break-all; word-break: break-all; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; } Chris@12: .text-muted { color: #999; } Chris@12: Chris@12: .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } Chris@12: .container::after { content: ""; display: table; clear: both; } Chris@12: Chris@12: .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; } Chris@12: Chris@12: .exception-message-wrapper { display: flex; align-items: center; min-height: 70px; } Chris@12: .exception-message { flex-grow: 1; padding: 30px 0; } Chris@12: .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } Chris@12: .exception-message.long { font-size: 18px; } Chris@12: .exception-message a { border-bottom: 1px solid rgba(255, 255, 255, 0.5); font-size: inherit; text-decoration: none; } Chris@12: .exception-message a:hover { border-bottom-color: #ffffff; } Chris@12: Chris@12: .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } Chris@12: Chris@12: .trace + .trace { margin-top: 30px; } Chris@12: .trace-head .trace-class { color: #222; font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } Chris@12: Chris@12: .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } Chris@12: Chris@12: .trace-file-path, .trace-file-path a { color: #222; margin-top: 3px; font-size: 13px; } Chris@12: .trace-class { color: #B0413E; } Chris@12: .trace-type { padding: 0 2px; } Chris@12: .trace-method { color: #B0413E; font-weight: bold; } Chris@12: .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } Chris@12: Chris@12: @media (min-width: 575px) { Chris@12: .hidden-xs-down { display: initial; } Chris@0: } Chris@0: EOF; Chris@0: } Chris@0: Chris@0: private function decorate($content, $css) Chris@0: { Chris@0: return << Chris@0: Chris@0: Chris@0: Chris@0: Chris@12: Chris@0: Chris@12: Chris@0: $content Chris@0: Chris@0: Chris@0: EOF; Chris@0: } Chris@0: Chris@0: private function formatClass($class) Chris@0: { Chris@0: $parts = explode('\\', $class); Chris@0: Chris@0: return sprintf('%s', $class, array_pop($parts)); Chris@0: } Chris@0: Chris@0: private function formatPath($path, $line) Chris@0: { Chris@0: $file = $this->escapeHtml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path); Chris@16: $fmt = $this->fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); Chris@0: Chris@16: if (!$fmt) { Chris@18: return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); Chris@0: } Chris@0: Chris@16: if (\is_string($fmt)) { Chris@17: $i = strpos($f = $fmt, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f); Chris@17: $fmt = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, PREG_SPLIT_DELIM_CAPTURE); Chris@16: Chris@16: for ($i = 1; isset($fmt[$i]); ++$i) { Chris@16: if (0 === strpos($path, $k = $fmt[$i++])) { Chris@17: $path = substr_replace($path, $fmt[$i], 0, \strlen($k)); Chris@16: break; Chris@16: } Chris@16: } Chris@16: Chris@17: $link = strtr($fmt[0], ['%f' => $path, '%l' => $line]); Chris@16: } else { Chris@18: try { Chris@18: $link = $fmt->format($path, $line); Chris@18: } catch (\Exception $e) { Chris@18: return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); Chris@18: } Chris@16: } Chris@16: Chris@16: return sprintf('in %s%s', $this->escapeHtml($link), $file, 0 < $line ? ' line '.$line : ''); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Formats an array as a string. Chris@0: * Chris@0: * @param array $args The argument array Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: private function formatArgs(array $args) Chris@0: { Chris@17: $result = []; Chris@0: foreach ($args as $key => $item) { Chris@0: if ('object' === $item[0]) { Chris@0: $formattedValue = sprintf('object(%s)', $this->formatClass($item[1])); Chris@0: } elseif ('array' === $item[0]) { Chris@17: $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); Chris@0: } elseif ('null' === $item[0]) { Chris@0: $formattedValue = 'null'; Chris@0: } elseif ('boolean' === $item[0]) { Chris@0: $formattedValue = ''.strtolower(var_export($item[1], true)).''; Chris@0: } elseif ('resource' === $item[0]) { Chris@0: $formattedValue = 'resource'; Chris@0: } else { Chris@0: $formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true))); Chris@0: } Chris@0: Chris@17: $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue); Chris@0: } Chris@0: Chris@0: return implode(', ', $result); Chris@0: } Chris@0: Chris@0: /** Chris@0: * HTML-encodes a string. Chris@0: */ Chris@0: private function escapeHtml($str) Chris@0: { Chris@0: return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); Chris@0: } Chris@12: Chris@12: private function getSymfonyGhostAsSvg() Chris@12: { Chris@18: return ''; Chris@12: } Chris@0: }