2022-10-24 17:13:07 +00:00
< ? php
/*
This file is part of KD2FW -- < http :// dev . kd2 . org />
Copyright ( c ) 2001 - 2019 BohwaZ < http :// bohwaz . net />
All rights reserved .
KD2FW is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
Foobar is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
along with Foobar . If not , see < https :// www . gnu . org / licenses />.
*/
namespace KD2 ;
/**
* Simple error and exception handler
*
* When enabled ( with ErrorManager :: enable ( ErrorManager :: DEVELOPMENT )) it will
* catch any error , warning or exception and display it along with useful debug
* information . If enabled it will also log the errors to a file and / or send
* every error by email .
*
* In production mode no details are given , but a unique reference to the log
* or email is displayed .
*
* This is similar in a way to http :// tracy . nette . org /
*
* @ author bohwaz < http :// bohwaz . net />
*/
class ErrorManager
{
/**
* Prod / dev modes
*/
const PRODUCTION = 1 ;
const DEVELOPMENT = 2 ;
2022-11-15 12:10:52 +00:00
const CLI_DEVELOPMENT = 4 ;
2022-10-24 17:13:07 +00:00
/**
* Term colors
*/
const RED = '[1;41m' ;
const RED_FAINT = '[1m' ;
const YELLOW = '[33m' ;
/**
* true = catch exceptions , false = do nothing
* @ var null
*/
static protected $enabled = null ;
/**
* HTML template used for displaying production errors
* @ var string
*/
static protected $production_error_template = ' <! DOCTYPE html >< html >< head >< title > Internal server error </ title >
< style type = " text/css " >
body { font - family : sans - serif ; }
code , p , h1 { max - width : 400 px ; margin : 1 em auto ; display : block ; }
code { text - align : right ; color : #666; }
a { color : blue ; }
form { text - align : center ; }
input { padding : . 3 em ; }
</ style ></ head >< body >< h1 > Server error </ h1 >< p > Sorry but the server encountered an internal error and was unable
to complete your request . Please try again later .</ p >
< if ( sent ) >< p > The webmaster has been noticed and this will be fixed ASAP .</ p ></ if >
< if ( logged ) >< code > Error reference : < b > { $ref } </ b ></ code ></ if >
< if ( report ) >< form method = " post " action = " { $report_url } " >< input type = " hidden " value = " { $report_json } " />< input type = " submit " value = " Report this error " /></ report_form ></ if >
< p >< a href = " / " >& larr ; Go back to the homepage </ a ></ p >
</ body ></ html > ' ;
/**
* E - Mail address where to send errors
* @ var boolean
*/
static protected $email_errors = false ;
/**
* Reporting URL
*/
static protected $report_url = null ;
/**
* Reporting automatically ?
*/
static protected $report_auto = true ;
/**
* Custom context
*/
static protected $context = [];
/**
* Custom exception handlers
* @ var array
*/
static protected $custom_handlers = [];
/**
* Does the terminal support ANSI colors
* @ var boolean
*/
static protected $term_color = false ;
/**
* Will be set to true when catching an exception to avoid double catching
* with the shutdown function
* @ var boolean
*/
static protected $catching = false ;
/**
* Used to store timers and memory consumption
* @ var array
*/
static protected $run_trace = [];
/**
* Handles PHP shutdown on fatal error to be able to catch the error
* @ return void
*/
static public function shutdownHandler ()
{
// Stop here if disabled or if the script ended with an exception
if ( ! self :: $enabled || self :: $catching )
return false ;
$error = error_get_last ();
if ( $error && in_array ( $error [ 'type' ], [ E_ERROR , E_CORE_ERROR , E_COMPILE_ERROR , E_PARSE , E_RECOVERABLE_ERROR , E_USER_ERROR ], TRUE ))
{
// Don't exit at the end, as there might be other shutdown handlers
// after this one
self :: exceptionHandler ( new \ErrorException ( $error [ 'message' ], 0 , $error [ 'type' ], $error [ 'file' ], $error [ 'line' ]), false );
}
}
/**
* Internal error handler to throw them as exceptions
* ( private use )
*/
static public function errorHandler ( $severity , $message , $file , $line )
{
if ( ! ( error_reporting () & $severity )) {
// Don't report this error (for example @unlink)
return ;
}
$types = [
E_ERROR => 'Fatal error' ,
E_USER_ERROR => 'User error' ,
E_RECOVERABLE_ERROR => 'Recoverable error' ,
E_CORE_ERROR => 'Core error' ,
E_COMPILE_ERROR => 'Compile error' ,
E_PARSE => 'Parse error' ,
E_WARNING => 'Warning' ,
E_CORE_WARNING => 'Core warning' ,
E_COMPILE_WARNING => 'Compile warning' ,
E_USER_WARNING => 'User warning' ,
E_NOTICE => 'Notice' ,
E_USER_NOTICE => 'User notice' ,
E_STRICT => 'Strict standards' ,
E_DEPRECATED => 'Deprecated' ,
E_USER_DEPRECATED => 'User deprecated' ,
];
$type = array_key_exists ( $severity , $types ) ? $types [ $severity ] : 'Unknown error' ;
$message = $type . ': ' . $message ;
// Catch ASSERT_BAIL errors differently because throwing an exception
// in this case results in an execution shutdown, and shutdown handler
// isn't even called. See https://bugs.php.net/bug.php?id=53619
if ( assert_options ( ASSERT_ACTIVE ) && assert_options ( ASSERT_BAIL ) && substr ( $message , 0 , 18 ) == 'Warning: assert():' )
{
$message .= ' (ASSERT_BAIL detected)' ;
self :: exceptionHandler ( new \ErrorException ( $message , 0 , $severity , $file , $line ));
return true ;
}
throw new \ErrorException ( $message , 0 , $severity , $file , $line );
return true ;
}
/**
* Main exception handler
* @ param object $e Exception or Error ( PHP 7 ) object
* @ param boolean $exit Exit the script at the end
* @ return void
*/
static public function exceptionHandler ( $e , $exit = true )
{
self :: $catching = true ;
try {
self :: reportException ( $e , $exit );
}
catch ( \Throwable $e2 ) {
echo $e2 ;
echo PHP_EOL . PHP_EOL . $e ;
exit ( 1 );
}
catch ( \Exception $e2 ) {
echo $e2 ;
echo PHP_EOL . PHP_EOL . $e ;
exit ( 1 );
}
return true ;
}
/**
* Main exception handler
* @ param object $e Exception or Error ( PHP 7 ) object
* @ param boolean $exit Exit the script at the end
* @ return void
*/
static public function reportException ( $e , $exit )
{
foreach ( self :: $custom_handlers as $class => $callback )
{
if ( $e instanceOf $class )
{
call_user_func ( $callback , $e );
$e = false ;
break ;
}
}
if ( $e === false )
{
if ( $exit )
{
exit ( 1 );
}
else
{
return ;
}
}
2023-01-28 02:11:31 +00:00
extract ( self :: buildExceptionReport ( $e , false ));
unset ( $e );
2022-10-24 17:13:07 +00:00
// Log exception to file
if ( ini_get ( 'log_errors' ))
{
error_log ( $log );
}
// Disable any output if it was buffering
if ( ob_get_level ())
{
ob_end_clean ();
}
2022-11-15 12:10:52 +00:00
$is_curl = 0 === strpos ( $_SERVER [ 'HTTP_USER_AGENT' ] ? ? '' , 'curl/' );
$is_cli = PHP_SAPI == 'cli' ;
if ( ! $is_cli ) {
http_response_code ( 500 );
}
if ( $is_curl && ! headers_sent ()) {
header ( 'Content-Type: text/plain; charset=utf-8' , true );
}
if (( $is_cli || $is_curl ) && ( self :: $enabled & self :: DEVELOPMENT || self :: $enabled & self :: CLI_DEVELOPMENT ))
2022-10-24 17:13:07 +00:00
{
foreach ( $report -> errors as $e )
{
self :: termPrint ( sprintf ( ' /!\\ %s ' , $e -> type ), self :: RED );
self :: termPrint ( $e -> message , self :: RED_FAINT );
if ( isset ( $e -> line ))
{
self :: termPrint ( sprintf ( 'Line %d in %s' , $e -> line , $e -> file ), self ::
YELLOW );
}
// Ignore the error stack belonging to ErrorManager
foreach ( $e -> backtrace as $i => $t )
{
$file = ! empty ( $t -> file ) ? $t -> file : '[internal function]' ;
$line = ! empty ( $t -> line ) ? '(' . $t -> line . ')' : '' ;
if ( isset ( $t -> args ))
{
$args = $t -> args ;
foreach ( $args as & $arg )
{
if ( strlen ( $arg ) > 20 )
{
$arg = substr ( $arg , 0 , 19 ) . '…' ;
}
}
unset ( $arg );
self :: termPrint ( sprintf ( '#%d %s%s: %s(%s)' , $i , $file , $line , $t -> function , implode ( ', ' , $args )));
}
else
{
self :: termPrint ( sprintf ( '#%d %s%s' , $i , $file , $line ));
}
}
}
}
2022-11-15 12:10:52 +00:00
else if (( $is_cli || $is_curl ) && self :: $enabled & self :: PRODUCTION ) {
self :: termPrint ( ' /!\\ An internal server error occurred ' , self :: RED );
self :: termPrint ( ' Error reference was: ' . $report -> context -> id , self :: YELLOW );
}
else if ( self :: $enabled & self :: PRODUCTION )
2022-10-24 17:13:07 +00:00
{
2023-02-14 12:26:40 +00:00
@ header_remove ( 'Content-Disposition' );
@ header ( 'Content-Type: text/html; charset=utf-8' , true );
2022-10-24 17:13:07 +00:00
self :: htmlProduction ( $report );
}
else
{
if ( ! headers_sent ()) {
header_remove ();
header ( 'Content-Type: text/html; charset=UTF-8' , true );
header ( 'HTTP/1.1 500 Internal Server Error' , true );
}
echo $html_report ;
}
// Log exception to email
if ( self :: $email_errors ) {
2023-01-28 02:11:31 +00:00
self :: sendEmail ( $title , $report , $log , $html_report );
2022-10-24 17:13:07 +00:00
}
// Send report to URL
if ( self :: $report_auto && self :: $report_url ) {
self :: sendReport ( $report , self :: $report_url );
}
if ( $exit )
{
exit ( 1 );
}
}
2023-01-28 02:11:31 +00:00
static public function reportExceptionSilent ( \Throwable $e ) : void
{
extract ( self :: buildExceptionReport ( $e ));
// Log exception to file
if ( ini_get ( 'log_errors' ))
{
error_log ( $log );
}
if ( self :: $email_errors ) {
self :: sendEmail ( $title , $report , $log , $html_report );
}
}
2022-10-24 17:13:07 +00:00
static protected function sendEmail ( string $title , \stdClass $report , string $log , string $html ) : void
{
// From: sender
$from = ! empty ( $_SERVER [ 'SERVER_NAME' ]) ? $_SERVER [ 'SERVER_NAME' ] : basename ( $report -> context -> root_directory ? ? __FILE__ );
$msgid = $report -> context -> id . '@' . $from ;
$boundary = sprintf ( '-----=%s' , md5 ( uniqid ( rand ())));
$header = sprintf ( " MIME-Version: 1.0 \r \n From: \" %s \" <%s> \r \n In-Reply-To: <%s> \r \n Message-Id: <%s> \r \n " , $from , self :: $email_errors , $msgid , $msgid );
$header .= sprintf ( " Content-Type: multipart/mixed; boundary= \" %s \" \r \n " , $boundary );
$header .= " \r \n " ;
$msg = " This message contains multiple MIME parts. \r \n \r \n " ;
$msg .= sprintf ( " --%s \r \n " , $boundary );
$msg .= " Content-Type: text/plain; charset= \" utf-8 \" \r \n " ;
$msg .= " Content-Transfer-Encoding: 8bit \r \n \r \n " ;
$msg .= $log . " \r \n \r \n " ;
$msg .= sprintf ( " --%s \r \n " , $boundary );
$msg .= " Content-Type: text/html; charset= \" utf-8 \" \r \n " ;
$msg .= " Content-Transfer-Encoding: 8bit \r \n \r \n " ;
$msg .= $html . " \r \n \r \n " ;
$msg .= sprintf ( " --%s-- " , $boundary );
mail ( self :: $email_errors , sprintf ( 'Error #%s: %s' , $report -> context -> id , $title ), $msg , $header );
}
/**
* Prints a line to STDERR , eventually using a color
*/
static public function termPrint ( $message , $color = null )
{
if ( ! defined ( '\STDERR' )) {
echo $message . PHP_EOL ;
return ;
}
if ( $color && self :: $term_color )
{
$message = chr ( 27 ) . $color . $message . chr ( 27 ) . " [0m " ;
}
fwrite ( \STDERR , $message . PHP_EOL );
}
/**
* Return file location without the document root
*/
static protected function getFileLocation ( $file )
{
if ( ! empty ( self :: $context [ 'root_directory' ]) && ( $pos = strpos ( $file , self :: $context [ 'root_directory' ])) === 0 )
{
return '...' . substr ( $file , strlen ( self :: $context [ 'root_directory' ]));
}
return $file ;
}
2023-01-28 02:11:31 +00:00
static public function buildExceptionReport ( \Throwable $e , bool $force_html = false ) : array
{
$report = self :: makeReport ( $e );
$log = sprintf ( '=========== Error ref. %s ===========' , $report -> context -> id )
. PHP_EOL . PHP_EOL . ( string ) $e . PHP_EOL . PHP_EOL
. '<errorReport>' . PHP_EOL . json_encode ( $report , \JSON_PRETTY_PRINT )
. PHP_EOL . '</errorReport>' . PHP_EOL ;
$html_report = null ;
if ( $force_html || self :: $enabled & self :: DEVELOPMENT || self :: $email_errors ) {
$html_report = self :: htmlReport ( $report );
}
$title = $e -> getMessage ();
return compact ( 'report' , 'log' , 'html_report' , 'title' );
}
2022-10-24 17:13:07 +00:00
/**
* Generates a report from an exception
*/
static public function makeReport ( $e )
{
$report = ( object ) [
'errors' => [],
];
while ( $e !== null )
{
$class = get_class ( $e );
$error = ( object ) [
'message' => $e -> getMessage (),
'errorCode' => $e -> getCode (),
'type' => in_array ( $class , [ 'ErrorException' , 'Error' ]) ? 'PHP error' : $class ,
'backtrace' => [
( object ) [
'file' => $e -> getFile () ? self :: getFileLocation ( $e -> getFile ()) : null ,
'line' => $e -> getLine (),
'code' => $e -> getFile () && $e -> getLine () ? self :: getSource ( $e -> getFile (), $e -> getLine ()) : null ,
],
],
];
foreach ( $e -> getTrace () as $i => $t )
{
// Ignore the error stack from ErrorManager
if ( isset ( $t [ 'class' ]) && $t [ 'class' ] === __CLASS__
&& ( $t [ 'function' ] === 'shutdownHandler' || $t [ 'function' ] === 'errorHandler' ))
{
continue ;
}
$args = [];
// Display call arguments
if ( ! empty ( $t [ 'args' ]))
{
// Find arguments variables names via reflection
try {
if ( isset ( $t [ 'class' ]))
{
$r = new \ReflectionMethod ( $t [ 'class' ], $t [ 'function' ]);
}
else
{
$r = new \ReflectionFunction ( $t [ 'function' ]);
}
$params = $r -> getParameters ();
}
catch ( \Exception $_ignore ) {
$params = [];
}
foreach ( $t [ 'args' ] as $name => $value )
{
if ( array_key_exists ( $name , $params ))
{
$name = '$' . $params [ $name ] -> name ;
}
if ( is_string ( $value ))
{
$value = self :: getFileLocation ( $value );
}
$args [ $name ] = self :: dump ( $value );
if ( strlen ( $args [ $name ]) > 2000 )
{
$args [ $name ] = substr ( $args [ $name ], 0 , 1999 ) . '…' ;
}
}
}
$trace = ( object ) [
// Add class name to function
'function' => isset ( $t [ 'class' ]) ? $t [ 'class' ] . $t [ 'type' ] . $t [ 'function' ] : $t [ 'function' ],
];
if ( isset ( $t [ 'file' ]))
{
$trace -> file = self :: getFileLocation ( $t [ 'file' ]);
}
if ( isset ( $t [ 'line' ]))
{
$trace -> line = ( int ) $t [ 'line' ];
}
if ( count ( $args ))
{
$trace -> args = $args ;
}
if ( isset ( $trace -> file ) && isset ( $trace -> line ))
{
$trace -> code = self :: getSource ( $t [ 'file' ], $t [ 'line' ]);
}
$error -> backtrace [] = $trace ;
}
$report -> errors [] = $error ;
$e = $e -> getPrevious ();
}
unset ( $error , $e , $params , $t );
$context = array_merge ([
'id' => base_convert ( substr ( sha1 ( json_encode ( $report -> errors )), 0 , 10 ), 16 , 36 ),
'date' => date ( DATE_ATOM ),
'os' => PHP_OS ,
'language' => 'PHP ' . PHP_VERSION ,
2022-11-15 12:10:52 +00:00
'environment' => self :: $enabled & self :: DEVELOPMENT ? 'development' : 'production:' . self :: $enabled ,
2022-10-24 17:13:07 +00:00
'php_sapi' => PHP_SAPI ,
'remote_ip' => isset ( $_SERVER [ 'REMOTE_ADDR' ]) ? $_SERVER [ 'REMOTE_ADDR' ] : null ,
'http_method' => isset ( $_SERVER [ 'REQUEST_METHOD' ]) ? $_SERVER [ 'REQUEST_METHOD' ] : null ,
'http_files' => self :: dump ( $_FILES ),
'http_post' => self :: dump ( $_POST , true ),
'duration' => isset ( self :: $context [ 'request_started' ]) ? ( microtime ( true ) - self :: $context [ 'request_started' ]) * 1000 : null ,
'memory_peak' => memory_get_peak_usage ( true ),
'memory_used' => memory_get_usage ( true ),
], self :: $context );
ksort ( $context );
unset ( $context [ 'request_started' ]);
$report -> context = ( object ) $context ;
if ( ! empty ( $_SERVER [ 'HTTP_HOST' ]) && ! empty ( $_SERVER [ 'REQUEST_URI' ]))
{
$proto = empty ( $_SERVER [ 'HTTPS' ]) ? 'http' : 'https' ;
$report -> context -> url = $proto . '://' . $_SERVER [ 'HTTP_HOST' ] . $_SERVER [ 'REQUEST_URI' ];
}
return $report ;
}
/**
* Displays an exception as HTML debug page
*/
static public function htmlException ( \stdClass $e ) : string
{
$out = sprintf ( '<section><header><h1>%s</h1><h2>%s</h2></header>' ,
$e -> type , nl2br ( htmlspecialchars ( $e -> message )));
foreach ( $e -> backtrace as $i => $t )
{
$out .= '<article>' ;
if ( isset ( $t -> file ) && isset ( $t -> line ))
{
$dir = dirname ( $t -> file );
$dir = $dir == '/' ? $dir : $dir . '/' ;
$out .= sprintf ( '<h3>in %s<b>%s</b>:<i>%d</i></h3>' , htmlspecialchars ( $dir ), htmlspecialchars ( basename ( $t -> file )), $t -> line );
}
if ( isset ( $t -> function ))
{
$out .= sprintf ( '<h4>→ %s <i>(%d arg.)</i></h4>' , htmlspecialchars ( $t -> function ), isset ( $t -> args ) ? count ( $t -> args ) : 0 );
// Display call arguments
if ( ! empty ( $t -> args ))
{
$out .= '<table>' ;
foreach ( $t -> args as $name => $value )
{
$out .= sprintf ( '<tr><th>%s</th><td><pre>%s</pre></td>' , htmlspecialchars ( $name ), htmlspecialchars ( $value ));
}
$out .= '</table>' ;
}
}
// Display source code
if ( isset ( $t -> code ) && isset ( $t -> line ))
{
$out .= self :: htmlSource ( $t -> code , $t -> line );
}
$out .= '</article>' ;
}
$out .= '</section>' ;
return $out ;
}
static public function htmlSource ( array $source , $line )
{
$out = '' ;
foreach ( $source as $i => $code )
{
$html = '<b>' . ( $i ) . '</b>' . htmlspecialchars ( $code , ENT_QUOTES );
if ( $i == $line )
{
$html = '<u>' . $html . '</u>' ;
}
$out .= $html . PHP_EOL ;
}
return '<pre><code>' . $out . '</code></pre>' ;
}
/**
* Get source code
* @ param string $file File location
* @ param integer $line Line to highlight
* @ return array
*/
static public function getSource ( $file , $line )
{
$out = [];
$start = max ( 0 , $line - 5 );
if ( ! file_exists ( $file )) {
return [ $line => 'Source file not found' ];
}
$file = new \SplFileObject ( $file );
$file -> seek ( $start );
for ( $i = $start + 1 ; $i < $start + 10 ; $i ++ )
{
if ( $file -> eof ())
{
break ;
}
$out [ $i ] = trim ( $file -> current (), " \r \n " );
$file -> next ();
}
unset ( $file );
return $out ;
}
static public function htmlProduction ( \stdClass $report )
{
if ( ! headers_sent ()) {
header_remove ();
header ( 'HTTP/1.1 500 Internal Server Error' , true , 500 );
header ( 'Content-Type: text/html; charset=UTF-8' , true );
}
echo self :: htmlTemplate ( self :: $production_error_template , $report );
}
static public function htmlTemplate ( $str , \stdClass $report )
{
$str = strtr ( $str , [
'{$ref}' => $report -> context -> id ,
'{$report_json}' => htmlspecialchars ( base64_encode ( json_encode ( $report )), ENT_QUOTES ),
'{$report_url}' => htmlspecialchars (( string ) self :: $report_url ),
]);
$str = preg_replace_callback ( '!<if\((sent|logged|report|email|log)\)>(.*?)</if>!is' , function ( $match ) {
switch ( $match [ 1 ]) {
case 'sent' :
case 'email' :
return self :: $email_errors || ( self :: $report_auto && self :: $report_url ) ? $match [ 2 ] : '' ;
case 'logged' :
case 'log' :
return ini_get ( 'error_log' ) ? $match [ 2 ] : '' ;
case 'report' :
return ( ! self :: $report_auto && self :: $report_url ) ? $match [ 2 ] : '' ;
}
}, $str );
return $str ;
}
static public function htmlReport ( \stdClass $report ) : string
{
$out = '' ;
// Display debug
$out .= self :: htmlTemplate ( ini_get ( 'error_prepend_string' ), $report );
foreach ( $report -> errors as $e )
{
$out .= self :: htmlException ( $e );
}
$out .= '<section><article><h2>Context</h2><table>' ;
foreach ( $report -> context as $name => $value )
{
$out .= sprintf ( '<tr><th>%s</th><td>%s</td></tr>' ,
htmlspecialchars ( $name ),
htmlspecialchars ( $value ? ? '' ));
}
$out .= '</table></article></section>' ;
$out .= self :: htmlTemplate ( ini_get ( 'error_append_string' ), $report );
return $out ;
}
/**
* Enable error manager
* @ param integer $type Type of error management ( ErrorManager :: PRODUCTION or ErrorManager :: DEVELOPMENT )
2022-11-15 12:10:52 +00:00
* You can also use ErrorManager :: PRODUCTION | ErrorManager :: CLI_DEVELOPMENT to get error messages in CLI but still hide errors
* on web front - end .
2022-10-24 17:13:07 +00:00
* @ return void
*/
static public function enable ( $type = self :: DEVELOPMENT )
{
if ( self :: $enabled )
return true ;
2023-01-28 02:11:31 +00:00
self :: $context [ 'request_started' ] = $_SERVER [ 'REQUEST_TIME_FLOAT' ] ? ? microtime ( true );
2022-10-24 17:13:07 +00:00
self :: $enabled = $type ;
self :: $term_color = function_exists ( 'posix_isatty' ) && defined ( '\STDOUT' ) && @ posix_isatty ( \STDOUT );
ini_set ( 'display_errors' , false );
ini_set ( 'log_errors' , false );
ini_set ( 'html_errors' , false );
ini_set ( 'zend.exception_ignore_args' , false ); // We want to get the args in exceptions (since PHP 7.4)
2022-11-15 12:10:52 +00:00
error_reporting ( $type & self :: DEVELOPMENT ? - 1 : E_ALL & ~ E_DEPRECATED & ~ E_STRICT );
2022-10-24 17:13:07 +00:00
2022-11-15 12:10:52 +00:00
if ( $type & self :: DEVELOPMENT && PHP_SAPI != 'cli' )
2022-10-24 17:13:07 +00:00
{
self :: setHtmlHeader ( ' <! DOCTYPE html >< meta charset = " utf-8 " />< style type = " text/css " >
body { font - family : sans - serif ; } * { margin : 0 ; padding : 0 ; }
u , code b , i , h3 { font - style : normal ; font - weight : normal ; text - decoration : none ; }
#icn { color: #fff; font-size: 2em; float: right; margin: 1em; padding: 1em; background: #900; border-radius: 50%; }
section header { background : #fdd; padding: 1em; }
section article { margin : 1 em ; }
section article h3 , section article h4 { font - size : 1 em ; font - family : mono ; }
code { border : 1 px dotted #ccc; display: block; }
code b { margin - right : 1 em ; color : #999; }
code u { background : #fcc; display: inline-block; width: 100%; }
table { border - collapse : collapse ; margin : 1 em ; } td , th { border : 1 px solid #ccc; padding: .2em .5em; text-align: left;
vertical - align : top ; }
</ style >
< pre id = " icn " > \__ /< br /> ( xx ) < br />//|| \\\\ </ pre > ' );
}
register_shutdown_function ([ self :: class , 'shutdownHandler' ]);
set_exception_handler ([ __CLASS__ , 'exceptionHandler' ]);
set_error_handler ([ __CLASS__ , 'errorHandler' ]);
2022-11-15 12:10:52 +00:00
if ( $type & self :: DEVELOPMENT )
2022-10-24 17:13:07 +00:00
{
self :: startTimer ( '_global' );
}
// Assign default context
static $defaults = [
'hostname' => 'SERVER_NAME' ,
'http_user_agent' => 'HTTP_USER_AGENT' ,
'http_referrer' => 'HTTP_REFERER' ,
'user_addr' => 'REMOTE_ADDR' ,
'server_addr' => 'SERVER_ADDR' ,
'root_directory' => 'DOCUMENT_ROOT' ,
];
foreach ( $defaults as $a => $b )
{
if ( isset ( $_SERVER [ $b ]) && ! isset ( self :: $context [ $a ]))
{
self :: $context [ $a ] = $_SERVER [ $b ];
}
}
}
/**
* Reset error management to PHP defaults
* @ return boolean
*/
static public function disable ()
{
self :: $enabled = false ;
ini_set ( 'error_prepend_string' , null );
ini_set ( 'error_append_string' , null );
ini_set ( 'log_errors' , false );
ini_set ( 'display_errors' , false );
ini_set ( 'error_reporting' , E_ALL & ~ E_DEPRECATED & ~ E_STRICT );
restore_error_handler ();
return restore_exception_handler ();
}
/**
* Sets a microsecond timer to track time and memory usage
* @ param string $name Timer name
*/
static public function startTimer ( $name )
{
self :: $run_trace [ $name ] = [ microtime ( true ), memory_get_usage ()];
}
/**
* Stops a timer and return time spent and memory used
* @ param string $name Timer name
*/
static public function stopTimer ( $name )
{
self :: $run_trace [ $name ] = [
microtime ( true ) - self :: $run_trace [ $name ][ 0 ],
memory_get_usage () - self :: $run_trace [ $name ][ 1 ],
];
return self :: $run_trace [ $name ];
}
/**
* Sets a log file to record errors
* @ param string $file Error log file
*/
static public function setLogFile ( $file )
{
ini_set ( 'log_errors' , true );
return ini_set ( 'error_log' , $file );
}
/**
* Sets an email address that should receive the logs
* Set to FALSE to disable email sending ( default )
* @ param string $email Email address
*/
static public function setEmail ( $email )
{
self :: $email_errors = $email ;
}
/**
* @ deprecated
*/
static public function setExtraDebugEnv ( $env )
{
self :: setContext ( $env );
}
/**
* Set the report context
* @ param mixed $env Variable content , could be application version , or an array of information ...
*/
static public function setContext ( array $context )
{
self :: $context = array_merge ( self :: $context , $context );
}
/**
* Enable or disable reporting of errors to a remote URL
* @ param null | string $url Reporting URL
* @ param boolean $auto Automatic reporting ? If not users will be able to report the error by clicking a button on the error page
*/
static public function setRemoteReporting ( $url , $auto )
{
self :: $report_url = empty ( $url ) ? null : $url ;
self :: $report_auto = ( bool ) $auto ;
}
/**
* Set the HTML header used by the debug error page
* @ param string $html HTML header
*/
static public function setHtmlHeader ( $html )
{
ini_set ( 'error_prepend_string' , $html );
}
/**
* Set the HTML footer used by the debug error page
* @ param string $html HTML footer
*/
static public function setHtmlFooter ( $html )
{
ini_set ( 'error_append_string' , $html );
}
/**
* Set the content of the HTML template used to display an error in production
* { $ref } will be replaced by the error reference if log or email is enabled
* < if ( email ) >...</ if > block will be removed if email reporting is disabled
* < if ( log ) >...</ if > block will be removed if log reporting is disabled
* @ param string $html HTML template
*/
static public function setProductionErrorTemplate ( $html )
{
self :: $production_error_template = $html ;
}
static public function setCustomExceptionHandler ( $class , Callable $callback )
{
self :: $custom_handlers [ $class ] = $callback ;
}
static public function debug ( ... $vars )
{
echo '<pre>' ;
foreach ( $vars as $var ) {
echo self :: dump ( $var );
echo '<hr />' ;
}
echo '</pre>' ;
}
/**
* Copy of var_dump but returns a string instead of a variable
* @ param mixed $var variable to dump
* @ param bool $hide_values Do not return values if set to TRUE
* @ param integer $level Indentation level ( internal use )
* @ return string
*/
static public function dump ( $var , $hide_values = false , $level = 0 )
{
if ( $level > 20 )
{
return '*RECURSION*' ;
}
switch ( gettype ( $var ))
{
case 'boolean' :
return 'bool(' . ( $var ? 'true' : 'false' ) . ')' ;
case 'integer' :
return 'int(' . $var . ')' ;
case 'double' :
return 'float(' . $var . ')' ;
case 'string' :
return 'string(' . strlen ( $var ) . ') "' . ( $hide_values ? '***HIDDEN***' : $var ) . '"' ;
case 'NULL' :
return 'NULL' ;
case 'resource' :
return 'resource(' . ( int ) $var . ') of type (' . get_resource_type ( $var ) . ')' ;
case 'array' :
case 'object' :
if ( is_object ( $var ))
{
$out = 'object(' . get_class ( $var ) . ') (' . count (( array ) $var ) . ') {' . PHP_EOL ;
}
else
{
$out = 'array(' . count (( array ) $var ) . ') {' . PHP_EOL ;
}
$level ++ ;
if ( $var instanceof \Traversable ) {
$var2 = [];
// Iterate as long as we can
while ( @ $var -> valid ()) {
$var2 [] = $var -> current ();
$var -> next ();
}
$var = $var2 ;
}
foreach ( $var as $key => $value )
{
$out .= str_repeat ( ' ' , $level * 2 );
$out .= is_string ( $key ) ? '["' . $key . '"]' : '[' . $key . ']' ;
$out .= '=> ' . self :: dump ( $value , $hide_values , $level ) . PHP_EOL ;
}
$out .= str_repeat ( ' ' , -- $level * 2 ) . '}' ;
return $out ;
default :
return gettype ( $var );
}
}
/**
* Upload a report to a remote errbit - compatible API
* @ see https :// airbrake . io / docs / api / #create-notice-v3
*/
static public function sendReport ( \stdClass $report , $url )
{
$data = json_encode ( $report );
$headers = [
'Content-Type: application/json' ,
'Content-Lenth: ' . strlen ( $data ),
];
if ( function_exists ( 'curl_init' ))
{
$ch = curl_init ( $url );
curl_setopt_array ( $ch , [
CURLOPT_HTTPHEADER => $headers ,
CURLOPT_RETURNTRANSFER => true ,
CURLOPT_FOLLOWLOCATION => false ,
CURLOPT_MAXREDIRS => 3 ,
CURLOPT_CUSTOMREQUEST => 'POST' ,
CURLOPT_TIMEOUT => 10 ,
CURLOPT_POSTFIELDS => $data ,
]);
$body = curl_exec ( $ch );
$code = curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
curl_close ( $ch );
}
else
{
$opts = [ 'http' => [
'method' => 'POST' ,
'header' => $headers ,
'content' => $data ,
'max_redirects' => 3 ,
'timeout' => 10 ,
'ignore_errors' => true ,
]];
$body = file_get_contents ( $url , false , stream_context_create ( $opts ));
$code = null ;
foreach ( $http_response_header as $header )
{
$a = substr ( $header , 0 , 7 );
if ( $a == 'HTTP/1.' )
{
$code = substr ( $header , 11 , 3 );
}
}
unset ( $http_response_header );
}
return [
'code' => ( int ) $code ,
'body' => $body ,
'data' => json_decode ( $body ),
];
}
/**
* Returns list of reports from error log
*
* @ param string | null $log_file Log file to use , if NULL then the log file set in error_log will be used
* @ param string | null $filter_id Only return errors matching with this ID
*/
static public function getReportsFromLog ( $log_file = null , $filter_id = null )
{
if ( ! $log_file )
{
$log_file = ini_get ( 'error_log' );
}
if ( ! file_exists ( $log_file ))
{
return [];
}
$reports = [];
$report = null ;
foreach ( file ( $log_file ) as $line )
{
$line = trim ( $line );
if ( $line == '<errorReport>' )
{
$report = '' ;
}
elseif ( $line == '</errorReport>' )
{
$report = json_decode ( $report );
if ( ! is_null ( $report ) && isset ( $report -> context -> id ) && ( ! $filter_id || $filter_id == $report -> context -> id ))
{
$reports [] = $report ;
}
$report = null ;
}
elseif ( $report !== null )
{
$report .= $line ;
}
}
unset ( $line , $report , $log_file );
return $reports ;
}
}