PHP Classes
elePHPant
Icontem

File: ErrorManager.php

Recommend this page to a friend!
  Classes of Indrek Altpere  >  Simple error handling class  >  ErrorManager.php  >  Download  
File: ErrorManager.php
Role: Class source
Content type: text/plain
Description: Main class file
Class: Simple error handling class
Intercept and log PHP execution errors
Author: By
Last change: Documentation cleanup (typos).
Handle deprecation errors (compatible with PHP 5 older versions like 5.2).
Remove email field from automatically hidden sensitive field list.
Added GetLastError to fetch last recorded error array.
Add unhandled exception logging.
Improve logged data: detect cli mode, added request method name, added referer, added user agent, error time displayed with milliseconds, added UTC timestamp logging, if possible user $global_Start_time to also log request start time.
Simplified dieCallback calling.
Date: 5 months ago
Size: 15,955 bytes
 

Contents

Class file image Download
<?php

/**
 * @author Indrek Altpere
 * @copyright Indrek Altpere
 *
 * Provides means of better and more complete error logging with custom logfiles and stack traces with paassed variables to each error can be debugged and fixed more easily.
 */
/**
 * Set default values to error reporting variables:
 * No error displaying to end user, no startup errors (not to reveal inner workings of the site)
 * Error logging turned on and html errors off since in text logfile, html errors have no meaning.
 */
$errarr = array(
	'display_errors' => 'Off',
	'display_startup_errors' => 'Off',
	'log_errors' => 'On',
	'html_errors' => 'Off',
);

foreach ($errarr as $inikey => $inival) {
	ini_set($inikey, $inival);
}

//try to ensure working on older PHP5 versions
$_defines = array('E_RECOVERABLE_ERROR' => 4096 /*since 5.2.0*/, 'E_DEPRECATED' => 8192 /*since 5.3.0*/, 'E_USER_DEPRECATED' => 16384 /*since 5.3.0*/);
foreach ($_defines as $_errname => $_errvalue)
	if (!defined($_errname))
		define($_errname, $_errvalue);

class ErrorManager {

	/**
	 * Current log level
	 * @var int
	 */
	private static $logLevel = E_ALL;

	/**
	 * Current debug mode
	 * @var bool
	 */
	private static $debug = false;

	/**
	 * Error log file, stacktrace will be written to that file and detailed error information
	 * @var string
	 */
	private static $logFile = '';

	/**
	 * Major error log file (php parse error or some other huge error like calling a function on non object variable etc)
	 * @var string
	 */
	private static $fatalLogFile = '';

	/**
	 * Error level, when errormanager needs to call callback function if set and exit execution
	 * @var int
	 */
	private static $dieLevel = 0;

	/**
	 * Callback function to call when script is supposed to die
	 * @var string|array
	 */
	private static $dieCallBack = null;

	/**
	 * Whether to escape outputted debug code between html comment tags or to display it directly on page
	 * @var bool
	 */
	private static $asComments = false;

	/**
	 * Number of errors logged during the request
	 * @var int
	 */
	private static $errorCount = 0;

	/**
	 * Returns number of errors logged during the request
	 * @return int
	 */
	public static function ErrorCount() {
		return self::$errorCount;
	}

	/**
	 * Error code to readable string mappings
	 * @var array
	 */
	private static $errorTypes = array(
		E_PARSE => 'Parsing Error',
		E_ALL => 'All errors occurred at once',
		E_WARNING => 'Warning',
		E_CORE_WARNING => 'Core Warning',
		E_COMPILE_WARNING => 'Compile Warning',
		E_USER_WARNING => 'User Warning',
		E_ERROR => 'Error',
		E_CORE_ERROR => 'Core Error',
		E_COMPILE_ERROR => 'Compile Error',
		E_USER_ERROR => 'User Error',
		E_RECOVERABLE_ERROR => 'Recoverable error',
		E_NOTICE => 'Notice',
		E_USER_NOTICE => 'User Notice',
		E_DEPRECATED => 'Deprecated',
		E_USER_DEPRECATED => 'User Deprecated',
		E_STRICT => 'Strict Error',
	);

	/**
	 * Sensitive data keys that are to be filtered out and replaced with asterisks (*) so they will not be logged into error log
	 * @var array
	 */
	private static $sensitiveDataKeys = array('password', 'password1', 'password2');

	/**
	 * Sets current log level, log level that is caught by errorhandler when occurs
	 * @param int $newLogLevel Log level to set the error handler to catch
	 * @param bool $systemlevelalso Whether to set the php error_reporting to set variable too or not (might reduce php overhead of always calling log function when using only potion of available error codes)
	 */
	public static function SetLogLevel($newLogLevel, $systemlevelalso = false) {
		self::$logLevel = (int) $newLogLevel;
		if ($systemlevelalso) {
			error_reporting((int) $newLogLevel);
		}
	}

	/**
	 * Sets the debug mode on or off (debug mode = errors get output to browser, but between <!-- -->
	 * @param bool $debug
	 */
	public static function SetDebug($debug, $ascomments = true) {
		self::$debug = $debug === true || $debug === 'On';
		if (self::$debug) {
			$mode = 'On';
			self::$asComments = $ascomments;
		} else {
			$mode = 'Off';
			self::$asComments = false;
		}
		ini_set('display_errors', $mode);
		ini_set('display_startup_errors', $mode);
		ini_set('html_errors', $mode);
	}

	/**
	 * Sets main error logfile to new value
	 * @param string $logFile Log file where full stacktraces are logged (please use full path if possible)
	 * @param bool $setFatalLogfile Whether to set fatal log file to $logFile + '.sys.log' or not automatically
	 */
	public static function SetLogFile($logFile, $setFatalLogfile = true) {
		self::$logFile = $logFile;
		if ($setFatalLogfile) {
			self::SetFatalLogFile($logFile . '.sys.log');
		}
	}

	/**
	 * Set log file where php will log all fatal errors that php does not allow code to handle
	 * @param string $fatalLogFile Filename where fatal errors are logged (please use full path if possible)
	 */
	public static function SetFatalLogFile($fatalLogFile) {
		self::$fatalLogFile = $fatalLogFile;
		ini_set('error_log', $fatalLogFile);
	}

	/**
	 * Disables the new ErrorManager() instanciating
	 */
	private function __construct() {

	}

	private static $lastError = null;

	/**
	 * Returns last catched error
	 * @return array|null
	 */
	public static function GetLastError() {
		return self::$lastError;
	}

	public static function ClearLastError() {
		self::$lastError = null;
	}

	/**
	 * Function that does all the logging
	 * @param int $errno Error number/level
	 * @param string $errmsg Error message
	 * @param string $filename File where error occurred
	 * @param int $linenum Line where error occurred
	 * @param string $vars Function variables
	 */
	public static function Log($errno, $errmsg, $filename, $linenum, $vars, $exception = null) {
		//only remember stuff we're interested in
		if ($errno & self::$logLevel)
			self::$lastError = array('errno' => $errno, 'message' => $errmsg, 'file' => $filename, 'line' => $linenum);
		if (!($errno & self::$logLevel & error_reporting())) {
			return;
		}
		self::$errorCount++;
		global $user, $global_start_time;
		//global $user is assumed to be an object that has ->id and ->username fields in it
		//global $global_start_time is assumed to be script execution time, with milliseconds
		$unixtime = microtime(true);
		$secs = number_format($unixtime, 3, '.', '');
		$secs_str = substr($secs, strpos($secs, '.'));
		$time = date('Y-m-d H:i:s', floor($unixtime)) . $secs_str . ' (' . gmdate('Y-m-d H:i:s', floor($unixtime)) . $secs_str . ' UTC)';
		$is_exception = $exception && ($exception instanceof Exception || class_exists('Error', false) && $exception instanceof Error);
		//in exception mode, use Exception to get stacktrace, otherwise default back to debug_backtrace
		if ($is_exception) {
			$traceArr = $exception->getTrace();
			//add exception creation ifnromation as the first element on stack
			array_unshift($traceArr, array(
				'file' => $exception->getFile(),
				'line' => $exception->getLine(),
				'function' => '__construct',
				'class' => get_class($exception),
				'type' => '->',
				//for some weird exceptions, where constructor does magic and then calls parent::__construct with different values than passed in,
				// args shown in log will not be 1:1 to actual runtime values
				'args' => array($exception->getMessage(), $exception->getCode(), $exception->getPrevious()),
			));
		} else {
			$traceArr = debug_backtrace();
			//no trace or too short? make empty
			if (!is_array($traceArr) || count($traceArr) == 1)
				$traceArr = array();
			//remove the self call from stack
			array_shift($traceArr);
		}
		$traceStr = '';
		foreach ($traceArr as $trace) {
			$traceargs = '';
			if (isset($trace['args'])) {
				if (is_array($trace['args'])) {
					$traceargs = self::ArrayToString(self::RemoveSensitiveData($trace['args']));
				} else {
					$traceargs = self::RemoveSensitiveData($trace['args']);
				}
			}
			if (isset($trace['class'])) {
				//for baseobject, use its tostring to add [id=xx] to stacktrace
				$className = isset($trace['object']) && is_subclass_of($trace['object'], 'BaseObject', false) ? '' . $trace['object'] : $trace['class'];
				$traceStr .= "\t\t" . $className . (isset($trace['type']) ? $trace['type'] : (isset($trace['object']) ? '->' : '::')) . $trace['function'] . "(" . $traceargs . ')' . (isset($trace['file']) ? ' called from ' . $trace['file'] . (isset($trace['line']) ? ' at line ' . $trace['line'] : '') : '' ) . "\r\n";
			} else {
				$traceStr .= "\t\t" . $trace['function'] . "(" . $traceargs . ')' . (isset($trace['file']) ? ' called from ' . $trace['file'] . (isset($trace['line']) ? ' at line ' . $trace['line'] : '') : '' ) . "\r\n";
			}
		}
		$is_cli = empty($_SERVER['REMOTE_ADDR']);
		$err = $time . ' ip=' . (!$is_cli ? $_SERVER['REMOTE_ADDR'] : '(cli)') . ' user=' .
			($user && $user->id ?
				($user->username ? $user->username : '(empty)') . '[' . $user->id . ']' : 'guest' ) . ' phpvers=' . phpversion() . "\r\n";
		$err .= "\t" . ($is_exception ? 'Uncaught Exception' : (isset(self::$errorTypes[$errno]) ? self::$errorTypes[$errno] : 'Unkown error code ' . $errno)) . ': ' . $errmsg . "\r\n";
		$err .= "\t\tfile=" . $filename . ' line=' . $linenum . "\r\n";
		if ($is_cli)
			$err .= "\t\t\tSCRIPT_NAME=" . (isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '') . "\r\n";
		else
			$err .= "\t\t\tREQUEST_URI=" . (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '') .
				' METHOD=' . (isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '') .
				' LENGTH=' . (isset($_SERVER['CONTENT_LENGTH']) ? $_SERVER['CONTENT_LENGTH'] : 0) .
				"\r\n";
		if ($global_start_time) {
			$start_secs = number_format($global_start_time, 3, '.', '');
			$start_secs_str = substr($start_secs, strpos($start_secs, '.'));
			$err .= "\t\t\tSTART_TIME=" . date('Y-m-d H:i:s', floor($global_start_time)) . $start_secs_str . ' (' . gmdate('Y-m-d H:i:s', floor($global_start_time)) . " UTC)\r\n";
		}
		if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '')
			$err .= "\t\t\tREFERER=" . $_SERVER['HTTP_REFERER'] . "\r\n";
		if (isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] != '')
			$err .= "\t\t\tUSER_AGENT=" . $_SERVER['HTTP_USER_AGENT'] . "\r\n";
		if (isset($_GET) && count($_GET)) {
			$err .= "\t\t\t_GET = " . self::ArrayToString(self::RemoveSensitiveData($_GET), 1, "\t\t\t") . "\r\n";
		}
		if (isset($_POST) && count($_POST)) {
			$err .= "\t\t\t_POST = " . self::ArrayToString(self::RemoveSensitiveData($_POST), 1, "\t\t\t") . "\r\n";
		}
		if (isset($_FILES) && count($_FILES)) {
			$err .= "\t\t\t_FILES = " . self::ArrayToString(self::RemoveSensitiveData($_FILES), 1, "\t\t\t") . "\r\n";
		}
		$err .= "Trace:\r\n" . $traceStr;

		if (self::$debug) {
			if (self::$asComments) {
				echo "<!--\r\n" . str_replace(array('<!--', '-->'), array('<*!--', '--*>'), $err) . "\r\n-->\r\n";
			} else {
				echo "<pre>\r\n" . str_replace(array('<!--', '-->'), array('<*!--', '--*>'), $err) . "\r\n</pre>\r\n";
			}
		}
		$err .= "\r\n";

		//if logfile is set, let the system log into that file specifically by appending the composed string to it
		if (self::$logFile) {
			error_log($err, 3, self::$logFile);
			//in uncaught exception mode, which qualifies as fatal error, make sure to also log to fatalLogFile
			if (self::$fatalLogFile && $is_exception)
				error_log($err, 3, self::$fatalLogFile);
		} else {
			//otherwise log the error anywhere where the error_log has been set
			error_log($err);
		}
		//check if script needs to exit
		if ($errno & self::$dieLevel) {
			//check callback
			$call = self::$dieCallBack;
			if ($call && is_callable($call)) {
				$call($errno, $errmsg, $filename, $linenum, $vars);
			}
			exit;
		}
	}

	/**
	 * Removes sensitive data values from an array recursively
	 * @param array $array Array to unsensitize (passed by reference to reduce memory overhead by creating new arrays each time it is called)
	 * @return array Unsensitized array
	 */
	private static function RemoveSensitiveData($array, $level = 0) {
		//limit how deep logic goes
		if ($level >= 10)
			return $array;
		$is_array = is_array($array);
		$is_stdClass = !$is_array && is_object($array) && get_class($array) === 'stdClass';
		//do not process anything else than array/object
		if (!$is_array && !$is_stdClass)
			return $array;
		if ($is_array) {
			$arr = array();
			foreach ($array as $k => $v) {
				if (in_array($k, self::$sensitiveDataKeys, true)) {
					$arr[$k] = '*' . $k . '*';
				} elseif (is_array($v) || is_object($v) && get_class($v) === 'stdClass') {
					$arr[$k] = self::RemoveSensitiveData($v, $level + 1);
				} else
					$arr[$k] = $v;
			}
		} else { //is_stdObject
			$arr = new stdClass();
			foreach ($array as $k => $v) {
				if (in_array($k, self::$sensitiveDataKeys, true)) {
					$arr->$k = '*' . $k . '*';
				} elseif (is_array($v) || is_object($v) && get_class($v) === 'stdClass') {
					$arr->$k = self::RemoveSensitiveData($v, $level + 1);
				} else
					$arr->$k = $v;
			}
		}
		return $arr;
	}

	/**
	 * Converts array to string representation
	 * @param array $arr Array to convert to string
	 * @param int $level Level of recursion
	 * @return string String representation of an array
	 */
	private static function ArrayToString($arr, $level = 0) {
		$str = '';
		if ($level >= 10)
			return '*TOO DEEP, ' . $level . ' LEVELS*';
		$is_array = is_array($arr);
		$is_stdClass = !$is_array && is_object($arr) && get_class($arr) === 'stdClass';
		if ($is_stdClass)
			$arr = (array) $arr;
		if ($is_array || $is_stdClass) {
			$arrname = $is_array ? 'Array' : 'stdClass';
			if (!count($arr)) {
				if ($level == 0)
					return '';
				return $arrname . '()';
			}
			if ($level != 0)
				$str .= $arrname . '(';
			$i = 0;
			foreach ($arr as $k => $v) {
				$str .= ( $i > 0 ? ', ' : '') . ($level != 0 ? '[' . $k . '] => ' : '') . self::ArrayToString($v, $level + 1);
				$i++;
			}
			if ($level != 0)
				$str .= ')';
		} else {
			$bo = 'BaseObject';
			return (is_object($arr) ? ($arr instanceof $bo ? $arr . '' : get_class($arr)) : (is_string($arr) ? '"' . addslashes($arr) . '"' : (
						$arr === null ? 'null' : (is_bool($arr) ? ($arr ? 'true' : 'false') : $arr . '') )));
		}
		return $str;
	}

	/**
	 * Clears/unlinks logfiles
	 * @param bool $delete Whether to delete logfiles or just clear them
	 */
	public static function ClearLogs($delete = false) {
		if ($delete) {
			@unlink(self::$logFile);
			@unlink(self::$fatalLogFile);
		} else {
			@file_put_contents(self::$logFile, '');
			@file_put_contents(self::$fatalLogFile, '');
		}
	}

	/**
	 * Sets the die/exit error level when script is supposed to stop executing and set callback function that needs to be called before script dies/exits
	 * @param int $level Die/exit error level for script (E_ERROR or E_STRICT or such)
	 * @param string/array $callback Can be just a string (global function) or array that has two members, first is classname for static classes or object for object instances and second is function name that is called (error log parameters are passed on to function)
	 */
	public static function SetDieLevel($level = 0, $callback = null) {
		self::$dieLevel = $level;
		self::$dieCallBack = $callback;
	}

	/**
	 * Wrapper to handle uncaught exceptions
	 * @param Exception $exception
	 */
	public static function LogException($exception) {
		self::Log(E_ERROR, $exception->getMessage(), $exception->getFile(), $exception->getLine(), [], $exception);
		//make sure we still output fatal error headers, catching exception seems to default to 200 otherwise
		$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
		header($protocol . ' 500 Internal Server Error');
	}

}

//Set the error handling function to ErrorManager::Log()
set_error_handler(array('ErrorManager', 'Log'));

//Set the exception handling function to ErrorManager::LogException
set_exception_handler(array('ErrorManager', 'LogException'));