Page Menu
Home
In-Portal Phabricator
Search
Configure Global Search
Log In
Files
F1050544
logger.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, Jul 2, 10:34 PM
Size
36 KB
Mime Type
text/x-php
Expires
Fri, Jul 4, 10:34 PM (1 d, 4 h)
Engine
blob
Format
Raw Data
Handle
678776
Attached To
rINP In-Portal
logger.php
View Options
<?php
/**
* @version $Id: logger.php 16803 2024-10-20 18:10:28Z alex $
* @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'
=>
''
,
'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
(
Session
::
PURPOSE_STORAGE
);
$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
;
}
$code_fragment
=
$this
->
getCodeFragment
(
$this
->
_logRecord
[
'LogSourceFilename'
],
$this
->
_logRecord
[
'LogSourceFileLine'
]
);
if
(
$code_fragment
!==
null
)
{
$this
->
_logRecord
[
'LogCodeFragment'
]
=
$code_fragment
;
}
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
,
'_FILES'
=>
$_FILES
);
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'
]);
}
if
(
isset
(
$trace_info
[
'file'
],
$trace_info
[
'line'
])
)
{
$code_fragment
=
$this
->
getCodeFragment
(
$trace_info
[
'file'
],
$trace_info
[
'line'
]);
if
(
$code_fragment
!==
null
)
{
$trace
[
$trace_index
][
'code_fragment'
]
=
$code_fragment
;
}
}
}
$this
->
_logRecord
[
'LogBacktrace'
]
=
serialize
(
$this
->
_removeObjectsFromTrace
(
$trace
));
return
$this
;
}
/**
* Returns a code fragment.
*
* @param string $file Filename.
* @param integer $line Line.
*
* @return string|null
*/
protected
function
getCodeFragment
(
$file
,
$line
)
{
static
$path_filter_regexp
;
// Lazy detect path filter regexp, because it's not available at construction time.
if
(
$path_filter_regexp
===
null
)
{
$application
=&
kApplication
::
Instance
();
$path_filter_regexp
=
$application
->
ConfigValue
(
'SystemLogCodeFragmentPathFilterRegExp'
);
}
if
(
strpos
(
$file
,
'eval()
\'
d code'
)
!==
false
||
(
$path_filter_regexp
&&
!
preg_match
(
$path_filter_regexp
,
$file
))
)
{
return
null
;
}
$from_line
=
max
(
1
,
$line
-
10
);
$to_line
=
$line
+
10
;
// Prefix example: ">>> 5. " or " 5. ".
$prefix_length
=
4
+
strlen
(
$to_line
)
+
2
;
$cmd_parts
=
array
(
'sed'
,
'-n'
,
escapeshellarg
(
$from_line
.
','
.
$to_line
.
'p'
),
escapeshellarg
(
$file
),
);
$command
=
implode
(
' '
,
$cmd_parts
);
$ret
=
array
();
$code_fragment
=
preg_replace
(
'/(
\r\n
|
\n
|
\r
)$/'
,
''
,
shell_exec
(
$command
),
1
);
foreach
(
explode
(
"
\n
"
,
$code_fragment
)
as
$line_offset
=>
$code_fragment_part
)
{
$line_number
=
$from_line
+
$line_offset
;
$line_indicator
=
$line_number
==
$line
?
'>>> '
:
' '
;
$ret
[]
=
str_pad
(
$line_indicator
.
$line_number
.
'.'
,
$prefix_length
)
.
$code_fragment_part
;
}
return
implode
(
"
\n
"
,
$ret
);
}
/**
* 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
(
PHP_SAPI
!==
'cli'
&&
!
$this
->
Application
->
isDebugMode
()
)
{
$date
=
date
(
'Y-m-d H:i:s'
);
$reference
=
$this
->
_logRecord
[
'LogUniqueId'
]
.
'-'
.
time
();
$message
=
<<<HTML
<h1 style="margin-top: 0;">Oops, something went wrong</h1>
This is page is currently not available.
We are working on the problem, and appreciate your patience.<br/>
<br/>
Date: {$date}<br/>
Reference: {$reference}<br/>
HTML;
return
$message
;
}
if
(
!
isset
(
$errno
)
)
{
$errno
=
$this
->
_logRecord
[
'LogCode'
];
}
$errstr
=
$this
->
_logRecord
[
'LogMessage'
];
$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'
])
?
$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: #F5F5F5; margin: '
.
$margin
.
'; padding: 10px; border: 2px solid #0067b8; color: #0067b8;">'
.
$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
;
}
}
Event Timeline
Log In to Comment