Index: branches/5.2.x/core/kernel/utility/logger.php
===================================================================
--- branches/5.2.x/core/kernel/utility/logger.php	(revision 16713)
+++ branches/5.2.x/core/kernel/utility/logger.php	(revision 16714)
@@ -1,1510 +1,1522 @@
 <?php
 /**
 * @version	$Id$
 * @package	In-Portal
 * @copyright	Copyright (C) 1997 - 2012 Intechnic. All rights reserved.
 * @license      GNU/GPL
 * In-Portal is Open Source software.
 * This means that this software may have been modified pursuant
 * the GNU General Public License, and as distributed it includes
 * or is derivative of works licensed under the GNU General Public License
 * or other free or open source software licenses.
 * See http://www.in-portal.org/license for copyright notices and details.
 */
 
 defined('FULL_PATH') or die('restricted access!');
 
 /**
  * Class for logging system activity
  */
 class kLogger extends kBase {
 
 	/**
 	 * Prefix of all database related errors
 	 */
 	const DB_ERROR_PREFIX = 'SQL Error:';
 
 	/**
 	 * Logger state: logging of errors and user-defined messages
 	 */
 	const STATE_ENABLED = 1;
 
 	/**
 	 * Logger state: logging of user-defined messages only
 	 */
 	const STATE_USER_ONLY = 2;
 
 	/**
 	 * Logger state: don't log anything
 	 */
 	const STATE_DISABLED = 0;
 
 	/**
 	 * Log store: automatically determine where log should be written
 	 */
 	const LS_AUTOMATIC = 1;
 
 	/**
 	 * Log store: always write log to database
 	 */
 	const LS_DATABASE = 2;
 
 	/**
 	 * Log store: always write log to disk
 	 */
 	const LS_DISK = 3;
 
 	/**
 	 * Log level: system is unusable
 	 */
 	const LL_EMERGENCY = 0;
 
 	/**
 	 * Log level: action must be taken immediately
 	 */
 	const LL_ALERT = 1;
 
 	/**
 	 * Log level: the system is in a critical condition
 	 */
 	const LL_CRITICAL = 2;
 
 	/**
 	 * Log level: there is an error condition
 	 */
 	const LL_ERROR = 3;
 
 	/**
 	 * Log level: there is a warning condition
 	 */
 	const LL_WARNING = 4;
 
 	/**
 	 * Log level: a normal but significant condition
 	 */
 	const LL_NOTICE = 5;
 
 	/**
 	 * Log level: a purely informational message
 	 */
 	const LL_INFO = 6;
 
 	/**
 	 * Log level: messages generated to debug the application
 	 */
 	const LL_DEBUG = 7;
 
 	/**
 	 * Log type: PHP related activity
 	 */
 	const LT_PHP = 1;
 
 	/**
 	 * Log type: database related activity
 	 */
 	const LT_DATABASE = 2;
 
 	/**
 	 * Log type: custom activity
 	 */
 	const LT_OTHER = 3;
 
 	/**
 	 * Log interface: Front
 	 */
 	const LI_FRONT = 1;
 
 	/**
 	 * Log interface: Admin
 	 */
 	const LI_ADMIN = 2;
 
 	/**
 	 * Log interface: Cron (Front)
 	 */
 	const LI_CRON_FRONT = 3;
 
 	/**
 	 * Log interface: Cron (Admin)
 	 */
 	const LI_CRON_ADMIN = 4;
 
 	/**
 	 * Log interface: API
 	 */
 	const LI_API = 5;
 
 	/**
 	 * Log notification status: disabled
 	 */
 	const LNS_DISABLED = 0;
 
 	/**
 	 * Log notification status: pending
 	 */
 	const LNS_PENDING = 1;
 
 	/**
 	 * Log notification status: sent
 	 */
 	const LNS_SENT = 2;
 
 	/**
 	 * Database connection used for logging.
 	 *
 	 * @var kDBConnection
 	 */
 	protected $dbStorage;
 
 	/**
 	 * List of error/exception handlers
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected $_handlers = Array ();
 
 	/**
 	 * Long messages are saved here, because "trigger_error" doesn't support error messages over 1KB in size
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected static $_longMessages = Array ();
 
 	/**
 	 * Log record being worked on
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected $_logRecord = Array ();
 
 	/**
 	 * Maximal level of a message, that can be logged
 	 *
 	 * @var int
 	 * @access protected
 	 */
 	protected $_maxLogLevel = self::LL_NOTICE;
 
 	/**
 	 * State of the logger
 	 *
 	 * @var int
 	 * @access protected
 	 */
 	protected $_state = self::STATE_DISABLED;
 
 	/**
 	 * Caches state of debug mode
 	 *
 	 * @var bool
 	 * @access protected
 	 */
 	protected $_debugMode = false;
 
 	/**
 	 * Ignores backtrace record where following classes/files are mentioned
 	 *
 	 * @var array
 	 */
 	protected $_ignoreInTrace = array(
 		'kLogger' => 'logger.php',
 		'kErrorHandlerStack' => 'logger.php',
 		'kExceptionHandlerStack' => 'logger.php',
 		'kDBConnection' => 'db_connection.php',
 		'kDBConnectionDebug' => 'db_connection.php',
 		'kDBLoadBalancer' => 'db_load_balancer.php',
 	);
 
 	/**
 	 * Create event log
 	 *
 	 * @param Array $methods_to_call List of invokable kLogger class method with their parameters (if any)
 	 * @access public
 	 */
 	public function __construct($methods_to_call = Array ())
 	{
 		parent::__construct();
 
 		$this->dbStorage = $this->getDBStorage();
 
 		$system_config = kUtil::getSystemConfig();
 
 		$this->_debugMode = $this->Application->isDebugMode();
 		$this->setState($system_config->get('EnableSystemLog', self::STATE_DISABLED));
 		$this->_maxLogLevel = $system_config->get('SystemLogMaxLevel', self::LL_NOTICE);
 
 		foreach ($methods_to_call as $method_to_call) {
 			call_user_func_array(Array ($this, $method_to_call[0]), $method_to_call[1]);
 		}
 
 		if ( !kUtil::constOn('DBG_ZEND_PRESENT') && !$this->Application->isDebugMode() ) {
 			// don't report error on screen if debug mode is turned off
 			error_reporting(0);
 			ini_set('display_errors', 0);
 		}
 
 		register_shutdown_function(Array ($this, 'catchLastError'));
 	}
 
 	/**
 	 * Create separate connection for logging purposes.
 	 *
 	 * @return kDBConnection
 	 */
 	protected function getDBStorage()
 	{
 		$system_config = new kSystemConfig(true);
 		$vars = $system_config->getData();
 		$db_class = $this->Application->isDebugMode() ? 'kDBConnectionDebug' : 'kDBConnection';
 
 		// Can't use "kApplication::makeClass", because class factory isn't initialized at this point.
 		$db = new $db_class(SQL_TYPE, array($this, 'handleSQLError'), 'logger');
 		$db->setup($vars);
 
 		return $db;
 	}
 
 	/**
 	 * Sets state of the logged (enabled/user-only/disabled)
 	 *
 	 * @param $new_state
 	 * @return void
 	 * @access public
 	 */
 	public function setState($new_state = null)
 	{
 		if ( isset($new_state) ) {
 			$this->_state = (int)$new_state;
 		}
 
 		if ( $this->_state === self::STATE_ENABLED ) {
 			$this->_enableErrorHandling();
 		}
 		elseif ( $this->_state === self::STATE_DISABLED ) {
 			$this->_disableErrorHandling();
 		}
 	}
 
 	/**
 	 * Enable error/exception handling capabilities
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function _enableErrorHandling()
 	{
 		$this->_disableErrorHandling();
 
 		$this->_handlers[self::LL_ERROR] = new kErrorHandlerStack($this);
 		$this->_handlers[self::LL_CRITICAL] = new kExceptionHandlerStack($this);
 	}
 
 	/**
 	 * Disables error/exception handling capabilities
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function _disableErrorHandling()
 	{
 		foreach ($this->_handlers as $index => $handler) {
 			$this->_handlers[$index]->__destruct();
 			unset($this->_handlers[$index]);
 		}
 	}
 
 	/**
 	 * Initializes new log record. Use "kLogger::write" to save to db/disk
 	 *
 	 * @param string $message
 	 * @param int $code
 	 * @return kLogger
 	 * @access public
 	 */
 	public function prepare($message = '', $code = null)
 	{
 		$this->_logRecord = Array (
 			'LogUniqueId' => kUtil::generateId(),
 			'LogMessage' => $message,
 			'LogLevel' => self::LL_INFO,
 			'LogCode' => $code,
 			'LogType' => self::LT_OTHER,
 			'LogHostname' => $_SERVER['HTTP_HOST'],
 			'LogRequestSource' => php_sapi_name() == 'cli' ? 2 : 1,
 			'LogRequestURI' => php_sapi_name() == 'cli' ? implode(' ', $GLOBALS['argv']) : $_SERVER['REQUEST_URI'],
 			'LogUserId' => USER_GUEST,
 			'IpAddress' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
 			'LogSessionKey' => 0,
 			'LogProcessId' => getmypid(),
 			'LogUserData' => '',
 			'LogNotificationStatus' => self::LNS_DISABLED,
 		);
 
 		if ( $this->Application->isAdmin ) {
 			$this->_logRecord['LogInterface'] = defined('CRON') && CRON ? self::LI_CRON_ADMIN : self::LI_ADMIN;
 		}
 		else {
 			$this->_logRecord['LogInterface'] = defined('CRON') && CRON ? self::LI_CRON_FRONT : self::LI_FRONT;
 		}
 
 		if ( $this->Application->InitDone ) {
 			$this->_logRecord['LogUserId'] = $this->Application->RecallVar('user_id');
 			$this->_logRecord['LogSessionKey'] = $this->Application->GetSID();
 			$this->_logRecord['IpAddress'] = $this->Application->getClientIp();
 		}
 
 		return $this;
 	}
 
 	/**
 	 * Sets one or more fields of log record
 	 *
 	 * @param string|Array $field_name
 	 * @param string|null $field_value
 	 * @return kLogger
 	 * @access public
 	 * @throws UnexpectedValueException
 	 */
 	public function setLogField($field_name, $field_value = null)
 	{
 		if ( isset($field_value) ) {
 			$this->_logRecord[$field_name] = $field_value;
 		}
 		elseif ( is_array($field_name) ) {
 			$this->_logRecord = array_merge($this->_logRecord, $field_name);
 		}
 		else {
 			throw new UnexpectedValueException('Invalid arguments');
 		}
 
 		return $this;
 	}
 
 	/**
 	 * Sets user data
 	 *
 	 * @param string $data
 	 * @param bool $as_array
 	 * @return kLogger
 	 * @access public
 	 */
 	public function setUserData($data, $as_array = false)
 	{
 		if ( $as_array ) {
 			$data = serialize((array)$data);
 		}
 
 		return $this->setLogField('LogUserData', $data);
 	}
 
 	/**
 	 * Add user data
 	 *
 	 * @param string $data
 	 * @param bool $as_array
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addUserData($data, $as_array = false)
 	{
 		$new_data = $this->_logRecord['LogUserData'];
 
 		if ( $as_array ) {
 			$new_data = $new_data ? unserialize($new_data) : Array ();
 			$new_data[] = $data;
 			$new_data = serialize($new_data);
 		}
 		else {
 			$new_data .= ($new_data ? PHP_EOL : '') . $data;
 		}
 
 		return $this->setLogField('LogUserData', $new_data);
 	}
 
 	/**
 	 * Adds event to log record
 	 *
 	 * @param kEvent $event
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addEvent(kEvent $event)
 	{
 		$this->_logRecord['LogEventName'] = (string)$event;
 
 		return $this;
 	}
 
 	/**
 	 * Adds log source file & file to log record
 	 *
 	 * @param string|Array $file_or_trace file path
 	 * @param int $line file line
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addSource($file_or_trace = '', $line = 0)
 	{
 		if ( is_array($file_or_trace) ) {
 			$trace_info = $file_or_trace[0];
 			$this->_logRecord['LogSourceFilename'] = $trace_info['file'];
 			$this->_logRecord['LogSourceFileLine'] = $trace_info['line'];
 		}
 		else {
 			$this->_logRecord['LogSourceFilename'] = $file_or_trace;
 			$this->_logRecord['LogSourceFileLine'] = $line;
 		}
 
 		return $this;
 	}
 
 	/**
 	 * Adds session contents to log record
 	 *
 	 * @param bool $include_optional Include optional session variables
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addSessionData($include_optional = false)
 	{
 		if ( $this->Application->InitDone ) {
 			$this->_logRecord['LogSessionData'] = serialize($this->Application->Session->getSessionData($include_optional));
 		}
 
 		return $this;
 	}
 
 	/**
 	 * Adds user request information to log record
 	 *
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addRequestData()
 	{
 		$request_data = array(
 			'Headers' => $this->Application->HttpQuery->getHeaders(),
 		);
 
 		$request_variables = Array('_GET' => $_GET, '_POST' => $_POST, '_COOKIE' => $_COOKIE);
 
 		foreach ( $request_variables as $title => $data ) {
 			if ( !$data ) {
 				continue;
 			}
 
 			$request_data[$title] = $data;
 		}
 
 		$this->_logRecord['LogRequestData'] = serialize($request_data);
 
 		return $this;
 	}
 
 	/**
 	 * Adds trace to log record
 	 *
 	 * @param Array $trace
 	 * @param int $skip_levels
 	 * @param Array $skip_files
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addTrace($trace = null, $skip_levels = 1, $skip_files = null)
 	{
 		$trace = $this->createTrace($trace, $skip_levels, $skip_files);
 
 		foreach ($trace as $trace_index => $trace_info) {
 			if ( isset($trace_info['args']) ) {
 				$trace[$trace_index]['args'] = $this->_implodeObjects($trace_info['args']);
 			}
 		}
 
 		$this->_logRecord['LogBacktrace'] = serialize($this->_removeObjectsFromTrace($trace));
 
 		return $this;
 	}
 
 	/**
 	 * Remove objects from trace, since before PHP 5.2.5 there wasn't possible to remove them initially
 	 *
 	 * @param Array $trace
 	 * @return Array
 	 * @access protected
 	 */
 	protected function _removeObjectsFromTrace($trace)
 	{
 		if ( version_compare(PHP_VERSION, '5.3', '>=') ) {
 			return $trace;
 		}
 
 		$trace_indexes = array_keys($trace);
 
 		foreach ($trace_indexes as $trace_index) {
 			unset($trace[$trace_index]['object']);
 		}
 
 		return $trace;
 	}
 
 	/**
 	 * Implodes object to prevent memory leaks
 	 *
 	 * @param Array $array
 	 * @return Array
 	 * @access protected
 	 */
 	protected function _implodeObjects($array)
 	{
 		$ret = Array ();
 
 		foreach ($array as $key => $value) {
 			if ( is_array($value) ) {
 				$ret[$key] = $this->_implodeObjects($value);
 			}
 			elseif ( is_object($value) ) {
 				if ( $value instanceof kEvent ) {
 					$ret[$key] = 'Event: ' . (string)$value;
 				}
 				elseif ( $value instanceof kBase ) {
 					$ret[$key] = (string)$value;
 				}
 				else {
 					$ret[$key] = 'Class: ' . get_class($value);
 				}
 			}
 			elseif ( is_resource($value) ) {
 				$ret[$key] = (string)$value;
 			}
 			elseif ( strlen($value) > 200 ) {
 				$ret[$key] = substr($value, 0, 50) . ' ...';
 			}
 			else {
 				$ret[$key] = $value;
 			}
 		}
 
 		return $ret;
 	}
 
 	/**
 	 * Removes first N levels from trace
 	 *
 	 * @param Array $trace
 	 * @param int $levels
 	 * @param Array $files
 	 * @return Array
 	 * @access public
 	 */
 	public function createTrace($trace = null, $levels = null, $files = null)
 	{
 		if ( !isset($trace) ) {
 			$trace = debug_backtrace(false);
 		}
 
 		if ( !$trace ) {
 			// no trace information
 			return $trace;
 		}
 
 		if ( isset($levels) && is_numeric($levels) ) {
 			for ($i = 0; $i < $levels; $i++) {
 				array_shift($trace);
 			}
 		}
 
 		if ( isset($files) && is_array($files) ) {
 			$classes = array_keys($files);
 
 			while ( true ) {
 				$trace_info = $trace[0];
 				$file = isset($trace_info['file']) ? basename($trace_info['file']) : '';
 				$class = isset($trace_info['class']) ? $trace_info['class'] : '';
 
 				if ( ($file && !in_array($file, $files)) || ($class && !in_array($class, $classes)) ) {
 					break;
 				}
 
 				array_shift($trace);
 			}
 		}
 
 		return $trace;
 	}
 
 	/**
 	 * Adds PHP error to log record
 	 *
 	 * @param int $errno
 	 * @param string $errstr
 	 * @param string $errfile
 	 * @param int $errline
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addError($errno, $errstr, $errfile = null, $errline = null)
 	{
 		$errstr = self::expandMessage($errstr, !$this->_debugMode);
 		$this->_logRecord['LogLevel'] = $this->_getLogLevelByErrorNo($errno);
 
 		if ( $this->isLogType(self::LT_DATABASE, $errstr) ) {
 			list ($errno, $errstr, $sql) = self::parseDatabaseError($errstr);
 
 			$this->_logRecord['LogType'] = self::LT_DATABASE;
 			$this->_logRecord['LogUserData'] = $sql;
 
 			$trace = $this->createTrace(null, 4, $this->_ignoreInTrace);
 			$this->addSource($trace);
 			$this->addTrace($trace, 0);
 		}
 		else {
 			$this->_logRecord['LogType'] = self::LT_PHP;
 
 			$this->addSource((string)$errfile, $errline);
 			$this->addTrace(null, 4);
 		}
 
 		$this->_logRecord['LogCode'] = $errno;
 		$this->_logRecord['LogMessage'] = $errstr;
 
 		return $this;
 	}
 
 	/**
 	 * Adds PHP exception to log record
 	 *
 	 * @param Exception $exception
 	 * @return kLogger
 	 * @access public
 	 */
 	public function addException($exception)
 	{
 		$errstr = self::expandMessage($exception->getMessage(), !$this->_debugMode);
 		$this->_logRecord['LogLevel'] = self::LL_CRITICAL;
 
 		$exception_trace = $exception->getTrace();
 
 		array_unshift($exception_trace, array(
 			'function' => '',
 			'file' => $exception->getFile() !== null ? $exception->getFile() : 'n/a',
 			'line' => $exception->getLine() !== null ? $exception->getLine() : 'n/a',
 			'args' => array(),
 		));
 
 		if ( $this->isLogType(self::LT_DATABASE, $errstr) ) {
 			list ($errno, $errstr, $sql) = self::parseDatabaseError($errstr);
 
 			$this->_logRecord['LogType'] = self::LT_DATABASE;
 			$this->_logRecord['LogUserData'] = $sql;
 
 			$trace = $this->createTrace($exception_trace, null, $this->_ignoreInTrace);
 			$this->addSource($trace);
 			$this->addTrace($trace, 0);
 		}
 		else {
 			$this->_logRecord['LogType'] = self::LT_PHP;
 			$errno = $exception->getCode();
 
 			$this->addSource((string)$exception->getFile(), $exception->getLine());
 			$this->addTrace($exception_trace, 0);
 		}
 
 		$this->_logRecord['LogCode'] = $errno;
 		$this->_logRecord['LogMessage'] = $errstr;
 
 		return $this;
 	}
 
 	/**
 	 * Allows to map PHP error numbers to syslog log level
 	 *
 	 * @param int $errno
 	 * @return int
 	 * @access protected
 	 */
 	protected function _getLogLevelByErrorNo($errno)
 	{
 		$error_number_mapping = Array (
 			self::LL_ERROR 		=>	Array (E_RECOVERABLE_ERROR, E_USER_ERROR, E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE),
 			self::LL_WARNING	=>	Array (E_WARNING, E_USER_WARNING, E_CORE_WARNING, E_COMPILE_WARNING),
 			self::LL_NOTICE		=>	Array (E_NOTICE, E_USER_NOTICE, E_STRICT),
 		);
 
 		if ( version_compare(PHP_VERSION, '5.3.0', '>=') ) {
 			$error_number_mapping[self::LL_NOTICE][] = E_DEPRECATED;
 			$error_number_mapping[self::LL_NOTICE][] = E_USER_DEPRECATED;
 		}
 
 		foreach ($error_number_mapping as $log_level => $error_numbers) {
 			if ( in_array($errno, $error_numbers) ) {
 				return $log_level;
 			}
 		}
 
 		return self::LL_ERROR;
 	}
 
 	/**
 	 * Changes log level of a log record
 	 *
 	 * @param int $log_level
 	 * @return kLogger
 	 * @access public
 	 */
 	public function setLogLevel($log_level)
 	{
 		$this->_logRecord['LogLevel'] = $log_level;
 
 		return $this;
 	}
 
 	/**
 	 * Writes prepared log to database or disk, when database isn't available
 	 *
 	 * @param integer $storage_medium Storage medium.
 	 * @param boolean $check_origin   Check error origin.
 	 *
 	 * @return integer|boolean
 	 * @throws InvalidArgumentException When unknown storage medium is given.
 	 */
 	public function write($storage_medium = self::LS_AUTOMATIC, $check_origin = false)
 	{
 		if ( $check_origin && isset($this->_logRecord['LogSourceFilename']) ) {
 			$origin_allowed = self::isErrorOriginAllowed($this->_logRecord['LogSourceFilename']);
 		}
 		else {
 			$origin_allowed = true;
 		}
 
 		if ( !$this->_logRecord
 			|| $this->_logRecord['LogLevel'] > $this->_maxLogLevel
 			|| !$origin_allowed
 			|| $this->_state == self::STATE_DISABLED
 		) {
 			// Nothing to save OR less detailed logging requested OR origin not allowed OR disabled.
 			$this->_logRecord = array();
 
 			return false;
 		}
 
 		$this->_logRecord['LogMemoryUsed'] = memory_get_usage();
 		$this->_logRecord['LogTimestamp'] = adodb_mktime();
 		$this->_logRecord['LogDate'] = adodb_date('Y-m-d H:i:s');
 
 		if ( $storage_medium == self::LS_AUTOMATIC ) {
 			$storage_medium = $this->dbStorage->connectionOpened() ? self::LS_DATABASE : self::LS_DISK;
 		}
 
 		if ( $storage_medium == self::LS_DATABASE ) {
 			$result = $this->dbStorage->doInsert($this->_logRecord, TABLE_PREFIX . 'SystemLog');
 		}
 		elseif ( $storage_medium == self::LS_DISK ) {
 			$result = $this->_saveToFile(RESTRICTED . '/system.log');
 		}
 		else {
 			throw new InvalidArgumentException('Unknown storage medium "' . $storage_medium . '"');
 		}
 
 		$unique_id = $this->_logRecord['LogUniqueId'];
 
 		if ( $this->_logRecord['LogNotificationStatus'] == self::LNS_SENT ) {
 			$this->_sendNotification($unique_id);
 		}
 
 		$this->_logRecord = Array ();
 
 		return $result ? $unique_id : false;
 	}
 
 	/**
 	 * Catches last error happened before script ended
 	 *
 	 * @return void
 	 * @access public
 	 */
 	public function catchLastError()
 	{
 		$this->write();
 		$last_error = error_get_last();
 
 		if ( !is_null($last_error) && isset($this->_handlers[self::LL_ERROR]) ) {
 			/** @var kErrorHandlerStack $handler */
 			$handler = $this->_handlers[self::LL_ERROR];
 
 			$handler->handle($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']);
 		}
 	}
 
 	/**
 	 * Deletes log with given id from database or disk, when database isn't available
 	 *
 	 * @param int $unique_id
 	 * @param int $storage_medium
 	 * @return void
 	 * @access public
 	 * @throws InvalidArgumentException
 	 */
 	public function delete($unique_id, $storage_medium = self::LS_AUTOMATIC)
 	{
 		if ( $storage_medium == self::LS_AUTOMATIC ) {
 			$storage_medium = $this->dbStorage->connectionOpened() ? self::LS_DATABASE : self::LS_DISK;
 		}
 
 		if ( $storage_medium == self::LS_DATABASE ) {
 			$sql = 'DELETE FROM ' . TABLE_PREFIX . 'SystemLog
 					WHERE LogUniqueId = ' . $unique_id;
 			$this->dbStorage->Query($sql);
 		}
 		elseif ( $storage_medium == self::LS_DISK ) {
 			// TODO: no way to delete a line from a file
 		}
 		else {
 			throw new InvalidArgumentException('Unknown storage medium "' . $storage_medium . '"');
 		}
 	}
 
 	/**
 	 * Send notification (delayed or instant) about log record to e-mail from configuration
 	 *
 	 * @param bool $instant
 	 * @return kLogger
 	 * @access public
 	 */
 	public function notify($instant = false)
 	{
 		$this->_logRecord['LogNotificationStatus'] = $instant ? self::LNS_SENT : self::LNS_PENDING;
 
 		return $this;
 	}
 
 	/**
 	 * Sends notification e-mail about message with given $unique_id
 	 *
 	 * @param int $unique_id
 	 * @return void
 	 * @access protected
 	 */
 	protected function _sendNotification($unique_id)
 	{
 		$notification_email = $this->Application->ConfigValue('SystemLogNotificationEmail');
 
 		if ( !$notification_email ) {
 			trigger_error('System Log notification E-mail not specified', E_USER_NOTICE);
 
 			return;
 		}
 
 		$send_params = Array (
 			'to_name' => $notification_email,
 			'to_email' => $notification_email,
 		);
 
 		// initialize list outside of e-mail event with right settings
 		$this->Application->recallObject('system-log.email', 'system-log_List', Array ('unique_id' => $unique_id));
 
 		$this->Application->emailAdmin('SYSTEM.LOG.NOTIFY', null, $send_params);
 		$this->Application->removeObject('system-log.email');
 	}
 
 	/**
 	 * Adds error/exception handler
 	 *
 	 * @param string|Array $handler
 	 * @param bool $is_exception
 	 * @return void
 	 * @access public
 	 */
 	public function addErrorHandler($handler, $is_exception = false)
 	{
 		$this->_handlers[$is_exception ? self::LL_CRITICAL : self::LL_ERROR]->add($handler);
 	}
 
 	/**
 	 * SQL Error Handler
 	 *
 	 * When not debug mode, then fatal database query won't break anything.
 	 *
 	 * @param int $code
 	 * @param string $msg
 	 * @param string $sql
 	 * @return bool
 	 * @access public
 	 * @throws RuntimeException
 	 */
 	public function handleSQLError($code, $msg, $sql)
 	{
 		$error_msg = self::shortenMessage(self::DB_ERROR_PREFIX . ' #' . $code . ' - ' . $msg . '. SQL: ' . trim($sql));
 
 		if ( isset($this->Application->Debugger) ) {
 			if ( kUtil::constOn('DBG_SQL_FAILURE') && !defined('IS_INSTALL') ) {
 				throw new RuntimeException($error_msg);
 			}
 			else {
 				$this->Application->Debugger->appendTrace();
 			}
 		}
 
 		if ( PHP_SAPI === 'cli' ) {
 			throw new RuntimeException($error_msg);
 		}
 
 		// Next line also trigger attached error handlers.
 		trigger_error($error_msg, E_USER_WARNING);
 
 		return true;
 	}
 
 	/**
 	 * Packs information about error into a single line
 	 *
 	 * @param string $errno
 	 * @param bool $strip_tags
 	 * @return string
 	 * @access public
 	 */
 	public function toString($errno = null, $strip_tags = false)
 	{
 		if ( !isset($errno) ) {
 			$errno = $this->_logRecord['LogCode'];
 		}
 
 		$errstr = $this->_logRecord['LogMessage'];
-		$errfile = $this->_logRecord['LogSourceFilename'];
+		$errfile = $this->convertPathToRelative($this->_logRecord['LogSourceFilename']);
 		$errline = $this->_logRecord['LogSourceFileLine'];
 
 		if ( PHP_SAPI === 'cli' ) {
 			$result = sprintf('  [%s]  ' . PHP_EOL . '  %s', $errno, $errstr);
 
 			if ( $this->_logRecord['LogBacktrace'] ) {
 				$result .= $this->printBacktrace(unserialize($this->_logRecord['LogBacktrace']));
 			}
 		}
 		else {
 			$result = '<strong>' . $errno . ': </strong>' . "{$errstr} in {$errfile} on line {$errline}";
 		}
 
 		return $strip_tags ? strip_tags($result) : $result;
 	}
 
 	/**
 	 * Prints backtrace result
 	 *
 	 * @param array $trace Trace.
 	 *
 	 * @return string
 	 */
 	protected function printBacktrace(array $trace)
 	{
 		if ( !$trace ) {
 			return '';
 		}
 
 		$ret = PHP_EOL . PHP_EOL . PHP_EOL . 'Exception trace:' . PHP_EOL;
 
 		foreach ( $trace as $trace_info ) {
 			$class = isset($trace_info['class']) ? $trace_info['class'] : '';
 			$type = isset($trace_info['type']) ? $trace_info['type'] : '';
 			$function = $trace_info['function'];
 			$args = isset($trace_info['args']) && $trace_info['args'] ? '...' : '';
-			$file = isset($trace_info['file']) ? $trace_info['file'] : 'n/a';
+			$file = isset($trace_info['file']) ? $this->convertPathToRelative($trace_info['file']) : 'n/a';
 			$line = isset($trace_info['line']) ? $trace_info['line'] : 'n/a';
 
 			$ret .= sprintf(' %s%s%s(%s) at %s:%s' . PHP_EOL, $class, $type, $function, $args, $file, $line);
 		}
 
 		return $ret;
 	}
 
 	/**
+	 * Short description.
+	 *
+	 * @param string $absolute_path Absolute path.
+	 *
+	 * @return string
+	 */
+	protected function convertPathToRelative($absolute_path)
+	{
+		return preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '...', $absolute_path, 1);
+	}
+
+	/**
 	 * Saves log to file (e.g. when not possible to save into database)
 	 *
 	 * @param $filename
 	 * @return bool
 	 * @access protected
 	 */
 	protected function _saveToFile($filename)
 	{
 		$time = adodb_date('Y-m-d H:i:s');
 		$log_file = new SplFileObject($filename, 'a');
 
 		return $log_file->fwrite('[' . $time . '] #' . $this->toString(null, true) . PHP_EOL) > 0;
 	}
 
 	/**
 	 * Checks if log type of current log record matches given one
 	 *
 	 * @param int $log_type
 	 * @param string $log_message
 	 * @return bool
 	 * @access public
 	 */
 	public function isLogType($log_type, $log_message = null)
 	{
 		if ( $this->_logRecord['LogType'] == $log_type ) {
 			return true;
 		}
 
 		if ( $log_type == self::LT_DATABASE ) {
 			if ( !isset($log_message) ) {
 				$log_message = $this->_logRecord['LogMessage'];
 			}
 
 			return strpos($log_message, self::DB_ERROR_PREFIX) !== false;
 		}
 
 		return false;
 	}
 
 	/**
 	 * Shortens message
 	 *
 	 * @param string $message
 	 * @return string
 	 * @access public
 	 */
 	public static function shortenMessage($message)
 	{
 		$max_len = ini_get('log_errors_max_len');
 
 		if ( strlen($message) > $max_len ) {
 			$long_key = kUtil::generateId();
 			self::$_longMessages[$long_key] = $message;
 
 			return mb_substr($message, 0, $max_len - strlen($long_key) - 2) . ' #' . $long_key;
 		}
 
 		return $message;
 	}
 
 	/**
 	 * Expands shortened message
 	 *
 	 * @param string $message
 	 * @param bool $clear_cache Allow debugger to expand message after it's been expanded by kLogger
 	 * @return string
 	 * @access public
 	 */
 	public static function expandMessage($message, $clear_cache = true)
 	{
 		if ( preg_match('/(.*)#([\d]+)$/', $message, $regs) ) {
 			$long_key = $regs[2];
 
 			if ( isset(self::$_longMessages[$long_key]) ) {
 				$message = self::$_longMessages[$long_key];
 
 				if ( $clear_cache ) {
 					unset(self::$_longMessages[$long_key]);
 				}
 			}
 		}
 
 		return $message;
 	}
 
 	/**
 	 * Determines if error should be logged based on it's origin.
 	 *
 	 * @param string $file File.
 	 *
 	 * @return boolean
 	 */
 	public static function isErrorOriginAllowed($file)
 	{
 		static $error_origin_regexp;
 
 		// Lazy detect error origins, because they're not available at construction time.
 		if ( !$error_origin_regexp ) {
 			$error_origins = array();
 			$application = kApplication::Instance();
 
 			foreach ( $application->ModuleInfo as $module_info ) {
 				$error_origins[] = preg_quote(rtrim($module_info['Path'], '/'), '/');
 			}
 
 			$error_origins = array_unique($error_origins);
 			$error_origin_regexp = '/^' . preg_quote(FULL_PATH, '/') . '\/(' . implode('|', $error_origins) . ')\//';
 		}
 
 		// Allow dynamically generated code.
 		if ( strpos($file, 'eval()\'d code') !== false ) {
 			return true;
 		}
 
 		// Allow known modules.
 		if ( preg_match('/^' . preg_quote(MODULES_PATH, '/') . '\//', $file) ) {
 			return preg_match($error_origin_regexp, $file) == 1;
 		}
 
 		// Don't allow Vendors.
 		if ( preg_match('/^' . preg_quote(FULL_PATH, '/') . '\/vendor\//', $file) ) {
 			return false;
 		}
 
 		// Allow everything else within main folder.
 		return preg_match('/^' . preg_quote(FULL_PATH, '/') . '\//', $file) == 1;
 	}
 
 	/**
 	 * Parses database error message into error number, error message and sql that caused that error
 	 *
 	 * @static
 	 * @param string $message
 	 * @return Array
 	 * @access public
 	 */
 	public static function parseDatabaseError($message)
 	{
 		$regexp = '/' . preg_quote(self::DB_ERROR_PREFIX) . ' #(.*?) - (.*?)\. SQL: (.*?)$/s';
 
 		if ( preg_match($regexp, $message, $regs) ) {
 			// errno, errstr, sql
 			return Array ($regs[1], $regs[2], $regs[3]);
 		}
 
 		return Array (0, $message, '');
 	}
 }
 
 
 /**
  * Base class for error or exception handling
  */
 abstract class kHandlerStack extends kBase {
 
 	/**
 	 * List of added handlers
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected $_handlers = Array ();
 
 	/**
 	 * Reference to event log, which created this object
 	 *
 	 * @var kLogger
 	 * @access protected
 	 */
 	protected $_logger;
 
 	/**
 	 * Remembers if handler is activated
 	 *
 	 * @var bool
 	 * @access protected
 	 */
 	protected $_enabled = false;
 
 	public function __construct(kLogger $logger)
 	{
 		parent::__construct();
 
 		$this->_logger = $logger;
 
 		if ( !kUtil::constOn('DBG_ZEND_PRESENT') ) {
 			$this->attach();
 			$this->_enabled = true;
 		}
 	}
 
 	/**
 	 * Detaches from error handling routines on class destruction
 	 *
 	 * @return void
 	 * @access public
 	 */
 	public function __destruct()
 	{
 		if ( !$this->_enabled ) {
 			return;
 		}
 
 		$this->detach();
 		$this->_enabled = false;
 	}
 
 	/**
 	 * Attach to error handling routines
 	 *
 	 * @abstract
 	 * @return void
 	 * @access protected
 	 */
 	abstract protected function attach();
 
 	/**
 	 * Detach from error handling routines
 	 *
 	 * @abstract
 	 * @return void
 	 * @access protected
 	 */
 	abstract protected function detach();
 
 	/**
 	 * Adds new handler to the stack
 	 *
 	 * @param callable $handler
 	 * @return void
 	 * @access public
 	 */
 	public function add($handler)
 	{
 		$this->_handlers[] = $handler;
 	}
 
 	/**
 	 * Returns `true`, when no other error handlers should process this error.
 	 *
 	 * @param integer $errno Error code.
 	 *
 	 * @return boolean
 	 */
 	protected function _handleFatalError($errno)
 	{
 		$debug_mode = defined('DEBUG_MODE') && DEBUG_MODE;
 		$skip_reporting = defined('DBG_SKIP_REPORTING') && DBG_SKIP_REPORTING;
 
 		if ( !$this->_handlers || ($debug_mode && $skip_reporting) ) {
 			// when debugger absent OR it's present, but we actually can't see it's error report (e.g. during ajax request)
 
 			if ( $this->_isFatalError($errno) ) {
 				$this->_displayFatalError($errno);
 			}
 
 			if ( !$this->_handlers ) {
 				return true;
 			}
 		}
 
 		return false;
 	}
 
 	/**
 	 * Determines if given error is a fatal
 	 *
 	 * @abstract
 	 * @param Exception|int $errno
 	 * @return bool
 	 */
 	abstract protected function _isFatalError($errno);
 
 	/**
 	 * Displays div with given error message
 	 *
 	 * @param string $errno
 	 * @return void
 	 * @access protected
 	 */
 	protected function _displayFatalError($errno)
 	{
 		$errno = $this->_getFatalErrorTitle($errno);
 
 		$margin = $this->Application->isAdmin ? '8px' : 'auto';
 		$error_msg = $this->_logger->toString($errno, PHP_SAPI === 'cli');
 
 		if ( PHP_SAPI === 'cli' ) {
 			echo $error_msg;
 			exit(1);
 		}
 
 		echo '<div style="background-color: #FEFFBF; margin: ' . $margin . '; padding: 10px; border: 2px solid red; text-align: center">' . $error_msg . '</div>';
 		exit;
 	}
 
 	/**
 	 * Returns title to show for a fatal
 	 *
 	 * @abstract
 	 * @param Exception|int $errno
 	 * @return string
 	 */
 	abstract protected function _getFatalErrorTitle($errno);
 }
 
 
 /**
  * Class, that handles errors
  */
 class kErrorHandlerStack extends kHandlerStack {
 
 	/**
 	 * Attach to error handling routines
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function attach()
 	{
 		// set as error handler
 		$error_handler = set_error_handler(Array ($this, 'handle'));
 
 		if ( $error_handler ) {
 			// wrap around previous error handler, if any was set
 			$this->_handlers[] = $error_handler;
 		}
 	}
 
 	/**
 	 * Detach from error handling routines
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function detach()
 	{
 		restore_error_handler();
 	}
 
 	/**
 	 * Determines if given error is a fatal
 	 *
 	 * @param int $errno
 	 * @return bool
 	 * @access protected
 	 */
 	protected function _isFatalError($errno)
 	{
 		$fatal_errors = Array (E_USER_ERROR, E_RECOVERABLE_ERROR, E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE);
 
 		return in_array($errno, $fatal_errors);
 	}
 
 	/**
 	 * Returns title to show for a fatal
 	 *
 	 * @param int $errno
 	 * @return string
 	 * @access protected
 	 */
 	protected function _getFatalErrorTitle($errno)
 	{
 		return 'Fatal Error';
 	}
 
 	/**
 	 * Default error handler
 	 *
 	 * @param int $errno
 	 * @param string $errstr
 	 * @param string $errfile
 	 * @param int $errline
 	 * @param Array $errcontext
 	 * @return bool
 	 * @access public
 	 */
 	public function handle($errno, $errstr, $errfile = null, $errline = null, $errcontext = Array ())
 	{
 		$log = $this->_logger->prepare()->addError($errno, $errstr, $errfile, $errline);
 
 		if ( $this->_handleFatalError($errno) ) {
 			$log->write(kLogger::LS_AUTOMATIC, !$this->_isFatalError($errno));
 
 			return true;
 		}
 
 		$log->write(kLogger::LS_AUTOMATIC, !$this->_isFatalError($errno));
 
 		$res = false;
 
 		foreach ($this->_handlers as $handler) {
 			$res = call_user_func($handler, $errno, $errstr, $errfile, $errline, $errcontext);
 		}
 
 		return $res;
 	}
 }
 
 
 /**
  * Class, that handles exceptions
  */
 class kExceptionHandlerStack extends kHandlerStack {
 
 	/**
 	 * Attach to error handling routines
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function attach()
 	{
 		// set as exception handler
 		$exception_handler = set_exception_handler(Array ($this, 'handle'));
 
 		if ( $exception_handler ) {
 			// wrap around previous exception handler, if any was set
 			$this->_handlers[] = $exception_handler;
 		}
 	}
 
 	/**
 	 * Detach from error handling routines
 	 *
 	 * @return void
 	 * @access protected
 	 */
 	protected function detach()
 	{
 		restore_exception_handler();
 	}
 
 	/**
 	 * Determines if given error is a fatal
 	 *
 	 * @param Exception $errno
 	 * @return bool
 	 */
 	protected function _isFatalError($errno)
 	{
 		return true;
 	}
 
 	/**
 	 * Returns title to show for a fatal
 	 *
 	 * @param Exception $errno
 	 * @return string
 	 */
 	protected function _getFatalErrorTitle($errno)
 	{
 		return get_class($errno);
 	}
 
 	/**
 	 * Handles exception
 	 *
 	 * @param Exception $exception
 	 * @return bool
 	 * @access public
 	 */
 	public function handle($exception)
 	{
 		$log = $this->_logger->prepare()->addException($exception);
 
 		if ( $exception instanceof kRedirectException ) {
 			/** @var kRedirectException $exception */
 
 			$exception->run();
 		}
 
 		if ( $this->_handleFatalError($exception) ) {
 			$log->write();
 
 			return true;
 		}
 
 		$log->write();
 
 		$res = false;
 
 		foreach ($this->_handlers as $handler) {
 			$res = call_user_func($handler, $exception);
 		}
 
 		return $res;
 	}
 }