' . PHP_EOL;
echo generate_box_close();
}
}
/**
* Observium's SQL debugging. Chooses nice output depending upon web or cli.
* Use format param:
* 'compress' (default) print compressed and highlight query;
* 'full', 'html' print fully formatted query (multiline);
* 'log' for a return compressed query.
*
* @param string $query
* @param string $format
*/
function print_sql($query, $format = 'compress')
{
switch ($format) {
case 'full':
case 'format':
case 'html':
// Fully formatted
$output = (new Doctrine\SqlFormatter\SqlFormatter()) -> format($query);
break;
case 'log':
// Only compress and return for log
return (new Doctrine\SqlFormatter\SqlFormatter()) -> compress($query);
default:
// Only compress and highlight in single line (default)
$compressed = (new Doctrine\SqlFormatter\SqlFormatter()) -> compress($query);
$output = (new Doctrine\SqlFormatter\SqlFormatter()) -> highlight($compressed);
}
if (!is_cli()) {
$output = '
' . $output . '
';
} else {
$output = rtrim($output);
}
echo $output;
}
// DOCME needs phpdoc block
// Observium's variable debugging. Chooses nice output depending upon web or cli
// TESTME needs unit testing
function print_vars($vars, $trace = NULL)
{
if (PHP_SAPI === 'cli' || (isset($GLOBALS['cli']) && $GLOBALS['cli'])) {
// In cli, still prefer ref
if (class_exists('ref', FALSE)) {
ref ::config('shortcutFunc', ['print_vars', 'print_debug_vars']);
ref ::config('showUrls', FALSE);
if (OBS_DEBUG > 0) {
if (is_null($trace)) {
$backtrace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
} else {
$backtrace = $trace;
}
ref ::config('Backtrace', $backtrace); // pass original backtrace
ref ::config('showStringMatches', FALSE);
} else {
ref ::config('showBacktrace', FALSE);
ref ::config('showResourceInfo', FALSE);
ref ::config('showStringMatches', FALSE);
ref ::config('showMethods', FALSE);
}
rt($vars);
} else {
print_r($vars);
echo PHP_EOL;
}
} elseif (class_exists('ref')) {
// in Web use old ref class, when Tracy not possible to use
ref ::config('shortcutFunc', ['print_vars', 'print_debug_vars']);
ref ::config('showUrls', FALSE);
if (OBS_DEBUG > 0) {
if (is_null($trace)) {
$backtrace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
} else {
$backtrace = $trace;
}
ref ::config('Backtrace', $backtrace); // pass original backtrace
} else {
ref ::config('showBacktrace', FALSE);
ref ::config('showResourceInfo', FALSE);
ref ::config('showStringMatches', FALSE);
ref ::config('showMethods', FALSE);
}
//ref::config('stylePath', $GLOBALS['config']['html_dir'] . '/css/ref.css');
//ref::config('scriptPath', $GLOBALS['config']['html_dir'] . '/js/ref.js');
r($vars);
} else {
// Just fallback to php var dump
echo '
';
print_r($vars);
echo '
';
}
}
/**
* Call to print_vars in debug mode only
* By default var displayed only for debug level 2
*
* @param mixed $vars Variable to print
* @param integer $debug_level Minimum debug level, default 2
*/
function print_debug_vars($vars, $debug_level = 2)
{
// For level 2 display always (also empty), for level 1 only non empty vars
if (defined('OBS_DEBUG') && OBS_DEBUG &&
OBS_DEBUG >= $debug_level && (OBS_DEBUG > 1 || !safe_empty($vars))) {
$trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
print_vars($vars, $trace);
} elseif (defined('OBS_CACHE_DEBUG') && OBS_CACHE_DEBUG) {
$trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
print_vars($vars, $trace);
}
}
/**
* Convert SNMP timeticks string into seconds
*
* SNMP timeticks can be in two different normal formats:
* - "(2105)" == 21.05 sec
* - "0:0:00:21.05" == 21.05 sec
* Sometime devices return wrong type or numeric instead timetick:
* - "Wrong Type (should be Timeticks): 1632295600" == 16322956 sec
* - "1546241903" == 15462419.03 sec
* Parse the timeticks string and convert it to seconds.
*
* @param string $timetick
* @param bool $float - Return a float with microseconds
*
* @return int|float
*/
function timeticks_to_sec($timetick, $float = FALSE)
{
if (str_contains($timetick, 'Wrong Type')) {
// Wrong Type (should be Timeticks): 1632295600
[, $timetick] = explode(': ', $timetick, 2);
}
$timetick = trim($timetick, " \t\n\r\0\x0B\"()"); // Clean string
if (is_numeric($timetick)) {
// When "Wrong Type" or timetick as an integer, than time with count of ten millisecond ticks
$time = $timetick / 100;
return ($float ? (float)$time : (int)$time);
}
if (!preg_match('/^[\d\.: ]+$/', $timetick)) {
return FALSE;
}
$timetick_array = explode(':', $timetick);
if (count($timetick_array) == 1 && is_numeric($timetick)) {
$secs = $timetick;
$microsecs = 0;
} else {
//list($days, $hours, $mins, $secs) = $timetick_array;
$secs = array_pop($timetick_array);
$mins = array_pop($timetick_array);
$hours = array_pop($timetick_array);
$days = array_pop($timetick_array);
[$secs, $microsecs] = explode('.', $secs);
$hours += $days * 24;
$mins += $hours * 60;
$secs += $mins * 60;
// Sometime used non standard years counter
if (count($timetick_array)) {
$years = array_pop($timetick_array);
$secs += $years * 31536000; // 365 * 24 * 60 * 60;
}
//print_vars($timetick_array);
}
$time = ($float ? (float)$secs + $microsecs / 100 : (int)$secs);
print_debug("Timeticks converted $timetick -> $time");
return $time;
}
/**
* Convert SNMP DateAndTime string into unixtime
*
* field octets contents range
* ----- ------ -------- -----
* 1 1-2 year 0..65536
* 2 3 month 1..12
* 3 4 day 1..31
* 4 5 hour 0..23
* 5 6 minutes 0..59
* 6 7 seconds 0..60
* (use 60 for leap-second)
* 7 8 deci-seconds 0..9
* 8 9 direction from UTC '+' / '-'
* 9 10 hours from UTC 0..11
* 10 11 minutes from UTC 0..59
*
* For example, Tuesday May 26, 1992 at 1:30:15 PM EDT would be displayed as:
* 1992-5-26,13:30:15.0,-4:0
*
* Note that if only local time is known, then timezone information (fields 8-10) is not present.
*
* @param string $datetime DateAndTime string
* @param boolean $use_gmt Return unixtime converted to GMT or Local (by default)
*
* @return integer Unixtime
*/
function datetime_to_unixtime($datetime, $use_gmt = FALSE)
{
$timezone = get_timezone();
$datetime = trim($datetime);
if (preg_match('/(?\d+)-(?\d+)-(?\d+)(?:,(?\d+):(?\d+):(?\d+)(?\.\d+)?(?:,(?[+\-]?)(?\d+):(?\d+))?)/', $datetime, $matches)) {
if (isset($matches['tzh'])) {
// Use TZ offset from datetime string
$offset = $matches['tzs'] . ($matches['tzh'] * 3600 + $matches['tzm'] * 60); // Offset from GMT in seconds
} else {
// Or use system offset
$offset = $timezone['php_offset'];
}
$time_tmp = mktime($matches['hour'], $matches['min'], $matches['sec'], $matches['mon'], $matches['day'], $matches['year']); // Generate unixtime
$time_gmt = $time_tmp + ($offset * -1); // Unixtime from string in GMT
$time_local = $time_gmt + $timezone['php_offset']; // Unixtime from string in local timezone
} else {
$time_local = time(); // Current unixtime with local timezone
$time_gmt = $time_local - $timezone['php_offset']; // Current unixtime in GMT
}
if (OBS_DEBUG > 1) {
$debug_msg = 'UNIXTIME from DATETIME "' . ($time_tmp ? $datetime : 'time()') . '": ';
$debug_msg .= 'LOCAL (' . format_unixtime($time_local) . '), GMT (' . format_unixtime($time_gmt) . '), TZ (' . $timezone['php'] . ')';
print_message($debug_msg);
}
if ($use_gmt) {
return ($time_gmt);
}
return ($time_local);
}
/**
* Format seconds to requested time format.
*
* Default format is "long".
*
* Supported formats:
* long => '1 year, 1 day, 1h 1m 1s'
* longest => '1 year, 1 day, 1 hour 1 minute 1 second'
* short-3 => '1y 1d 1h'
* short-2 => '1y 1d'
* shorter => *same as short-2 above
* (else) => '1y 1d 1h 1m 1s'
*
* @param int|string $uptime Time is seconds
* @param string $format Optional format
*
* @return string
*/
function format_uptime($uptime, $format = "long")
{
if (!is_numeric($uptime)) {
print_debug("Passed incorrect value to " . __FUNCTION__ . "()");
print_debug_vars($uptime);
//return FALSE;
return '0s'; // incorrect, but for keep compatibility
}
$uptime = (int)round($uptime);
if ($uptime <= 0) {
return '0s';
}
$up['y'] = floor($uptime / 31536000);
$up['d'] = floor($uptime % 31536000 / 86400);
$up['h'] = floor($uptime % 86400 / 3600);
$up['m'] = floor($uptime % 3600 / 60);
$up['s'] = floor($uptime % 60);
$result = '';
if (str_starts($format, 'long')) {
if ($up['y'] > 0) {
$result .= $up['y'] . ' year' . ($up['y'] != 1 ? 's' : '');
if ($up['d'] > 0 || $up['h'] > 0 || $up['m'] > 0 || $up['s'] > 0) {
$result .= ', ';
}
}
if ($up['d'] > 0) {
$result .= $up['d'] . ' day' . ($up['d'] != 1 ? 's' : '');
if ($up['h'] > 0 || $up['m'] > 0 || $up['s'] > 0) {
$result .= ', ';
}
}
if ($format === 'longest') {
if ($up['h'] > 0) {
$result .= $up['h'] . ' hour' . ($up['h'] != 1 ? 's ' : ' ');
}
if ($up['m'] > 0) {
$result .= $up['m'] . ' minute' . ($up['m'] != 1 ? 's ' : ' ');
}
if ($up['s'] > 0) {
$result .= $up['s'] . ' second' . ($up['s'] != 1 ? 's ' : ' ');
}
} else {
if ($up['h'] > 0) {
$result .= $up['h'] . 'h ';
}
if ($up['m'] > 0) {
$result .= $up['m'] . 'm ';
}
if ($up['s'] > 0) {
$result .= $up['s'] . 's ';
}
}
} else {
$count = $format === 'shorter' ? 2 : 6;
if (str_starts($format, 'short-')) {
// short-2 => 2, short-3 => 3 and up to 6
[, $tmp] = explode('-', $format, 2);
if (is_numeric($count) && $count >= 1 && $count <= 6) {
$count = (int)$tmp;
}
}
foreach ($up as $period => $value) {
if ($value == 0) {
continue;
}
$result .= $value . $period . ' ';
$count--;
if ($count == 0) {
break;
}
}
}
return trim($result);
}
/**
* Get current timezones for mysql and php.
* Use this function when need display timedate from mysql
* for fix diffs between this timezones
*
* Example:
* Array
* (
* [mysql] => +03:00
* [php] => +03:00
* [php_abbr] => MSK
* [php_offset] => +10800
* [mysql_offset] => +10800
* [diff] => 0
* )
*
* @param bool $refresh Refresh timezones
*
* @return array Timezones info
*/
function get_timezone($refresh = FALSE)
{
global $cache;
if ($refresh || !isset($cache['timezone'])) {
$timezone = [];
if ($refresh) {
// Call to external exec only when refresh (basically it's not required)
$timezone['system'] = external_exec('date "+%:z"'); // return '+03:00'
}
if (!OBS_DB_SKIP) {
$timezone['mysql'] = dbFetchCell('SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP);'); // return '03:00:00'
if ($timezone['mysql'][0] !== '-') {
$timezone['mysql'] = '+' . $timezone['mysql'];
}
$timezone['mysql'] = preg_replace('/:00$/', '', $timezone['mysql']); // convert to '+03:00'
}
[$timezone['php'], $timezone['php_abbr'], $timezone['php_name'], $timezone['php_daylight']] = explode('|', date('P|T|e|I'));
//$timezone['php'] = date('P'); // return '+03:00'
//$timezone['php_abbr'] = date('T'); // return 'MSK'
//$timezone['php_name'] = date('e'); // return 'Europe/Moscow'
//$timezone['php_daylight'] = date('I'); // return '0'
foreach (['php', 'mysql'] as $entry) {
if (!isset($timezone[$entry])) {
continue;
} // skip mysql if OBS_DB_SKIP
$sign = $timezone[$entry][0];
[$hours, $minutes] = explode(':', $timezone[$entry]);
$timezone[$entry . '_offset'] = $sign . (abs($hours) * 3600 + $minutes * 60); // Offset from GMT in seconds
}
if (OBS_DB_SKIP) {
// If mysql skipped, just return system/php timezones without caching
return $timezone;
}
// Get the difference in sec between mysql and php timezones
$timezone['diff'] = (int)$timezone['mysql_offset'] - (int)$timezone['php_offset'];
$cache['timezone'] = $timezone;
}
return $cache['timezone'];
}
/**
* Convert common MAC strings to fixed 12 char string
*
* @param string $mac MAC string (ie: 66:c:9b:1b:62:7e, 00 02 99 09 E9 84)
*
* @return string Cleaned MAC string (ie: 00029909e984)
*/
function mac_zeropad($mac)
{
$mac = strtolower(trim($mac));
if (str_contains($mac, ':')) {
// STRING: 66:c:9b:1b:62:7e
$mac_parts = explode(':', $mac);
if (count($mac_parts) === 6) {
$mac = '';
foreach ($mac_parts as $part) {
$mac .= zeropad($part);
}
}
} else {
// Hex-STRING: 00 02 99 09 E9 84
// Cisco MAC: 1234.5678.9abc
// Other Vendors: 00-0B-DC-00-68-AF
// Some other: 0x123456789ABC
$mac = str_replace([' ', '.', '-', '0x'], '', $mac);
}
if (strlen($mac) === 12 && ctype_xdigit($mac)) {
return $mac;
}
// Strip out non-hex digits (Not sure when this required, copied for compat with old format_mac())
$mac = preg_replace('/[[:^xdigit:]]/', '', $mac);
return (strlen($mac) === 12) ? $mac : NULL;
}
/**
* Formats a MAC address string with the specified delimiter.
*
* @param string $mac MAC address string in any known format.
* @param string $split_char Allowed delimiters for specific formats: ':', '', ' ', '0x'.
*
* @return string The formatted MAC address string.
*/
function format_mac($mac, $split_char = ':')
{
// Clean MAC string
$mac = mac_zeropad($mac);
// Add colons
$mac = preg_replace('/([[:xdigit:]]{2})(?!$)/', '$1:', $mac);
// Convert fake MACs to IP
//if (preg_match('/ff:fe:([[:xdigit:]]+):([[:xdigit:]]+):([[:xdigit:]]+):([[:xdigit:]]{1,2})/', $mac, $matches))
if (preg_match('/ff:fe:([[:xdigit:]]{2}):([[:xdigit:]]{2}):([[:xdigit:]]{2}):([[:xdigit:]]{2})/', $mac, $matches)) {
if ($matches[1] == '00' && $matches[2] == '00') {
$mac = hexdec($matches[3]) . '.' . hexdec($matches[4]) . '.X.X'; // Cisco, why you convert 192.88.99.1 to 0:0:c0:58 (should be c0:58:63:1)
} else {
$mac = hexdec($matches[1]) . '.' . hexdec($matches[2]) . '.' . hexdec($matches[3]) . '.' . hexdec($matches[4]);
}
}
if ($split_char === '') {
$mac = str_replace(':', $split_char, $mac);
} elseif ($split_char === ' ') {
$mac = strtoupper(str_replace(':', $split_char, $mac));
} elseif ($split_char === '.') {
// Cisco like format
$parts = explode(':', $mac, 6);
$mac = $parts[0] . $parts[1] . '.' . $parts[2] . $parts[3] . '.' . $parts[4] . $parts[5];
} elseif ($split_char === '0x') {
$mac = '0x' . strtoupper(str_replace(':', '', $mac));
}
return $mac;
}
/**
* Checks if the required exec functions are available.
*
* @return bool TRUE if proc_open and proc_get_status are available and not disabled, FALSE otherwise.
*/
function is_exec_available()
{
// Check if ini_get is not disabled
if (!function_exists('ini_get')) {
print_warning('WARNING: Function ini_get() is disabled via the `disable_functions` option in php.ini configuration file. Please clean this function from this list.');
return TRUE; // NOTE, this is not a critical function for functionally
}
$required_functions = ['proc_open', 'proc_get_status'];
$disabled_functions = explode(',', ini_get('disable_functions'));
foreach ($required_functions as $func) {
if (in_array($func, $disabled_functions)) {
print_error('ERROR: Function ' . $func . '() is disabled via the `disable_functions` option in php.ini configuration file. Please clean this function from this list.');
return FALSE;
}
}
return TRUE;
}
/**
* Opens pipes for a command, sets non-blocking mode for stdout and stderr, and returns the process resource.
*
* @param string $command The command to be executed.
* @param array $pipes An array that will be filled with pipe resources.
* @param string|null $cwd Optional. The working directory for the command. Defaults to NULL, which uses the current working directory.
* @param array $env Optional. An array of environment variables for the command. Defaults to an empty array.
*
* @return resource|false The process resource on success, or false on failure.
*/
function pipe_open($command, &$pipes, $cwd = NULL, $env = [])
{
$descriptorspec = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
if (is_resource($process)) {
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
}
return $process;
}
/**
* Reads the output of a command executed through pipes.
*
* @param string $command The command to be executed.
* @param array $pipes An array of pipe resources.
* @param bool $fullread Optional. Determines if the entire output should be read. Default is true.
*
* @return string The output of the command with the last end-of-line character removed.
*/
function pipe_read($command, &$pipes, $fullread = TRUE)
{
// $pipes like this:
// 0 => writeable handle connected to child stdin
// 1 => readable handle connected to child stdout
// Any error output will be appended to /tmp/error-output.txt
fwrite($pipes[0], $command);
fclose($pipes[0]);
if ($fullread) {
// Read while not end of file
$stdout = '';
while (!feof($pipes[1])) {
$stdout .= fgets($pipes[1]);
}
} else {
// Output not matter, for rrdtool
$iter = 0;
$line = '';
$stdout = '';
while (strlen($line) < 1 && $iter < 1000) {
// wait for 10 milliseconds to loosen loop (max 1s)
usleep(1000);
$line = fgets($pipes[1], 1024);
$stdout .= $line;
$iter++;
}
}
return preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
}
/**
* Closes the pipes and the process.
*
* @param resource $process The process resource.
* @param array $pipes An array of pipe resources.
*
* @return int The termination status of the process that was run.
*/
function pipe_close($process, &$pipes)
{
// Close each pipe resource if it's valid
foreach ($pipes as $key => $pipe) {
if (is_resource($pipe)) {
fclose($pipe);
}
}
// Close pipes before proc_close() to avoid deadlock
return proc_close($process);
}
/**
* Execute an external command and return its stdout, with execution details stored in a reference parameter.
*
* @param string $command The command to execute.
* @param array &$exec_status Reference parameter to store execution details (stdout, stderr, exitcode, runtime).
* @param int $timeout The timeout for the command in seconds. Set to null for no timeout.
* @param bool $debug If true, the debugging function will be called to print execution details.
*
* @return string The stdout from the executed command.
*/
function external_exec($command, &$exec_status = [], $timeout = NULL, $debug = FALSE)
{
$command = trim($command);
$debug = $debug || (defined('OBS_DEBUG') && OBS_DEBUG);
if ($debug > 0) {
external_exec_debug_cmd($command);
}
if ($command === '') {
$exec_status = [
'stdout' => '',
'stderr' => 'Empty command passed',
'exitcode' => -1,
'runtime' => 0
];
return '';
}
$descriptorspec = [
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = proc_open('exec ' . $command, $descriptorspec, $pipes);
if (!is_resource($process)) {
$exec_status = [
'stdout' => FALSE,
'stderr' => '',
'exitcode' => -1,
'runtime' => 0
];
return FALSE;
}
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
$stdout = $stderr = '';
$exec_status = [];
$start = microtime(TRUE);
$runtime = 0;
while (TRUE) {
$read = [];
if (!feof($pipes[1])) {
$read[] = $pipes[1];
}
if (!feof($pipes[2])) {
$read[] = $pipes[2];
}
if (empty($read)) {
break;
}
$write = NULL;
$except = NULL;
if (stream_select($read, $write, $except, $timeout) === FALSE) {
break; // Handle stream_select() failure
}
foreach ($read as $pipe) {
if ($pipe === $pipes[1]) {
$stdout .= fread($pipe, 8192);
} elseif ($pipe === $pipes[2]) {
$stderr .= fread($pipe, 8192);
}
}
$runtime = microtime(TRUE) - $start;
$status = proc_get_status($process);
// Break from this loop if the process exited before timeout
if (!$status['running']) {
// Check for the rare situation of a wrong process status due to a proc_get_status() bug
// See: https://bugs.php.net/bug.php?id=69014
if (feof($pipes[1]) === FALSE) {
// Store the status in $status_fix to use later if needed
if (!isset($status_fix)) {
$status_fix = $status;
}
if ($debug > 1) {
print_error("Possible proc_get_status() bug encountered. Process is still running, but the status indicates otherwise.");
}
} else {
// Process exited normally, so we can break the loop
break;
}
}
if ($timeout !== NULL) {
$timeout -= $runtime;
if ($timeout < 0) {
proc_terminate($process, 9);
$status['running'] = FALSE;
$status['exitcode'] = -1;
break;
}
}
}
$exec_status['endtime'] = microtime(TRUE);
// FIXME -- Check if necessary in PHP7+
if ($status['running']) {
// Fix sometimes wrong status by adding a delay to wait for the process to finish
$delay = 0;
$delay_step = 5000; // Step increment of 5 milliseconds
$delay_max = 150000; // Maximum delay of 150 milliseconds
// Keep waiting in increments of delay_step while the process is running
// and the total delay is less than the maximum allowed delay (delay_max)
while ($status['running'] && $delay < $delay_max) {
usleep($delay_step); //
$status = proc_get_status($process);
$delay += $delay_step;
}
$exec_status['exitdelay'] = (int)($delay / 1000);
} elseif (isset($status_fix)) {
// If $status_fix is set, use it to correct the wrong process status
$status = $status_fix;
}
$stdout = preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
$stderr = rtrim($stderr);
if ($status['running']) {
proc_terminate($process, 9);
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$exec_status['stdout'] = $stdout;
$exec_status['stderr'] = $stderr;
$exec_status['exitcode'] = $status['exitcode'] ?? -1;
$exec_status['runtime'] = $runtime;
if ($debug) {
external_exec_debug($exec_status);
}
return $stdout;
}
/**
* Print sanitized debugging information for a command.
*
* This function hides sensitive data (such as SNMP authentication parameters
* and usernames/passwords) from the debug output to ensure security.
*
* @param string $command The command to execute and debug.
*/
function external_exec_debug_cmd($command)
{
$debug_command = ($command === '' && isset($GLOBALS['snmp_command'])) ? $GLOBALS['snmp_command'] : $command;
if (OBS_DEBUG < 2 && $GLOBALS['config']['snmp']['hide_auth'] &&
preg_match("/snmp(bulk)?(get|getnext|walk)(\s+-(t|r|Cr)['\d\s]+){0,3}(\s+-Cc)?\s+-v[123]c?\s+/", $debug_command)) {
// Hide SNMP authentication parameters from debug output
$pattern = "/\s+(?:(\-[uxXaA])\s*(?:'.*?')|(\-c)\s*(?:'.*?(@\S+)?'))/"; // do not hide contexts, only community and v3 auth
$debug_command = preg_replace($pattern, ' \1\2 ***\3', $debug_command);
} elseif (OBS_DEBUG < 2 && preg_match("!\ --(user(?:name)?|password)=!", $debug_command)) {
// Hide any username/password in debug output, e.g. in WMIC
$pattern = "/ --(user(?:name)?|password)=(\S+|\'[^\']*\')/";
$debug_command = preg_replace($pattern, ' --\1=***', $debug_command);
}
print_console(PHP_EOL . 'CMD[%y' . $debug_command . '%n]' . PHP_EOL);
}
/**
* Print detailed debugging information for an executed command.
*
* This function displays the exit code, runtime, exit delay (if applicable),
* and the contents of stdout and stderr from the executed command.
*
* @param array $exec_status An array containing execution details:
* - 'stdout': The standard output of the command.
* - 'stderr': The standard error output of the command.
* - 'exitcode': The exit code returned by the command.
* - 'runtime': The time taken to execute the command.
* - 'exitdelay': The delay before the command exited (optional).
*/
function external_exec_debug($exec_status)
{
print_console("CMD EXITCODE[" . ($exec_status['exitcode'] !== 0 ? '%r' : '%g') . $exec_status['exitcode'] . "%n]");
print_console("CMD RUNTIME[" . ($exec_status['runtime'] > 7 ? '%r' : '%g') . round($exec_status['runtime'], 4) . "s%n]");
if ($exec_status['exitdelay'] > 0) {
print_console("CMD EXITDELAY[%r" . $exec_status['exitdelay'] . "ms%n]");
}
print_message("STDOUT[" . PHP_EOL . $exec_status['stdout'] . PHP_EOL . "]");
if ($exec_status['exitcode'] && $exec_status['stderr']) {
print_message("STDERR[" . PHP_EOL . $exec_status['stderr'] . PHP_EOL . "]");
}
}
/**
* Get information about process by it identifier (pid)
*
* @param int $pid The process identifier.
* @param boolean $stats If true, additionally show cpu/memory stats
*
* @return array|false Array with information about process, If process not found, return FALSE
*/
function get_pid_info($pid, $stats = FALSE)
{
$pid = (int)$pid;
if ($pid < 1) {
print_debug("Incorrect PID passed");
//trigger_error("PID ".$pid." doesn't exists", E_USER_WARNING);
return FALSE;
}
if (!$stats && stripos(PHP_OS, 'Linux') === 0) {
// Do not use call to ps on Linux and extended stat not required
// FIXME. Need something same on BSD and other Unix platforms
if ($pid_stat = lstat("/proc/$pid")) {
$pid_info = ['PID' => (string)$pid];
$ps_stat = explode(" ", file_get_contents("/proc/$pid/stat"));
$pid_info['PPID'] = $ps_stat[3];
$pid_info['UID'] = $pid_stat['uid'] . '';
$pid_info['GID'] = $pid_stat['gid'] . '';
$pid_info['STAT'] = $ps_stat[2];
$pid_info['COMMAND'] = trim(str_replace("\0", " ", file_get_contents("/proc/$pid/cmdline")));
$pid_info['STARTED'] = date("r", $pid_stat['mtime']);
$pid_info['STARTED_UNIX'] = $pid_stat['mtime'];
} else {
$pid_info = FALSE;
}
} else {
// Use ps call, have troubles on high load systems!
if ($stats) {
// Add CPU/Mem stats
$options = 'pid,ppid,uid,gid,pcpu,pmem,vsz,rss,tty,stat,time,lstart,args';
} else {
$options = 'pid,ppid,uid,gid,tty,stat,time,lstart,args';
}
//$timezone = get_timezone(); // Get system timezone info, for correct started time conversion
$ps = external_exec('/bin/ps -ww -o ' . $options . ' -p ' . $pid, $exec_status, 1); // Set timeout 1sec for exec
$ps = explode("\n", rtrim($ps));
if ($exec_status['exitcode'] === 127) {
print_debug("/bin/ps command not found, not possible to get process info.");
return NULL;
}
if ($exec_status['exitcode'] !== 0 || count($ps) < 2) {
print_debug("PID " . $pid . " doesn't exists");
//trigger_error("PID ".$pid." doesn't exists", E_USER_WARNING);
return FALSE;
}
// " PID PPID UID GID %CPU %MEM VSZ RSS TT STAT TIME STARTED COMMAND"
// "14675 10250 1000 1000 0.0 0.2 194640 11240 pts/4 S+ 00:00:00 Mon Mar 21 14:48:08 2016 php ./test_pid.php"
//
// " PID PPID UID GID TT STAT TIME STARTED COMMAND"
// "14675 10250 1000 1000 pts/4 S+ 00:00:00 Mon Mar 21 14:48:08 2016 php ./test_pid.php"
//print_vars($ps);
// Parse output
$keys = preg_split("/\s+/", $ps[0], -1, PREG_SPLIT_NO_EMPTY);
$entries = preg_split("/\s+/", $ps[1], count($keys) - 1, PREG_SPLIT_NO_EMPTY);
$started = preg_split("/\s+/", array_pop($entries), 6, PREG_SPLIT_NO_EMPTY);
$command = array_pop($started);
//$started[] = str_replace(':', '', $timezone['system']); // Add system TZ to started time
$started[] = external_exec('date "+%z"'); // Add system TZ to started time
$started_rfc = array_shift($started) . ','; // Sun
// Reimplode and convert to RFC2822 started date 'Sun, 20 Mar 2016 18:01:53 +0300'
$started_rfc .= ' ' . $started[1]; // 20
$started_rfc .= ' ' . $started[0]; // Mar
$started_rfc .= ' ' . $started[3]; // 2016
$started_rfc .= ' ' . $started[2]; // 18:01:53
$started_rfc .= ' ' . $started[4]; // +0300
//$started_rfc .= implode(' ', $started);
$entries[] = $started_rfc;
$entries[] = $command; // Re-add command
//print_vars($entries);
//print_vars($started);
$pid_info = [];
foreach ($keys as $i => $key) {
$pid_info[$key] = $entries[$i];
}
$pid_info['STARTED_UNIX'] = strtotime($pid_info['STARTED']);
//print_vars($pid_info);
}
return $pid_info;
}
/**
* Add information about process into DB
*
* @param array|int $device Device array
* @param int $pid PID for process. If empty used current PHP process ID
*
* @return int DB id for inserted row
*/
function add_process_info($device, $pid = NULL)
{
global $argv, $config;
$process_name = OBS_SCRIPT_NAME;
$process = OBS_PROCESS_NAME;
// Ability for skip any process checking
// WARNING. USE AT OWN RISK
if (isset($config['check_process'][$process]) && !$config['check_process'][$process]) {
if (OBS_DEBUG) {
print_error("WARNING. Process '$process_name' adding disabled.");
}
return NULL;
}
// Check if device_id passed instead array
if (is_numeric($device)) {
$device = ['device_id' => $device];
}
if (!is_numeric($pid)) {
$pid = getmypid();
}
$pid_info = get_pid_info($pid);
if (is_array($pid_info)) {
if ($process_name === 'poller.php' || $process_name === 'alerter.php') {
// Try detect parent poller wrapper
$parent_info = $pid_info;
do {
$found = FALSE;
$parent_info = get_pid_info($parent_info['PPID']);
if (str_contains($parent_info['COMMAND'], $process_name)) {
$found = TRUE;
} elseif (str_contains($parent_info['COMMAND'], 'poller-wrapper.py')) {
$pid_info['PPID'] = $parent_info['PID'];
}
} while ($found);
}
$update_array = [
'process_pid' => $pid,
'process_name' => $process_name,
'process_ppid' => $pid_info['PPID'],
'process_uid' => $pid_info['UID'],
'process_command' => $pid_info['COMMAND'],
'process_start' => $pid_info['STARTED_UNIX'],
'device_id' => $device['device_id']
];
if ($config['poller_id'] > 0 && is_cli()) {
$update_array['poller_id'] = $config['poller_id'];
}
return dbInsert($update_array, 'observium_processes');
}
print_debug("Process info not added for PID: $pid");
return NULL;
}
/**
* Delete information about process from DB
*
* @param array $device Device array
* @param int $pid PID for process. If empty used current PHP process ID
*
* @return int DB id for inserted row
*/
function del_process_info($device, $pid = NULL)
{
global $argv, $config;
$process_name = basename($argv[0]);
// Ability for skip any process checking
// WARNING. USE AT OWN RISK
$process = str_replace('.php', '', $process_name);
if (isset($config['check_process'][$process]) && !$config['check_process'][$process]) {
if (OBS_DEBUG) {
print_error("WARNING. Process '$process_name' delete disabled.");
}
return NULL;
}
// Check if device_id passed instead array
if (is_numeric($device)) {
$device = ['device_id' => $device];
}
if (!is_numeric($pid)) {
$pid = getmypid();
}
if ($pid) {
$params = [$pid, $process_name, $device['device_id'], $config['poller_id']];
return dbDelete('observium_processes', '`process_pid` = ? AND `process_name` = ? AND `device_id` = ? AND `poller_id` = ?', $params);
}
return NULL;
}
function check_process_run($device, $pid = NULL)
{
global $config, $argv;
$check = FALSE;
$process_name = basename($argv[0]);
// Ability for skip any process checking
// WARNING. USE AT OWN RISK
$process = str_replace('.php', '', $process_name);
if (isset($config['check_process'][$process]) && !$config['check_process'][$process]) {
if (OBS_DEBUG) {
print_error("WARNING. Process '$process_name' checking disabled.");
}
return $check;
}
// Check if device_id passed instead array
if (is_numeric($device)) {
$device = ['device_id' => $device];
}
$query = 'SELECT * FROM `observium_processes` WHERE `process_name` = ? AND `device_id` = ? AND `poller_id` = ?';
$params = [$process_name, $device['device_id'], $config['poller_id']];
if (is_numeric($pid)) {
$query .= ' AND `process_pid` = ?';
$params[] = (int)$pid;
}
foreach (dbFetchRows($query, $params) as $process) {
// We found processes in DB, check if it exist on system
$pid_info = get_pid_info($process['process_pid']);
if (is_array($pid_info) && strpos($pid_info['COMMAND'], $process_name) !== FALSE) {
// Process still running
$check = array_merge($pid_info, $process);
} else {
// Remove stalled DB entries
dbDelete('observium_processes', '`process_id` = ?', [$process['process_id']]);
}
}
return $check;
}
/**
* Determine array is associative?
*
* @param array $array
*
* @return boolean
*/
function is_array_assoc($array)
{
return is_array($array) && !array_is_list($array);
}
/**
* Determine array is sequential list?
*
* @param array $array
*
* @return boolean
*/
function is_array_list($array)
{
return is_array($array) && array_is_list($array);
}
/**
* Checks if the given key or index exists in the array.
* Case-insensitive implementation
*
* @param string|int $key Value to check.
* @param array $array An array with keys to check.
*
* @return bool
*/
function array_key_iexists($key, array $array)
{
return in_array(strtolower($key), array_map('strtolower', array_keys($array)), TRUE);
}
/**
* Get all values from specific key in a multidimensional array
*
* @param $key string
* @param $arr array
*
* @return null|string|array
*/
function array_value_recursive($key, array $arr)
{
$val = [];
array_walk_recursive($arr, function ($v, $k) use ($key, &$val) {
if ($k == $key) {
array_push($val, $v);
}
});
return count($val) > 1 ? $val : array_pop($val);
}
/**
* @param $array
* @param $string
* @param string $delimiter
*
* @return mixed|null
*/
function array_get_nested($array, $string, $delimiter = '->')
{
foreach (explode($delimiter, $string) as $key) {
if (!array_key_exists($key, (array)$array)) {
return NULL;
}
$array = $array[$key];
}
return $array;
}
/**
* Insert a value or key/value pair after a specific key in an array. If key doesn't exist, value is appended
* to the end of the array.
*
* @param array $array
* @param string $key
* @param array|string $new
*
* @return array
*/
function array_push_after(array $array, $key, $new)
{
$keys = array_keys($array);
$index = array_search($key, $keys, TRUE);
$count = count($array);
if ($index === FALSE) {
return array_merge_recursive($array, (array)$new);
}
$pos = $index + 1;
return array_merge_recursive(array_slice($array, 0, $pos, TRUE), (array)$new, array_slice($array, $pos, $count - 1, TRUE));
}
function array_filter_key($array, $keys = [], $condition = TRUE) {
if (!is_array($array) || ($condition === TRUE && empty($keys)) || !array_is_list($keys)) {
return [];
}
if ($condition === FALSE || $condition === '!=' || $condition === '!==') {
return array_filter($array, static function($key) use ($keys) { return !in_array($key, $keys, TRUE); }, ARRAY_FILTER_USE_KEY);
}
return array_filter($array, static function($key) use ($keys) { return in_array($key, $keys, TRUE); }, ARRAY_FILTER_USE_KEY);
}
/**
* Fast string compare function, checks if string contain $needle
* Note: function renamed from str_contains() for not to intersect with php8 function.
*
* @param string $string The string to search in
* @param mixed $needle If needle is not a string, it is converted to an string
* @param mixed $encoding For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
* @param bool $case_insensitivity If case_insensitivity is TRUE, comparison is case insensitive
*
* @return bool Returns TRUE if $string starts with $needle or FALSE otherwise
*/
function str_contains_array($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
{
if (is_array($string)) {
// This function required string to search
return FALSE;
}
// If needle is array, use recursive compare
if (is_array($needle)) {
foreach ($needle as $findme) {
if (str_contains_array($string, (string)$findme, $encoding, $case_insensitivity)) {
$GLOBALS['str_last_needle'] = (string)$findme;
return TRUE;
}
}
$GLOBALS['str_last_needle'] = (string)$findme;
return FALSE;
}
$needle = (string)$needle;
$string = (string)$string;
$GLOBALS['str_last_needle'] = $needle;
$compare = $string === $needle;
if ($needle === '') {
return $compare;
}
if ($case_insensitivity) {
// Case-INsensitive
// NOTE, multibyte compare required mb_* functions and slower than general functions
if ($encoding && check_extension_exists('mbstring') &&
mb_strlen($string, $encoding) !== strlen($string)) {
//$encoding = 'UTF-8';
//return mb_strripos($string, $needle, -mb_strlen($string, $encoding), $encoding) !== FALSE;
return $compare || mb_stripos($string, $needle) !== FALSE;
}
return $compare || stripos($string, $needle) !== FALSE;
}
// Case-sensitive
return $compare || str_contains((string)$string, $needle);
}
function str_icontains_array($string, $needle, $encoding = FALSE)
{
return str_contains_array($string, $needle, $encoding, TRUE);
}
/**
* Fast string compare function, checks if string begin with $needle
*
* @param string $string The string to search in
* @param mixed $needle If needle is not a string, it is converted to an string
* @param mixed $encoding For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
* @param boolean $case_insensitivity If case_insensitivity is TRUE, comparison is case insensitive
*
* @return boolean Returns TRUE if $string starts with $needle or FALSE otherwise
*/
function str_starts($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
{
if (is_array($string)) {
// This function required string to search
return FALSE;
}
// If needle is array, use recursive compare
if (is_array($needle)) {
foreach ($needle as $findme) {
if (str_starts($string, (string)$findme, $encoding, $case_insensitivity)) {
$GLOBALS['str_last_needle'] = (string)$findme;
return TRUE;
}
}
$GLOBALS['str_last_needle'] = (string)$findme;
return FALSE;
}
$needle = (string)$needle;
$string = (string)$string;
$GLOBALS['str_last_needle'] = $needle;
if ($needle === '') {
return $string === $needle;
}
if ($case_insensitivity) {
// Case-INsensitive
// NOTE, multibyte compare required mb_* functions and slower than general functions
if ($encoding &&
check_extension_exists('mbstring') && mb_strlen($string, $encoding) !== strlen($string)) {
//$encoding = 'UTF-8';
return mb_strripos($string, $needle, -mb_strlen($string, $encoding), $encoding) !== FALSE;
}
return $needle !== ''
? strncasecmp($string, $needle, strlen($needle)) === 0
: $string === '';
}
// Case-sensitive
return str_starts_with((string)$string, $needle);
}
function str_istarts($string, $needle, $encoding = FALSE)
{
return str_starts($string, $needle, $encoding, TRUE);
}
/**
* Fast string compare function, checks if string end with $needle
*
* @param string $string The string to search in
* @param mixed $needle If needle is not a string, it is converted to an string
* @param mixed $encoding For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
* @param boolean $case_insensitivity If case_insensitivity is TRUE, comparison is case insensitive
*
* @return boolean Returns TRUE if $string ends with $needle or FALSE otherwise
*/
function str_ends($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
{
if (is_array($string)) {
// This function required string to search
return FALSE;
}
// If needle is array, use recursive compare
if (is_array($needle)) {
foreach ($needle as $findme) {
if (str_ends($string, (string)$findme, $encoding, $case_insensitivity)) {
$GLOBALS['str_last_needle'] = (string)$findme;
return TRUE;
}
}
$GLOBALS['str_last_needle'] = (string)$findme;
return FALSE;
}
$needle = (string)$needle;
$string = (string)$string;
$GLOBALS['str_last_needle'] = $needle;
$compare = $needle !== '';
if ($needle === '') {
return $string === $needle;
}
// NOTE, multibyte compare required mb_* functions and slower than general functions
if ($encoding && $compare &&
check_extension_exists('mbstring') && mb_strlen($string, $encoding) !== strlen($string)) {
//$encoding = 'UTF-8';
$diff = mb_strlen($string, $encoding) - mb_strlen($needle, $encoding);
if ($case_insensitivity) {
return $diff >= 0 && mb_stripos($string, $needle, $diff, $encoding) !== FALSE;
}
return $diff >= 0 && mb_strpos($string, $needle, $diff, $encoding) !== FALSE;
}
// Case sensitive compare
if (!$case_insensitivity) {
return str_ends_with((string)$string, $needle);
}
$nlen = strlen($needle);
return $compare
? substr_compare($string, $needle, -$nlen, $nlen, $case_insensitivity) === 0
: $string === '';
}
function str_iends($string, $needle, $encoding = FALSE)
{
return str_ends($string, $needle, $encoding, TRUE);
}
/**
* Compress long strings to hexified compressed string. Can be uncompressed by str_decompress().
*
* @param string $string
*
* @return string
*/
function str_compress($string)
{
if (!is_string($string)) {
return $string;
}
if ($compressed = gzdeflate($string, 9)) {
$compressed = gzdeflate($compressed, 9);
if (OBS_DEBUG > 1) {
$compressed = safe_base64_encode($compressed);
print_cli("DEBUG: String '$string' [" . strlen($string) . "] compressed to '" . $compressed . "' [" . strlen($compressed) . "].");
return $compressed;
}
return safe_base64_encode($compressed);
}
return $string;
}
/**
* Decompress strings compressed by str_compress().
*
* @param string $compressed
*
* @return string
*/
function str_decompress($compressed)
{
if (!is_string($compressed) || !ctype_print($compressed) || !$bin = safe_base64_decode($compressed)) {
return FALSE;
}
$string = gzinflate(gzinflate($bin));
if (!is_string($string)) {
// Not an compressed string?
return FALSE;
}
if (OBS_DEBUG > 1) {
print_cli("DEBUG: String '$compressed' [" . strlen($compressed) . "] decompressed to '" . $string . "' [" . strlen($string) . "].");
}
return $string;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function is_cli()
{
if (defined('__PHPUNIT_PHAR__') && isset($GLOBALS['cache']['is_cli'])) {
// Allow override is_cli() in PHPUNIT
return $GLOBALS['cache']['is_cli'];
}
if (!defined('OBS_CLI')) {
define('OBS_CLI', PHP_SAPI === 'cli' && empty($_SERVER['REMOTE_ADDR']));
if (defined('OBS_DEBUG') && OBS_DEBUG > 1) {
print_cli("DEBUG: is_cli() == " . (OBS_CLI ? 'TRUE' : 'FALSE') . ", PHP_SAPI: '" . PHP_SAPI . "', REMOTE_ADDR: '" . $_SERVER['REMOTE_ADDR'] . "'");
}
}
return OBS_CLI;
}
function cli_is_piped()
{
if (!defined('OBS_CLI_PIPED')) {
define('OBS_CLI_PIPED', check_extension_exists('posix') && !posix_isatty(STDOUT));
}
return OBS_CLI_PIPED;
}
// Detect if script runned from crontab
// DOCME needs phpdoc block
// TESTME needs unit testing
function is_cron()
{
if (!defined('OBS_CRON')) {
$cron = is_cli() && !isset($_SERVER['TERM']);
// For more accurate check if STDOUT exist (but this requires posix extension)
if ($cron) {
$cron = $cron && cli_is_piped();
}
define('OBS_CRON', $cron);
}
return OBS_CRON;
}
/**
* Detect if current URI is link to graph
*
* @return boolean TRUE if current script is graph
*/
// TESTME needs unit testing
function is_graph()
{
if (!defined('OBS_GRAPH')) {
// defined in html/graph.php
define('OBS_GRAPH', FALSE);
}
return OBS_GRAPH;
}
/**
* Detect if current URI is API
*
* @return boolean TRUE if current script is API
*/
// TESTME needs unit testing
function is_api()
{
if (!defined('OBS_API')) {
// defined in html/graph.php
define('OBS_API', FALSE);
}
return OBS_API;
}
function is_ajax()
{
if (!defined('OBS_AJAX')) {
define('OBS_AJAX', (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') ||
str_starts_with($_SERVER['REQUEST_URI'], '/ajax/'));
}
return OBS_AJAX;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function is_ssl()
{
if (isset($_SERVER['HTTPS'])) {
if ($_SERVER['HTTPS'] === '1' || strtolower($_SERVER['HTTPS']) === 'on') {
return TRUE;
}
} elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
return TRUE;
} elseif (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) === 'on') {
return TRUE;
} elseif (isset($_SERVER['SERVER_PORT']) && ($_SERVER['SERVER_PORT'] == '443')) {
return TRUE;
}
//elseif (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') { return TRUE; }
return FALSE;
}
function is_iframe() {
//bdump($_SERVER['HTTP_SEC_FETCH_DEST']);
// Note, Safari detect as iframe: