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@0: $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); 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@0: $prev = set_exception_handler(array($handler, 'handle')); Chris@0: if (is_array($prev) && $prev[0] instanceof ErrorHandler) { Chris@0: restore_exception_handler(); Chris@0: $prev[0]->setExceptionHandler(array($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@0: 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: * @param FlattenException $exception A FlattenException instance 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@0: $content = ''; Chris@0: if ($this->debug) { Chris@0: try { Chris@0: $count = count($exception->getAllPrevious()); Chris@0: $total = $count + 1; Chris@0: foreach ($exception->toArray() as $position => $e) { Chris@0: $ind = $count - $position + 1; Chris@0: $class = $this->formatClass($e['class']); Chris@0: $message = nl2br($this->escapeHtml($e['message'])); Chris@0: $content .= sprintf(<<<'EOF' Chris@0:

Chris@0: %d/%d Chris@0: %s%s: Chris@0: %s Chris@0:

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

$title

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: * @param FlattenException $exception A FlattenException instance Chris@0: * Chris@0: * @return string The stylesheet as a string Chris@0: */ Chris@0: public function getStylesheet(FlattenException $exception) Chris@0: { Chris@0: return <<<'EOF' Chris@0: .sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 } Chris@0: .sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; } Chris@0: .sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; } Chris@0: .sf-reset .clear_fix { display:inline-block; } Chris@0: .sf-reset * html .clear_fix { height:1%; } Chris@0: .sf-reset .clear_fix { display:block; } Chris@0: .sf-reset, .sf-reset .block { margin: auto } Chris@0: .sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; } Chris@0: .sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px } Chris@0: .sf-reset strong { font-weight:bold; } Chris@0: .sf-reset a { color:#6c6159; cursor: default; } Chris@0: .sf-reset a img { border:none; } Chris@0: .sf-reset a:hover { text-decoration:underline; } Chris@0: .sf-reset em { font-style:italic; } Chris@0: .sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif } Chris@0: .sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; } Chris@0: .sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; } Chris@0: .sf-reset .exception_message { margin-left: 3em; display: block; } Chris@0: .sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; } Chris@0: .sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px; Chris@0: border-bottom-right-radius: 16px; Chris@0: border-bottom-left-radius: 16px; Chris@0: border-bottom:1px solid #ccc; Chris@0: border-right:1px solid #ccc; Chris@0: border-left:1px solid #ccc; Chris@0: word-wrap: break-word; Chris@0: } Chris@0: .sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px; Chris@0: border-top-left-radius: 16px; Chris@0: border-top-right-radius: 16px; Chris@0: border-top:1px solid #ccc; Chris@0: border-right:1px solid #ccc; Chris@0: border-left:1px solid #ccc; Chris@0: overflow: hidden; Chris@0: word-wrap: break-word; Chris@0: } Chris@0: .sf-reset a { background:none; color:#868686; text-decoration:none; } Chris@0: .sf-reset a:hover { background:none; color:#313131; text-decoration:underline; } Chris@0: .sf-reset ol { padding: 10px 0; } Chris@0: .sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px; Chris@0: border-radius: 10px; Chris@0: border: 1px solid #ccc; 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@0: Chris@0: Chris@0: 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@0: $fmt = $this->fileLinkFormat; Chris@0: Chris@0: if ($fmt && $link = is_string($fmt) ? strtr($fmt, array('%f' => $path, '%l' => $line)) : $fmt->format($path, $line)) { Chris@0: return sprintf(' in %s line %d', $this->escapeHtml($link), $file, $line); Chris@0: } Chris@0: Chris@0: return sprintf(' in %s line %d', $this->escapeHtml($path), $file, $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@0: $result = array(); 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@0: $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@0: $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $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@0: }