'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(); $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')); } /** * 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 ( 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'] = time(); $this->_logRecord['LogDate'] = date('Y-m-d H:i:s'); if ( $storage_medium == self::LS_AUTOMATIC ) { $storage_medium = $this->Conn->connectionOpened() ? self::LS_DATABASE : self::LS_DISK; } if ( $storage_medium == self::LS_DATABASE ) { $result = $this->Conn->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->Conn->connectionOpened() ? self::LS_DATABASE : self::LS_DISK; } if ( $storage_medium == self::LS_DATABASE ) { $sql = 'DELETE FROM ' . TABLE_PREFIX . 'SystemLog WHERE LogUniqueId = ' . $unique_id; $this->Conn->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(); } } // 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']; $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 = '' . $errno . ': ' . "{$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'; $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; } /** * Saves log to file (e.g. when not possible to save into database) * * @param $filename * @return bool * @access protected */ protected function _saveToFile($filename) { $time = 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; } else { echo '