2015-08-18 00:00:26 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* 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\HttpFoundation\Response ;
use Symfony\Component\Debug\Exception\FlattenException ;
use Symfony\Component\Debug\Exception\OutOfMemoryException ;
/**
* 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 < fabien @ symfony . com >
* @ author Nicolas Grekas < p @ tchwork . com >
*/
class ExceptionHandler
{
private $debug ;
private $charset ;
private $handler ;
private $caughtBuffer ;
private $caughtLength ;
private $fileLinkFormat ;
public function __construct ( $debug = true , $charset = null , $fileLinkFormat = null )
{
if ( false !== strpos ( $charset , '%' ) xor false === strpos ( $fileLinkFormat , '%' )) {
// Swap $charset and $fileLinkFormat for BC reasons
$pivot = $fileLinkFormat ;
$fileLinkFormat = $charset ;
$charset = $pivot ;
}
$this -> debug = $debug ;
$this -> charset = $charset ? : ini_get ( 'default_charset' ) ? : 'UTF-8' ;
$this -> fileLinkFormat = $fileLinkFormat ? : ini_get ( 'xdebug.file_link_format' ) ? : get_cfg_var ( 'xdebug.file_link_format' );
}
/**
* 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 ExceptionHandler The registered exception handler
*/
public static function register ( $debug = true , $charset = null , $fileLinkFormat = null )
{
$handler = new static ( $debug , $charset , $fileLinkFormat );
$prev = set_exception_handler ( array ( $handler , 'handle' ));
if ( is_array ( $prev ) && $prev [ 0 ] instanceof ErrorHandler ) {
restore_exception_handler ();
$prev [ 0 ] -> setExceptionHandler ( array ( $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 ( $handler )
{
if ( null !== $handler && ! is_callable ( $handler )) {
throw new \LogicException ( 'The exception handler must be a valid PHP callable.' );
}
$old = $this -> handler ;
$this -> handler = $handler ;
return $old ;
}
/**
* Sets the format for links to source files .
*
* @ param string $format The format for links to source files
*
* @ return string The previous file link format .
*/
public function setFileLinkFormat ( $format )
{
$old = $this -> fileLinkFormat ;
$this -> fileLinkFormat = $format ;
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 -> failSafeHandle ( $exception );
return ;
}
$caughtLength = $this -> caughtLength = 0 ;
ob_start ( array ( $this , 'catchOutput' ));
$this -> failSafeHandle ( $exception );
while ( null === $this -> caughtBuffer && ob_end_flush ()) {
// Empty loop, everything is in the condition
}
if ( isset ( $this -> caughtBuffer [ 0 ])) {
ob_start ( array ( $this , 'cleanOutput' ));
echo $this -> caughtBuffer ;
$caughtLength = ob_get_length ();
}
$this -> caughtBuffer = null ;
try {
call_user_func ( $this -> handler , $exception );
$this -> caughtLength = $caughtLength ;
} catch ( \Exception $e ) {
if ( ! $caughtLength ) {
// All handlers failed. Let PHP handle that now.
throw $exception ;
}
}
}
/**
* Sends a response for the given Exception .
*
* If you have the Symfony HttpFoundation component installed ,
* this method will use it to create and send the response . If not ,
* it will fallback to plain PHP functions .
*
* @ param \Exception $exception An \Exception instance
*
* @ see sendPhpResponse ()
* @ see createResponse ()
*/
private function failSafeHandle ( \Exception $exception )
{
if ( class_exists ( 'Symfony\Component\HttpFoundation\Response' , false )) {
$response = $this -> createResponse ( $exception );
$response -> sendHeaders ();
$response -> sendContent ();
} else {
$this -> sendPhpResponse ( $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 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 ));
}
/**
* Creates the error Response associated with the given Exception .
*
* @ param \Exception | FlattenException $exception An \Exception instance
*
* @ return Response A Response instance
*/
public function createResponse ( $exception )
{
if ( ! $exception instanceof FlattenException ) {
$exception = FlattenException :: create ( $exception );
}
return Response :: create ( $this -> decorate ( $this -> getContent ( $exception ), $this -> getStylesheet ( $exception )), $exception -> getStatusCode (), $exception -> getHeaders ()) -> setCharset ( $this -> charset );
}
/**
* Gets the HTML content associated with the given exception .
*
* @ param FlattenException $exception A FlattenException instance
*
* @ 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.' ;
}
$content = '' ;
if ( $this -> debug ) {
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
< h2 class = " block_exception clear_fix " >
< span class = " exception_counter " >% d /% d </ span >
< span class = " exception_title " >% s % s :</ span >
< span class = " exception_message " >% s </ span >
</ h2 >
< div class = " block " >
< ol class = " traces list_exception " >
EOF
, $ind , $total , $class , $this -> formatPath ( $e [ 'trace' ][ 0 ][ 'file' ], $e [ 'trace' ][ 0 ][ 'line' ]), $message );
foreach ( $e [ 'trace' ] as $trace ) {
$content .= ' <li>' ;
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 .= " </li> \n " ;
}
$content .= " </ol> \n </div> \n " ;
}
} catch ( \Exception $e ) {
// something nasty happened and we cannot throw an exception anymore
if ( $this -> debug ) {
$title = sprintf ( 'Exception thrown when handling an exception (%s: %s)' , get_class ( $e ), $this -> escapeHtml ( $e -> getMessage ()));
} else {
$title = 'Whoops, looks like something went wrong.' ;
}
}
}
return <<< EOF
< div id = " sf-resetcontent " class = " sf-reset " >
< h1 > $title </ h1 >
$content
</ div >
EOF ;
}
/**
* Gets the stylesheet associated with the given exception .
*
* @ param FlattenException $exception A FlattenException instance
*
* @ return string The stylesheet as a string
*/
public function getStylesheet ( FlattenException $exception )
{
return <<< EOF
. sf - reset { font : 11 px Verdana , Arial , sans - serif ; color : #333 }
. sf - reset . clear { clear : both ; height : 0 ; font - size : 0 ; line - height : 0 ; }
. sf - reset . clear_fix : after { display : block ; height : 0 ; clear : both ; visibility : hidden ; }
. sf - reset . clear_fix { display : inline - block ; }
. sf - reset * html . clear_fix { height : 1 % ; }
. sf - reset . clear_fix { display : block ; }
. sf - reset , . sf - reset . block { margin : auto }
. sf - reset abbr { border - bottom : 1 px dotted #000; cursor: help; }
. sf - reset p { font - size : 14 px ; line - height : 20 px ; color : #868686; padding-bottom:20px }
. sf - reset strong { font - weight : bold ; }
. sf - reset a { color : #6c6159; cursor: default; }
. sf - reset a img { border : none ; }
. sf - reset a : hover { text - decoration : underline ; }
. sf - reset em { font - style : italic ; }
. sf - reset h1 , . sf - reset h2 { font : 20 px Georgia , " Times New Roman " , Times , serif }
. sf - reset . exception_counter { background - color : #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; }
. sf - reset . exception_title { margin - left : 3 em ; margin - bottom : 0.7 em ; display : block ; }
. sf - reset . exception_message { margin - left : 3 em ; display : block ; }
. sf - reset . traces li { font - size : 12 px ; padding : 2 px 4 px ; list - style - type : decimal ; margin - left : 20 px ; }
. sf - reset . block { background - color : #FFFFFF; padding:10px 28px; margin-bottom:20px;
- webkit - border - bottom - right - radius : 16 px ;
- webkit - border - bottom - left - radius : 16 px ;
- moz - border - radius - bottomright : 16 px ;
- moz - border - radius - bottomleft : 16 px ;
border - bottom - right - radius : 16 px ;
border - bottom - left - radius : 16 px ;
border - bottom : 1 px solid #ccc;
border - right : 1 px solid #ccc;
border - left : 1 px solid #ccc;
}
. sf - reset . block_exception { background - color : #ddd; color: #333; padding:20px;
- webkit - border - top - left - radius : 16 px ;
- webkit - border - top - right - radius : 16 px ;
- moz - border - radius - topleft : 16 px ;
- moz - border - radius - topright : 16 px ;
border - top - left - radius : 16 px ;
border - top - right - radius : 16 px ;
border - top : 1 px solid #ccc;
border - right : 1 px solid #ccc;
border - left : 1 px solid #ccc;
overflow : hidden ;
word - wrap : break - word ;
}
. sf - reset a { background : none ; color : #868686; text-decoration:none; }
. sf - reset a : hover { background : none ; color : #313131; text-decoration:underline; }
. sf - reset ol { padding : 10 px 0 ; }
. sf - reset h1 { background - color : #FFFFFF; padding: 15px 28px; margin-bottom: 20px;
- webkit - border - radius : 10 px ;
- moz - border - radius : 10 px ;
border - radius : 10 px ;
border : 1 px solid #ccc;
}
EOF ;
}
private function decorate ( $content , $css )
{
return <<< EOF
<! DOCTYPE html >
< html >
< head >
< meta charset = " { $this -> charset } " />
< meta name = " robots " content = " noindex,nofollow " />
< style >
/* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */
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;}
html { background : #eee; padding: 10px }
img { border : 0 ; }
#sf-resetcontent { width:970px; margin:0 auto; }
$css
</ style >
</ head >
< body >
$content
</ body >
</ html >
EOF ;
}
private function formatClass ( $class )
{
$parts = explode ( '\\' , $class );
return sprintf ( '<abbr title="%s">%s</abbr>' , $class , array_pop ( $parts ));
}
private function formatPath ( $path , $line )
{
$path = $this -> escapeHtml ( $path );
$file = preg_match ( '#[^/\\\\]*$#' , $path , $file ) ? $file [ 0 ] : $path ;
if ( $linkFormat = $this -> fileLinkFormat ) {
$link = strtr ( $this -> escapeHtml ( $linkFormat ), array ( '%f' => $path , '%l' => ( int ) $line ));
return sprintf ( ' in <a href="%s" title="Go to source">%s line %d</a>' , $link , $file , $line );
}
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 );
}
/**
* Formats an array as a string .
*
* @ param array $args The argument array
*
* @ return string
*/
private function formatArgs ( array $args )
{
$result = array ();
foreach ( $args as $key => $item ) {
if ( 'object' === $item [ 0 ]) {
$formattedValue = sprintf ( '<em>object</em>(%s)' , $this -> formatClass ( $item [ 1 ]));
} elseif ( 'array' === $item [ 0 ]) {
$formattedValue = sprintf ( '<em>array</em>(%s)' , is_array ( $item [ 1 ]) ? $this -> formatArgs ( $item [ 1 ]) : $item [ 1 ]);
} elseif ( 'string' === $item [ 0 ]) {
$formattedValue = sprintf ( " '%s' " , $this -> escapeHtml ( $item [ 1 ]));
} elseif ( 'null' === $item [ 0 ]) {
$formattedValue = '<em>null</em>' ;
} elseif ( 'boolean' === $item [ 0 ]) {
$formattedValue = '<em>' . strtolower ( var_export ( $item [ 1 ], true )) . '</em>' ;
} elseif ( 'resource' === $item [ 0 ]) {
$formattedValue = '<em>resource</em>' ;
} else {
$formattedValue = str_replace ( " \n " , '' , var_export ( $this -> escapeHtml (( string ) $item [ 1 ]), true ));
}
$result [] = is_int ( $key ) ? $formattedValue : sprintf ( " '%s' => %s " , $key , $formattedValue );
}
return implode ( ', ' , $result );
}
/**
* Returns an UTF - 8 and HTML encoded string .
*
* @ deprecated since version 2.7 , to be removed in 3.0 .
*/
protected static function utf8Htmlize ( $str )
{
2015-08-27 19:03:05 +00:00
@ trigger_error ( 'The ' . __METHOD__ . ' method is deprecated since version 2.7 and will be removed in 3.0.' , E_USER_DEPRECATED );
2015-08-18 00:00:26 +00:00
return htmlspecialchars ( $str , ENT_QUOTES | ( PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0 ), 'UTF-8' );
}
/**
* HTML - encodes a string .
*/
private function escapeHtml ( $str )
{
return htmlspecialchars ( $str , ENT_QUOTES | ( PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0 ), $this -> charset );
}
/**
* @ internal
*/
public function catchOutput ( $buffer )
{
$this -> caughtBuffer = $buffer ;
return '' ;
}
/**
* @ internal
*/
public function cleanOutput ( $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 ;
}
}