1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Debug; 13 14use Symfony\Component\HttpFoundation\Response; 15use Symfony\Component\Debug\Exception\FlattenException; 16use Symfony\Component\Debug\Exception\OutOfMemoryException; 17 18/** 19 * ExceptionHandler converts an exception to a Response object. 20 * 21 * It is mostly useful in debug mode to replace the default PHP/XDebug 22 * output with something prettier and more useful. 23 * 24 * As this class is mainly used during Kernel boot, where nothing is yet 25 * available, the Response content is always HTML. 26 * 27 * @author Fabien Potencier <fabien@symfony.com> 28 * @author Nicolas Grekas <p@tchwork.com> 29 */ 30class ExceptionHandler 31{ 32 private $debug; 33 private $charset; 34 private $handler; 35 private $caughtBuffer; 36 private $caughtLength; 37 private $fileLinkFormat; 38 39 public function __construct($debug = true, $charset = null, $fileLinkFormat = null) 40 { 41 if (false !== strpos($charset, '%') xor false === strpos($fileLinkFormat, '%')) { 42 // Swap $charset and $fileLinkFormat for BC reasons 43 $pivot = $fileLinkFormat; 44 $fileLinkFormat = $charset; 45 $charset = $pivot; 46 } 47 $this->debug = $debug; 48 $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; 49 $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); 50 } 51 52 /** 53 * Registers the exception handler. 54 * 55 * @param bool $debug Enable/disable debug mode, where the stack trace is displayed 56 * @param string|null $charset The charset used by exception messages 57 * @param string|null $fileLinkFormat The IDE link template 58 * 59 * @return ExceptionHandler The registered exception handler 60 */ 61 public static function register($debug = true, $charset = null, $fileLinkFormat = null) 62 { 63 $handler = new static($debug, $charset, $fileLinkFormat); 64 65 $prev = set_exception_handler(array($handler, 'handle')); 66 if (is_array($prev) && $prev[0] instanceof ErrorHandler) { 67 restore_exception_handler(); 68 $prev[0]->setExceptionHandler(array($handler, 'handle')); 69 } 70 71 return $handler; 72 } 73 74 /** 75 * Sets a user exception handler. 76 * 77 * @param callable $handler An handler that will be called on Exception 78 * 79 * @return callable|null The previous exception handler if any 80 */ 81 public function setHandler($handler) 82 { 83 if (null !== $handler && !is_callable($handler)) { 84 throw new \LogicException('The exception handler must be a valid PHP callable.'); 85 } 86 $old = $this->handler; 87 $this->handler = $handler; 88 89 return $old; 90 } 91 92 /** 93 * Sets the format for links to source files. 94 * 95 * @param string $format The format for links to source files 96 * 97 * @return string The previous file link format. 98 */ 99 public function setFileLinkFormat($format) 100 { 101 $old = $this->fileLinkFormat; 102 $this->fileLinkFormat = $format; 103 104 return $old; 105 } 106 107 /** 108 * Sends a response for the given Exception. 109 * 110 * To be as fail-safe as possible, the exception is first handled 111 * by our simple exception handler, then by the user exception handler. 112 * The latter takes precedence and any output from the former is cancelled, 113 * if and only if nothing bad happens in this handling path. 114 */ 115 public function handle(\Exception $exception) 116 { 117 if (null === $this->handler || $exception instanceof OutOfMemoryException) { 118 $this->failSafeHandle($exception); 119 120 return; 121 } 122 123 $caughtLength = $this->caughtLength = 0; 124 125 ob_start(array($this, 'catchOutput')); 126 $this->failSafeHandle($exception); 127 while (null === $this->caughtBuffer && ob_end_flush()) { 128 // Empty loop, everything is in the condition 129 } 130 if (isset($this->caughtBuffer[0])) { 131 ob_start(array($this, 'cleanOutput')); 132 echo $this->caughtBuffer; 133 $caughtLength = ob_get_length(); 134 } 135 $this->caughtBuffer = null; 136 137 try { 138 call_user_func($this->handler, $exception); 139 $this->caughtLength = $caughtLength; 140 } catch (\Exception $e) { 141 if (!$caughtLength) { 142 // All handlers failed. Let PHP handle that now. 143 throw $exception; 144 } 145 } 146 } 147 148 /** 149 * Sends a response for the given Exception. 150 * 151 * If you have the Symfony HttpFoundation component installed, 152 * this method will use it to create and send the response. If not, 153 * it will fallback to plain PHP functions. 154 * 155 * @param \Exception $exception An \Exception instance 156 * 157 * @see sendPhpResponse() 158 * @see createResponse() 159 */ 160 private function failSafeHandle(\Exception $exception) 161 { 162 if (class_exists('Symfony\Component\HttpFoundation\Response', false)) { 163 $response = $this->createResponse($exception); 164 $response->sendHeaders(); 165 $response->sendContent(); 166 } else { 167 $this->sendPhpResponse($exception); 168 } 169 } 170 171 /** 172 * Sends the error associated with the given Exception as a plain PHP response. 173 * 174 * This method uses plain PHP functions like header() and echo to output 175 * the response. 176 * 177 * @param \Exception|FlattenException $exception An \Exception instance 178 */ 179 public function sendPhpResponse($exception) 180 { 181 if (!$exception instanceof FlattenException) { 182 $exception = FlattenException::create($exception); 183 } 184 185 if (!headers_sent()) { 186 header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); 187 foreach ($exception->getHeaders() as $name => $value) { 188 header($name.': '.$value, false); 189 } 190 header('Content-Type: text/html; charset='.$this->charset); 191 } 192 193 echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); 194 } 195 196 /** 197 * Creates the error Response associated with the given Exception. 198 * 199 * @param \Exception|FlattenException $exception An \Exception instance 200 * 201 * @return Response A Response instance 202 */ 203 public function createResponse($exception) 204 { 205 if (!$exception instanceof FlattenException) { 206 $exception = FlattenException::create($exception); 207 } 208 209 return Response::create($this->decorate($this->getContent($exception), $this->getStylesheet($exception)), $exception->getStatusCode(), $exception->getHeaders())->setCharset($this->charset); 210 } 211 212 /** 213 * Gets the HTML content associated with the given exception. 214 * 215 * @param FlattenException $exception A FlattenException instance 216 * 217 * @return string The content as a string 218 */ 219 public function getContent(FlattenException $exception) 220 { 221 switch ($exception->getStatusCode()) { 222 case 404: 223 $title = 'Sorry, the page you are looking for could not be found.'; 224 break; 225 default: 226 $title = 'Whoops, looks like something went wrong.'; 227 } 228 229 $content = ''; 230 if ($this->debug) { 231 try { 232 $count = count($exception->getAllPrevious()); 233 $total = $count + 1; 234 foreach ($exception->toArray() as $position => $e) { 235 $ind = $count - $position + 1; 236 $class = $this->formatClass($e['class']); 237 $message = nl2br($this->escapeHtml($e['message'])); 238 $content .= sprintf(<<<EOF 239 <h2 class="block_exception clear_fix"> 240 <span class="exception_counter">%d/%d</span> 241 <span class="exception_title">%s%s:</span> 242 <span class="exception_message">%s</span> 243 </h2> 244 <div class="block"> 245 <ol class="traces list_exception"> 246 247EOF 248 , $ind, $total, $class, $this->formatPath($e['trace'][0]['file'], $e['trace'][0]['line']), $message); 249 foreach ($e['trace'] as $trace) { 250 $content .= ' <li>'; 251 if ($trace['function']) { 252 $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); 253 } 254 if (isset($trace['file']) && isset($trace['line'])) { 255 $content .= $this->formatPath($trace['file'], $trace['line']); 256 } 257 $content .= "</li>\n"; 258 } 259 260 $content .= " </ol>\n</div>\n"; 261 } 262 } catch (\Exception $e) { 263 // something nasty happened and we cannot throw an exception anymore 264 if ($this->debug) { 265 $title = sprintf('Exception thrown when handling an exception (%s: %s)', get_class($e), $this->escapeHtml($e->getMessage())); 266 } else { 267 $title = 'Whoops, looks like something went wrong.'; 268 } 269 } 270 } 271 272 return <<<EOF 273 <div id="sf-resetcontent" class="sf-reset"> 274 <h1>$title</h1> 275 $content 276 </div> 277EOF; 278 } 279 280 /** 281 * Gets the stylesheet associated with the given exception. 282 * 283 * @param FlattenException $exception A FlattenException instance 284 * 285 * @return string The stylesheet as a string 286 */ 287 public function getStylesheet(FlattenException $exception) 288 { 289 return <<<EOF 290 .sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 } 291 .sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; } 292 .sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; } 293 .sf-reset .clear_fix { display:inline-block; } 294 .sf-reset * html .clear_fix { height:1%; } 295 .sf-reset .clear_fix { display:block; } 296 .sf-reset, .sf-reset .block { margin: auto } 297 .sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; } 298 .sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px } 299 .sf-reset strong { font-weight:bold; } 300 .sf-reset a { color:#6c6159; cursor: default; } 301 .sf-reset a img { border:none; } 302 .sf-reset a:hover { text-decoration:underline; } 303 .sf-reset em { font-style:italic; } 304 .sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif } 305 .sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; } 306 .sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; } 307 .sf-reset .exception_message { margin-left: 3em; display: block; } 308 .sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; } 309 .sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px; 310 -webkit-border-bottom-right-radius: 16px; 311 -webkit-border-bottom-left-radius: 16px; 312 -moz-border-radius-bottomright: 16px; 313 -moz-border-radius-bottomleft: 16px; 314 border-bottom-right-radius: 16px; 315 border-bottom-left-radius: 16px; 316 border-bottom:1px solid #ccc; 317 border-right:1px solid #ccc; 318 border-left:1px solid #ccc; 319 } 320 .sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px; 321 -webkit-border-top-left-radius: 16px; 322 -webkit-border-top-right-radius: 16px; 323 -moz-border-radius-topleft: 16px; 324 -moz-border-radius-topright: 16px; 325 border-top-left-radius: 16px; 326 border-top-right-radius: 16px; 327 border-top:1px solid #ccc; 328 border-right:1px solid #ccc; 329 border-left:1px solid #ccc; 330 overflow: hidden; 331 word-wrap: break-word; 332 } 333 .sf-reset a { background:none; color:#868686; text-decoration:none; } 334 .sf-reset a:hover { background:none; color:#313131; text-decoration:underline; } 335 .sf-reset ol { padding: 10px 0; } 336 .sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px; 337 -webkit-border-radius: 10px; 338 -moz-border-radius: 10px; 339 border-radius: 10px; 340 border: 1px solid #ccc; 341 } 342EOF; 343 } 344 345 private function decorate($content, $css) 346 { 347 return <<<EOF 348<!DOCTYPE html> 349<html> 350 <head> 351 <meta charset="{$this->charset}" /> 352 <meta name="robots" content="noindex,nofollow" /> 353 <style> 354 /* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */ 355 html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;} 356 357 html { background: #eee; padding: 10px } 358 img { border: 0; } 359 #sf-resetcontent { width:970px; margin:0 auto; } 360 $css 361 </style> 362 </head> 363 <body> 364 $content 365 </body> 366</html> 367EOF; 368 } 369 370 private function formatClass($class) 371 { 372 $parts = explode('\\', $class); 373 374 return sprintf('<abbr title="%s">%s</abbr>', $class, array_pop($parts)); 375 } 376 377 private function formatPath($path, $line) 378 { 379 $path = $this->escapeHtml($path); 380 $file = preg_match('#[^/\\\\]*$#', $path, $file) ? $file[0] : $path; 381 382 if ($linkFormat = $this->fileLinkFormat) { 383 $link = strtr($this->escapeHtml($linkFormat), array('%f' => $path, '%l' => (int) $line)); 384 385 return sprintf(' in <a href="%s" title="Go to source">%s line %d</a>', $link, $file, $line); 386 } 387 388 return sprintf(' in <a title="%s line %3$d" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">%s line %d</a>', $path, $file, $line); 389 } 390 391 /** 392 * Formats an array as a string. 393 * 394 * @param array $args The argument array 395 * 396 * @return string 397 */ 398 private function formatArgs(array $args) 399 { 400 $result = array(); 401 foreach ($args as $key => $item) { 402 if ('object' === $item[0]) { 403 $formattedValue = sprintf('<em>object</em>(%s)', $this->formatClass($item[1])); 404 } elseif ('array' === $item[0]) { 405 $formattedValue = sprintf('<em>array</em>(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); 406 } elseif ('string' === $item[0]) { 407 $formattedValue = sprintf("'%s'", $this->escapeHtml($item[1])); 408 } elseif ('null' === $item[0]) { 409 $formattedValue = '<em>null</em>'; 410 } elseif ('boolean' === $item[0]) { 411 $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>'; 412 } elseif ('resource' === $item[0]) { 413 $formattedValue = '<em>resource</em>'; 414 } else { 415 $formattedValue = str_replace("\n", '', var_export($this->escapeHtml((string) $item[1]), true)); 416 } 417 418 $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); 419 } 420 421 return implode(', ', $result); 422 } 423 424 /** 425 * Returns an UTF-8 and HTML encoded string. 426 */ 427 protected static function utf8Htmlize($str) 428 { 429 if (!preg_match('//u', $str) && function_exists('iconv')) { 430 set_error_handler('var_dump', 0); 431 $charset = ini_get('default_charset'); 432 if ('UTF-8' === $charset || $str !== @iconv($charset, $charset, $str)) { 433 $charset = 'CP1252'; 434 } 435 restore_error_handler(); 436 437 $str = iconv($charset, 'UTF-8', $str); 438 } 439 440 return htmlspecialchars($str, ENT_QUOTES | (PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), 'UTF-8'); 441 } 442 443 /** 444 * HTML-encodes a string. 445 */ 446 private function escapeHtml($str) 447 { 448 return htmlspecialchars($str, ENT_QUOTES | (PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), $this->charset); 449 } 450 451 /** 452 * @internal 453 */ 454 public function catchOutput($buffer) 455 { 456 $this->caughtBuffer = $buffer; 457 458 return ''; 459 } 460 461 /** 462 * @internal 463 */ 464 public function cleanOutput($buffer) 465 { 466 if ($this->caughtLength) { 467 // use substr_replace() instead of substr() for mbstring overloading resistance 468 $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); 469 if (isset($cleanBuffer[0])) { 470 $buffer = $cleanBuffer; 471 } 472 } 473 474 return $buffer; 475 } 476} 477