Observium_CE/includes/common.inc.php

5705 lines
180 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Observium
*
* This file is part of Observium.
*
* @package observium
* @subpackage common
* @copyright (C) 2006-2013 Adam Armstrong, (C) 2013-2021 Observium Limited
*
*/
// Common Functions
/// FIXME. There should be functions that use only standard php (and self) functions.
/**
* Autoloader for Classes used in Observium
*
*/
function observium_autoload($class_name) {
//var_dump($class_name);
if (isset($GLOBALS['config']['install_dir'])) {
$base_dir = $GLOBALS['config']['install_dir'] . '/libs/';
} else {
// not know why in phpunit $GLOBALS and $config reset on this stage
$base_dir = dirname(__DIR__) . '/libs/';
}
$class_array = explode('\\', $class_name);
$class_file = str_replace('_', '/', implode('/', $class_array)) . '.php';
//print_vars($class_array);
switch ($class_array[0]) {
case 'cli':
include_once($base_dir . 'cli/cli.php'); // Cli classes required base functions
$class_file = str_replace('/Table/', '/table/', $class_file);
//var_dump($class_file);
break;
case 'Phpfastcache':
case 'phpFastCache':
if (PHP_VERSION_ID >= 70300) {
$class_array[0] = 'Phpfastcache8';
$class_file = str_replace('_', '/', implode('/', $class_array)) . '.php';
}
break;
case 'Flight':
$class_file = array_pop($class_array) . '.php';
$class_file = 'flight/' . $class_file;
break;
case 'Ramsey':
if (PHP_VERSION_ID >= 80000 && $class_array[1] === 'Uuid') {
// PHP 7.2+ (for 8.1 required)
//$class_array[1] = 'Uuid4';
$class_file = str_replace('/Uuid/', '/Uuid4/', $class_file);
//$class_file = str_replace('_', '/', implode('/', $class_array)) . '.php';
}
break;
case 'Defuse':
case 'donatj':
$class_file = str_replace($class_array[0] . '/', '', $class_file);
// Initial base class file
$class_file_base = $base_dir . end($class_array) . '.php';
if (is_file($class_file_base)) {
$base_status = include_once($class_file_base);
if (defined('OBS_DEBUG') && OBS_DEBUG > 1) {
print_message("%WLoad base file for class '$class_name' from '$class_file_base': " . ($base_status ? '%gOK' : '%rFAIL'), 'console');
}
}
break;
case 'PhpUnitsOfMeasure':
include_once($base_dir . 'PhpUnitsOfMeasure/UnitOfMeasureInterface.php');
break;
default:
if (strpos($class_name, 'Parsedown') === 0) {
$class_file = 'parsedown/' . $class_file;
} elseif (is_file($base_dir . 'pear/' . $class_file)) {
// By default try Pear file
$class_file = 'pear/' . $class_file;
} elseif (is_dir($base_dir . 'pear/' . $class_name)) {
// And Pear dir
$class_file = 'pear/' . $class_name . '/' . $class_file;
}
//elseif (!is_cli() && is_file($GLOBALS['config']['html_dir'] . '/includes/' . $class_file))
//{
// // For WUI check class files in html_dir
// $base_dir = $GLOBALS['config']['html_dir'] . '/includes/';
//}
}
$full_path = $base_dir . $class_file;
$status = is_file($full_path);
if ($status) {
$status = include_once($full_path);
}
if (defined('OBS_DEBUG') && OBS_DEBUG > 1) {
print_message("%WLoad class '$class_name' from '$full_path': " . ($status ? '%gOK' : '%rFAIL'), 'console');
}
return $status;
}
// Register autoload function
spl_autoload_register('observium_autoload');
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function del_obs_attrib($attrib_type)
{
if (isset($GLOBALS['cache']['attribs'])) { unset($GLOBALS['cache']['attribs']); } // Reset attribs cache
return dbDelete('observium_attribs', "`attrib_type` = ?", array($attrib_type));
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function set_obs_attrib($attrib_type, $attrib_value)
{
if (isset($GLOBALS['cache']['attribs'])) { unset($GLOBALS['cache']['attribs']); } // Reset attribs cache
//if (dbFetchCell("SELECT COUNT(*) FROM `observium_attribs` WHERE `attrib_type` = ?;", array($attrib_type)))
if (dbExist('observium_attribs', '`attrib_type` = ?', array($attrib_type)))
{
$status = dbUpdate(array('attrib_value' => $attrib_value), 'observium_attribs', "`attrib_type` = ?", array($attrib_type));
} else {
$status = dbInsert(array('attrib_type' => $attrib_type, 'attrib_value' => $attrib_value), 'observium_attribs');
if ($status !== FALSE) { $status = TRUE; } // Note dbInsert return IDs if exist or 0 for not indexed tables
}
return $status;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function get_obs_attribs($type_filter)
{
if (!isset($GLOBALS['cache']['attribs']))
{
$attribs = array();
foreach (dbFetchRows("SELECT * FROM `observium_attribs`") as $entry)
{
$attribs[$entry['attrib_type']] = $entry['attrib_value'];
}
$GLOBALS['cache']['attribs'] = $attribs;
}
if (strlen($type_filter))
{
$attribs = array();
foreach ($GLOBALS['cache']['attribs'] as $type => $value)
{
if (strpos($type, $type_filter) !== FALSE)
{
$attribs[$type] = $value;
}
}
return $attribs; // Return filtered attribs
}
return $GLOBALS['cache']['attribs']; // All cached attribs
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function get_obs_attrib($attrib_type) {
if (isset($GLOBALS['cache']['attribs'][$attrib_type])) {
return $GLOBALS['cache']['attribs'][$attrib_type];
}
if ($row = dbFetchRow("SELECT `attrib_value` FROM `observium_attribs` WHERE `attrib_type` = ?;", [ $attrib_type ])) {
return $row['attrib_value'];
}
return NULL;
}
// FIXME. Function temporary placed here, since cache_* functions currently included in WUI only.
// MOVEME includes/cache.inc.php
/**
* Add clear cache attrib, this will request for clearing cache in next request.
*
* @param string $target Clear cache target: wui or cli (default if wui)
*/
function set_cache_clear($target = 'wui') {
if (OBS_DEBUG || (defined('OBS_CACHE_DEBUG') && OBS_CACHE_DEBUG)) {
print_error('<span class="text-warning">CACHE CLEAR SET.</span> Cache clear set.');
}
if (!$GLOBALS['config']['cache']['enable']) {
// Cache not enabled
return;
}
switch (strtolower($target)) {
case 'cli':
// Add clear CLI cache attrib. Currently not used
set_obs_attrib('cache_cli_clear', get_request_id());
break;
default:
// Add clear WUI cache attrib
set_obs_attrib('cache_wui_clear', get_request_id());
}
}
function set_status_var($var, $value) {
$GLOBALS['cache']['status_vars'][$var] = $value;
return TRUE;
}
function isset_status_var($var) {
return array_key_exists($var, (array)$GLOBALS['cache']['status_vars']);
}
function get_status_var($var) {
return $GLOBALS['cache']['status_vars'][$var];
}
/**
* Generate and store Unique ID for current system. Store in DB at first run.
* IDs is RFC 4122 version 4 (without dashes, varchar(32)), i.e. c39b2386c4e8487fad4a87cd367b279d
*
* @return string Unique system ID
* @throws Exception
*/
function get_unique_id() {
if (!defined('OBS_UNIQUE_ID')) {
$unique_id = get_obs_attrib('unique_id');
if (safe_empty($unique_id)) {
// Generate a version 4 (random) UUID object
$uuid4 = Ramsey\Uuid\Uuid::uuid4();
//$unique_id = $uuid4->toString(); // i.e. c39b2386-c4e8-487f-ad4a-87cd367b279d
$unique_id = $uuid4->getHex(); // i.e. c39b2386c4e8487fad4a87cd367b279d
dbInsert(array('attrib_type' => 'unique_id', 'attrib_value' => $unique_id), 'observium_attribs');
}
define('OBS_UNIQUE_ID', $unique_id);
}
return OBS_UNIQUE_ID;
}
/**
* Generate and store Unique Request ID for current script/page.
* ID unique between 2 different requests or page loads
* IDs is RFC 4122 version 4, i.e. 25769c6c-d34d-4bfe-ba98-e0ee856f3e7a
*
* @return string Unique Request ID
* @throws Exception
*/
function get_request_id() {
if (!defined('OBS_REQUEST_ID')) {
// Generate a version 4 (random) UUID object
$uuid4 = Ramsey\Uuid\Uuid::uuid4();
$request_id = $uuid4->toString(); // i.e. 25769c6c-d34d-4bfe-ba98-e0ee856f3e7a
define('OBS_REQUEST_ID', $request_id);
}
return OBS_REQUEST_ID;
}
/**
* Set new DB Schema version
*
* @param integer $db_rev New DB schema revision
* @param boolean $schema_insert Update (by default) or insert by first install db schema
* @return boolean Status of DB schema update
*/
function set_db_version($db_rev, $schema_insert = FALSE) {
if ($db_rev >= 211) { // Do not remove this check, since before this revision observium_attribs table not exist!
$status = set_obs_attrib('dbSchema', $db_rev);
} else {
if ($schema_insert) {
$status = dbInsert(array('version' => $db_rev), 'dbSchema');
if ($status !== FALSE) { $status = TRUE; } // Note dbInsert return IDs if exist or 0 for not indexed tables
} else {
$status = dbUpdate(array('version' => $db_rev), 'dbSchema');
}
}
if ($status) {
$GLOBALS['cache']['db_version'] = $db_rev; // Cache new db version
}
return $status;
}
/**
* Get current DB Schema version
*
* @return string DB schema version
*/
// TESTME needs unit testing
function get_db_version() {
if (!isset($GLOBALS['cache']['db_version'])) {
$db_rev = @get_obs_attrib('dbSchema');
if (!$db_rev) {
$db_rev = 0;
}
$db_rev = (int)$db_rev;
if ($db_rev > 0) {
$GLOBALS['cache']['db_version'] = $db_rev;
} else {
// Do not cache zero value
return $db_rev;
}
}
return $GLOBALS['cache']['db_version'];
}
/**
* Get current DB Size
*
* @return string DB size in bytes
*/
// TESTME needs unit testing
function get_db_size() {
return dbFetchCell('SELECT SUM(`data_length` + `index_length`) AS `size` FROM `information_schema`.`tables` WHERE `table_schema` = ?;', array($GLOBALS['config']['db_name']));
}
/**
* Get unique local id.
* Need to identify poller system.
*
* @return string
*/
function get_local_id() {
// http://0pointer.de/blog/projects/ids.html
switch (PHP_OS) {
case 'Linux':
// Note. system-uuid is good, but available only for root
if (is_file('/etc/machine-id')) {
// 1d56dd4b3c334a20bff1fc4878b9e1ee
return trim(file_get_contents('/etc/machine-id'));
}
if (is_file('/var/lib/dbus/machine-id')) {
return trim(file_get_contents('/var/lib/dbus/machine-id'));
}
break;
case 'FreeBSD':
// kern.hostuuid: fe38be37-5d64-11eb-b896-6470021048e6
list(, $id) = explode(': ', external_exec('sysctl kern.hostuuid'));
if ($id) {
return str_replace('-', '', trim($id));
}
break;
case 'Darwin':
// $ system_profiler SPHardwareDataType
// Hardware:
//
// Hardware Overview:
//
// Model Name: iMac
// Model Identifier: iMac21,1
// Chip: Apple M1
// Total Number of Cores: 8 (4 performance and 4 efficiency)
// Memory: 16 GB
// System Firmware Version: 7429.41.5
// OS Loader Version: 6723.140.2
// Serial Number (system): XXXX402RXXXX
// Hardware UUID: 360CXXXX-XXXX-XXXX-8C34-D2EA2266XXXX
// Provisioning UDID: 0000XXXX-0009193C36FB001E
// Activation Lock Status: Disabled
foreach (explode("\n", external_exec('system_profiler SPHardwareDataType')) as $line) {
if (str_contains($line, 'UUID:')) {
list(, $id) = explode(': ', $line);
if ($id) {
return str_replace('-', '', strtolower(trim($id)));
}
}
}
break;
}
// Derp way, need to store lock file, available only for current host (not in db!)..
$id_file = $GLOBALS['config']['log_dir'] . '/.machine-id';
if (is_file($id_file)) {
return file_get_contents($id_file);
}
try {
$uuid4 = Ramsey\Uuid\Uuid::uuid4();
} catch(Exception $e) {
return '';
}
$unique_id = $uuid4->getHex(); // i.e. c39b2386c4e8487fad4a87cd367b279d
if (file_put_contents($id_file, $unique_id)) {
// return generated id, only when lock file is writable, for prevent logs spamming
return $unique_id;
}
return '';
}
/**
* Get local hostname
*
* @return string FQDN local hostname
*/
function get_localhost() {
global $cache;
if (!isset($cache['localhost'])) {
$cache['localhost'] = php_uname('n');
if (!str_contains($cache['localhost'], '.')) {
// try use hostname -f for get FQDN hostname
$localhost_t = external_exec('/bin/hostname -f');
if (str_contains($localhost_t, '.')) {
$cache['localhost'] = $localhost_t;
}
}
}
return $cache['localhost'];
}
/**
* Get owner of current process
*
* @return string Username
*/
function get_localuser() {
if ($_SERVER['USER']) {
return $_SERVER['USER'];
}
if (function_exists('posix_geteuid')) {
return posix_getpwuid(posix_geteuid())['name'];
}
return external_exec('whoami');
}
/**
* Get the directory size
*
* @param string $dir
* @return integer Directory size in bytes
*/
function get_dir_size($dir) {
$size = 0;
//$files = [];
foreach (get_recursive_directory_iterator($dir) as $path => $file) {
//$files[] = $path;
$size += $file->getSize();
}
//print_vars($files);
return $size;
}
/**
* Recursively delete dir.
*
* @param string $dir
*
* @return bool
*/
function delete_dir($dir) {
if (!file_exists($dir)) {
print_debug("Dir '$dir' not exist.");
return TRUE;
}
$dirs = [];
$files = [];
// Delete files inside dir
foreach (get_recursive_directory_iterator($dir) as $path => $file) {
$files[] = $path;
if ($dir !== $file->getPath()) {
$dirs[] = $file->getPath();
}
if (!unlink($path)) {
// File not deleted
return FALSE;
}
/*
print_vars($file->getFilename());
echo PHP_EOL;
print_vars($file->getExtension());
echo PHP_EOL;
print_vars($file->getPath());
echo PHP_EOL;
*/
}
if (count($files)) {
print_debug("Deleted files:");
print_debug_vars($files);
}
// Now delete sub-dirs
foreach ($dirs as $d) {
if (!rmdir($d)) {
// Sub dir not deleted
return FALSE;
}
}
$dirs[] = $dir;
print_debug("Deleted dirs:");
print_debug_vars($dirs);
return rmdir($dir);
}
function percent($value, $max, $precision = 0) {
$percent = float_div($value, $max) * 100;
if (is_numeric($precision)) { return round($percent, $precision); }
return $percent;
}
/**
* Percent Class
*
* Given a percentage value return a class name (for CSS).
*
* @param int|string $percent
* @return string
*/
function percent_class($percent)
{
if ($percent < "25")
{
$class = "info";
} elseif ($percent < "50") {
$class = "";
} elseif ($percent < "75") {
$class = "success";
} elseif ($percent < "90") {
$class = "warning";
} else {
$class = "danger";
}
return $class;
}
/**
* Percent Colour
*
* This function returns a colour based on a 0-100 value
* It scales from green to red from 0-100 as default.
*
* @param integer $value
* @param integer $brightness
* @param integer $max
* @param integer $min
* @param string $thirdColourHex
* @return string
*/
function percent_colour($value, $brightness = 128, $max = 100, $min = 0, $thirdColourHex = '00') {
if ($value > $max) { $value = $max; }
if ($value < $min) { $value = $min; }
// Calculate first and second colour (Inverse relationship)
$div = float_div($value, $max);
$first = (1 - $div) * $brightness;
$second = $div * $brightness;
// Find the influence of the middle Colour (yellow if 1st and 2nd are red and green)
$diff = abs($first - $second);
$influence = ($brightness - $diff) / 2;
$first = (int)($first + $influence);
$second = (int)($second + $influence);
// Convert to HEX, format and return
$firstHex = str_pad(dechex($first), 2, 0, STR_PAD_LEFT);
$secondHex = str_pad(dechex($second), 2, 0, STR_PAD_LEFT);
return '#' . $secondHex . $firstHex . $thirdColourHex;
// alternatives:
// return $thirdColourHex . $firstHex . $secondHex;
// return $firstHex . $thirdColourHex . $secondHex;
}
/**
* Convert sequence of numbers in an array to range of numbers.
* Example:
* array(1,2,3,4,5,6,7,8,9,10) -> '1-10'
* array(1,2,3,5,7,9,10,11,12,14) -> '1-3,5,7,9-12,14'
*
* @param array $arr Array with sequence of numbers
* @param string $separator Use this separator for list
* @param bool $sort Sort input array or not
* @return string
*/
function range_to_list($arr, $separator = ',', $sort = TRUE) {
if (!is_array($arr)) { return ''; }
if ($sort) { sort($arr, SORT_NUMERIC); }
$ranges = [];
$count = count($arr);
for ($i = 0; $i < $count; $i++) {
$rstart = $arr[$i];
$rend = $rstart;
while (isset($arr[$i+1]) && ((int)$arr[$i+1] - (int)$arr[$i]) === 1) {
$rend = $arr[$i+1];
$i++;
}
if (is_numeric($rstart) && is_numeric($rend)) {
$ranges[] = ($rstart == $rend) ? $rstart : $rstart.'-'.$rend;
} else {
return ''; // Not numeric value(s)
}
}
return implode($separator, $ranges);
}
// '1-3,5,7,9-12,14' -> array(1,2,3,5,7,9,10,11,12,14)
function list_to_range($str, $separator = ',', $sort = TRUE) {
if (!is_string($str)) { return $str; }
// Clean spaces while separator not with spaces
if (!str_contains($separator, ' ')) {
$str = str_replace(' ', '', $str);
}
$arr = [];
foreach (explode($separator, trim($str)) as $list) {
$negative = FALSE;
if ($list[0] === '-') {
$negative = TRUE;
$list = substr($list, 1);
}
if (str_contains($list, '-')) {
list($min, $max) = explode('-', $list, 2);
if (!is_numeric($min) || !is_numeric($max)) { continue; }
if ($negative) {
$min = '-' . $min;
}
if ($min > $max) {
// ie 10-3
list($min, $max) = [ $max, $min ];
} elseif ($min == $max) {
// ie 1-1
$arr[] = (int)$min;
continue;
}
for ($i = $min; $i <= $max; $i++) {
$arr[] = (int)$i;
}
} elseif (is_numeric($list)) {
$arr[] = $negative ? (int)('-' . $list) : (int)$list;
}
}
if ($sort) { sort($arr, SORT_NUMERIC); }
return $arr;
}
/**
* Write a line to the specified logfile (or default log if not specified).
* We open & close for every line, somewhat lower performance but this means multiple concurrent processes could write to the file.
* Now marking process and pid, if things are running simultaneously you can still see what's coming from where.
*
* @param string $filename
* @param string $string
*
* @return false|void
*/
function logfile($filename, $string = NULL) {
global $config, $argv;
// Use default logfile if none specified
if (safe_empty($string)) {
$string = $filename;
$filename = $config['log_file'];
}
// Place logfile in log directory if no path specified
if (basename($filename) === $filename) {
$filename = $config['log_dir'] . '/' . $filename;
}
// Create logfile if not exist
if (is_file($filename)) {
if (!is_writable($filename)) {
print_debug("Log file '$filename' is not writable, check file permissions.");
return FALSE;
}
$fd = fopen($filename, 'ab');
} else {
$fd = fopen($filename, 'wb');
// Check writable file (only after creation for speedup)
if (!is_writable($filename)) {
print_debug("Log file '$filename' is not writable or not created.");
fclose($fd);
return FALSE;
}
}
//$string = '[' . date('Y/m/d H:i:s O') . '] ' . basename($argv[0]) . '(' . getmypid() . '): ' . trim($string) . PHP_EOL;
$string = '[' . date('Y/m/d H:i:s O') . '] ' . OBS_SCRIPT_NAME . '(' . getmypid() . '): ' . trim($string) . PHP_EOL;
fwrite($fd, $string);
fclose($fd);
}
/**
* Get used system versions
*
* @param string|null $program
*
* @return array|string
*/
function get_versions($program = NULL) {
$return_version = !empty($program); // return only version string for program
if (isset($GLOBALS['cache']['versions'])) {
// Already cached
if ($return_version) {
$key = strtolower($program) . '_version';
return isset($GLOBALS['cache']['versions'][$key]) ? $GLOBALS['cache']['versions'][$key] : '';
}
return $GLOBALS['cache']['versions'];
}
$versions = array(); // Init
if (!empty($program)) {
// Return only one not cached version
$programs = (array)$program;
} else {
// return array with all versions
$programs = [ 'os', 'php', 'python', 'mysql', 'snmp', 'rrdtool', 'fping', 'http' ];
}
foreach ($programs as $entry) {
switch ($entry) {
case 'os':
// Local system OS version
if (is_executable($GLOBALS['config']['install_dir'].'/scripts/distro')) {
$os = explode('|', external_exec($GLOBALS['config']['install_dir'].'/scripts/distro'), 6);
$versions['os_system'] = $os[0];
$versions['os_version'] = $os[1];
$versions['os_arch'] = $os[2];
$versions['os_distro'] = $os[3];
$versions['os_distro_version'] = $os[4];
$versions['os_virt'] = $os[5];
$versions['os_text'] = $os[0].' '.$os[1].' ['.$os[2].'] ('.$os[3].' '.$os[4].')';
}
if ($return_version) {
return (string)$versions['os_version'];
}
break;
case 'php':
// PHP
$php_version = PHP_VERSION;
$versions['php_full'] = $php_version;
$versions['php_version'] = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
if ($return_version) {
return $versions['php_version'];
}
$versions['php_old'] = version_compare($versions['php_version'], OBS_MIN_PHP_VERSION, '<');
// PHP OPcache
$versions['php_opcache'] = FALSE;
if (extension_loaded('Zend OPcache')) {
$opcache = ini_get('opcache.enable');
$php_version .= ' (OPcache: ';
if ($opcache && is_cli() && ini_get('opcache.enable_cli')) { // CLI
$php_version .= 'ENABLED)';
$versions['php_opcache'] = 'ENABLED';
} elseif ($opcache && !is_cli()) { // WUI
$php_version .= 'ENABLED)';
$versions['php_opcache'] = 'ENABLED';
} else {
$php_version .= 'DISABLED)';
$versions['php_opcache'] = 'DISABLED';
}
}
$versions['php_text'] = $php_version;
// PHP memory_limit
$php_memory_limit = unit_string_to_numeric(ini_get('memory_limit'));
$versions['php_memory_limit'] = $php_memory_limit;
if ($php_memory_limit < 0) {
$versions['php_memory_limit_text'] = 'Unlimited';
} else {
$versions['php_memory_limit_text'] = formatStorage($php_memory_limit);
}
break;
case 'python':
/** Python
* I.e.:
* python_version = 2.7.5
* python_text = 2.7.5
*/
$python_version = str_replace('Python ', '', external_exec('/usr/bin/env python --version 2>&1'));
$python_default = TRUE;
if (str_contains($python_version, 'No such file or directory')) {
// /usr/bin/env: 'python': No such file or directory
$python_version = str_replace('Python ', '', external_exec('/usr/bin/env python3 --version 2>&1'));
if (str_contains($python_version, 'No such file or directory')) {
// /usr/bin/env: 'python': No such file or directory
$python_version = 'Not found';
} else {
// Append info about no default python executable
$python_default = FALSE;
}
}
$versions['python_version'] = $python_version;
if ($return_version) {
return $versions['python_version'];
}
if (str_starts($python_version, '2.')) {
$versions['python_old'] = version_compare($versions['python_version'], OBS_MIN_PYTHON2_VERSION, '<');
} else {
$versions['python_old'] = version_compare($versions['python_version'], OBS_MIN_PYTHON3_VERSION, '<');
}
$versions['python_text'] = $python_version;
if (!$python_default) {
$versions['python_text'] .= ' (python3 not used as default python)';
}
break;
case 'mysql':
case 'mariadb':
/** MySQL
* I.e.:
* mysql_client = 5.0.12-dev
* mysql_full = 10.3.23-MariaDB-log
* mysql_name = MariaDB
* mysql_version = 10.3.23
* mysql_text = 10.3.23-MariaDB-log (extension: mysqli 5.0.12-dev)
*/
$mysql_client = dbClientInfo();
if (preg_match('/(\d+\.[\w\.\-]+)/', $mysql_client, $matches)) {
$mysql_client = $matches[1];
}
$versions['mysql_client'] = $mysql_client;
$mysql_version = dbFetchCell("SELECT version();");
$versions['mysql_full'] = $mysql_version;
list($versions['mysql_version']) = explode('-', $mysql_version);
if ($return_version) {
return $versions['mysql_version'];
}
$versions['mysql_name'] = str_icontains_array($mysql_version, 'Maria') ? 'MariaDB' : 'MySQL';
if ($versions['mysql_name'] === 'MariaDB') {
$versions['mysql_old'] = version_compare($versions['mysql_version'], OBS_MIN_MARIADB_VERSION, '<');
} else {
$versions['mysql_old'] = version_compare($versions['mysql_version'], OBS_MIN_MYSQL_VERSION, '<');
}
$mysql_version .= ' (extension: ' . OBS_DB_EXTENSION . ' ' . $mysql_client . ')';
$versions['mysql_text'] = $mysql_version;
// Define DB NAME for later use
if (!defined('OBS_DB_NAME')) {
define('OBS_DB_NAME', $versions['mysql_name']);
}
break;
case 'snmp':
/** SNMP
* I.e.:
* snmp_version = 5.7.2
* snmp_text = NET-SNMP 5.7.2
*/
$snmp_cmd = is_executable($GLOBALS['config']['snmpget']) ? $GLOBALS['config']['snmpget'] : '/usr/bin/env snmpget';
$snmp_version = str_replace(' version:', '', external_exec($snmp_cmd . " --version 2>&1"));
$versions['snmp_version'] = str_replace('NET-SNMP ', '', $snmp_version);
if ($return_version) {
return $versions['snmp_version'];
}
if (empty($versions['snmp_version'])) {
$versions['snmp_version'] = 'not found';
}
$versions['snmp_text'] = $snmp_version;
break;
case 'rrdtool':
/** RRDtool
* I.e.:
* rrdtool_version = 1.5.5
* rrdcached_version = 1.5.5
* rrdtool_text = 1.5.5 (rrdcached 1.5.5: unix:/var/run/rrdcached.sock)
*/
$rrdtool_cmd = is_executable($GLOBALS['config']['rrdtool']) ? $GLOBALS['config']['rrdtool'] : '/usr/bin/env rrdtool';
list(,$rrdtool_version) = explode(' ', external_exec($rrdtool_cmd . ' --version | head -n1'));
$versions['rrdtool_version'] = $rrdtool_version;
if ($return_version) {
return $versions['rrdtool_version'];
}
$versions['rrdtool_old'] = version_compare($versions['rrdtool_version'], OBS_MIN_RRD_VERSION, '<');
if (strlen($GLOBALS['config']['rrdcached'])) {
if (OBS_RRD_NOLOCAL) {
// Remote rrdcached daemon (unknown version)
$rrdtool_version .= ' (rrdcached remote: ' . $GLOBALS['config']['rrdcached'] . ')';
// Remote RRDcached require version 1.5.5
$versions['rrdtool_old'] = version_compare($versions['rrdtool_version'], '1.5.5', '<');
} else {
$rrdcached_exec = str_replace('rrdtool', 'rrdcached', $GLOBALS['config']['rrdtool']);
if (!is_executable($rrdcached_exec)) {
$rrdcached_exec = '/usr/bin/env rrdcached -h';
}
list(,$versions['rrdcached_version']) = explode(' ', external_exec($rrdcached_exec . ' -h | head -n1'));
$rrdtool_version .= ' (rrdcached ' . $versions['rrdcached_version'] . ': ' . $GLOBALS['config']['rrdcached'] . ')';
}
}
if (empty($rrdtool_version)) {
$rrdtool_version = 'not found';
$versions['rrdtool_version'] = $rrdtool_version;
$versions['rrdtool_old'] = TRUE;
}
$versions['rrdtool_text'] = $rrdtool_version;
break;
case 'fping':
/** Fping
* I.e.:
* fping_version = 3.13
* fping_text = 3.13 (IPv4 and IPv6)
*/
$fping_version = 'not found';
$fping_exec = is_executable($GLOBALS['config']['fping']) ? $GLOBALS['config']['fping'] : '/usr/bin/env fping';
$fping = external_exec($fping_exec . " -v 2>&1");
if (preg_match('/Version\s+(\d\S+)/', $fping, $matches)) {
$fping_version = $matches[1];
$fping_text = $fping_version;
if (version_compare($fping_version, '4.0', '>=')) {
$fping_text .= ' (IPv4 and IPv6)';
} elseif (is_executable($GLOBALS['config']['fping6'])) {
$fping_text .= ' (IPv4 and IPv6)';
} else {
$fping_text .= ' (IPv4 only)';
}
}
$versions['fping_version'] = $fping_version;
$versions['fping_text'] = $fping_text;
if ($return_version) {
return $versions['fping_version'];
}
break;
case 'http':
// Apache (or any http used?)
if (is_cli()) {
foreach (array('apache2', 'httpd') as $http_cmd) {
if (is_executable('/usr/sbin/'.$http_cmd)) {
$http_cmd = '/usr/sbin/'.$http_cmd;
} else {
$http_cmd = '/usr/bin/env '.$http_cmd;
}
$http_version = external_exec($http_cmd.' -v | awk \'/Server version:/ {print $3}\'');
if ($http_version) { break; }
}
if (empty($http_version)) {
$http_version = 'not found';
}
$versions['http_full'] = $http_version;
} else {
$versions['http_full'] = $_SERVER['SERVER_SOFTWARE'];
}
$versions['http_version'] = str_replace('Apache/', '', $versions['http_full']);
$versions['http_text'] = $versions['http_version'];
if ($return_version) {
return $versions['http_version'];
}
break;
}
}
// Cache for current execution
$GLOBALS['cache']['versions'] = $versions;
//print_vars($GLOBALS['cache']['versions']);
return $versions;
}
/**
* Print version information about used Observium and additional software.
*
* @return NULL
*/
function print_versions() {
get_versions();
$os_version = $GLOBALS['cache']['versions']['os_text'];
$php_version = $GLOBALS['cache']['versions']['php_text'];
$python_version = $GLOBALS['cache']['versions']['python_text'];
$mysql_version = $GLOBALS['cache']['versions']['mysql_text'];
$mysql_name = $GLOBALS['cache']['versions']['mysql_name'];
$snmp_version = $GLOBALS['cache']['versions']['snmp_text'];
$rrdtool_version = $GLOBALS['cache']['versions']['rrdtool_text'];
$fping_version = $GLOBALS['cache']['versions']['fping_text'];
$http_version = $GLOBALS['cache']['versions']['http_text'];
// PHP memory_limit
$php_memory_limit = $GLOBALS['cache']['versions']['php_memory_limit'];
$php_memory_limit_text = $GLOBALS['cache']['versions']['php_memory_limit_text'];
if (is_cli()) {
$timezone = get_timezone();
//print_vars($timezone);
$mysql_mode = dbFetchCell("SELECT @@SESSION.sql_mode;");
$mysql_charset = dbShowVariables("LIKE 'character_set_connection'");
if ($GLOBALS['cache']['versions']['php_old']) {
$php_version = '%r' . $php_version;
}
if ($GLOBALS['cache']['versions']['python_old']) {
$python_version = '%r' . $python_version;
}
if ($GLOBALS['cache']['versions']['mysql_old']) {
$mysql_version = '%r' . $mysql_version;
}
if ($GLOBALS['cache']['versions']['rrdtool_old']) {
$rrdtool_version = '%r' . $rrdtool_version;
}
echo(PHP_EOL);
print_cli_heading("Software versions");
print_cli_data("OS", $os_version);
print_cli_data("Apache", $http_version);
print_cli_data("PHP", $php_version);
print_cli_data("Python", $python_version);
print_cli_data($mysql_name, $mysql_version);
print_cli_data("SNMP", $snmp_version);
print_cli_data("RRDtool", $rrdtool_version);
print_cli_data("Fping", $fping_version);
// Additionally, in CLI always display Memory Limit, MySQL Mode and Charset info
echo(PHP_EOL);
print_cli_heading("Memory Limit", 3);
print_cli_data("PHP", ($php_memory_limit >= 0 && $php_memory_limit < 268435456 ? '%r' : '') . $php_memory_limit_text, 3);
echo(PHP_EOL);
print_cli_heading("$mysql_name mode", 3);
print_cli_data($mysql_name, $mysql_mode, 3);
echo(PHP_EOL);
print_cli_heading("Charset info", 3);
print_cli_data("PHP", ini_get("default_charset"), 3);
print_cli_data($mysql_name, $mysql_charset['character_set_connection'], 3);
echo(PHP_EOL);
print_cli_heading("Timezones info", 3);
print_cli_data("Date", date("l, d-M-y H:i:s T"), 3);
print_cli_data("PHP", $timezone['php'], 3);
print_cli_data($mysql_name, ($timezone['diff'] !== 0 ? '%r' : '') . $timezone['mysql'], 3);
if (OBS_DEBUG) {
$phpfastcache = get_cache_stats();
echo(PHP_EOL);
print_cli_heading("Fast Cache info", 3);
print_cli_data("Enabled", $phpfastcache['enabled'] ? '%gYes%n' : '%yNo%n', 3);
print_cli_data("Driver", $phpfastcache['driver'], 3);
print_cli_data("Total size", formatStorage($phpfastcache['size']), 3);
}
if (OBS_DISTRIBUTED) {
$id = $GLOBALS['config']['poller_id'];
echo(PHP_EOL);
print_cli_heading("Poller info", 3);
print_cli_data("ID", $id, 3);
print_cli_data("Name", ($id !== 0 ? $GLOBALS['config']['poller_name'] : 'Main'), 3);
}
echo(PHP_EOL);
} else {
$observium_date = format_unixtime(strtotime(OBSERVIUM_DATE), 'jS F Y');
if ($php_memory_limit >= 0 && $php_memory_limit < 268435456) {
$php_memory_limit_text = '<span class="text-danger">'.$php_memory_limit_text.'</span>';
}
// Check minimum versions
if ($GLOBALS['cache']['versions']['php_old']) {
$php_class = 'error';
$php_version = generate_tooltip_link(NULL, $php_version, 'Minimum supported: ' . OBS_MIN_PHP_VERSION);
} else {
$php_class = '';
$php_version = escape_html($php_version);
}
if ($GLOBALS['cache']['versions']['python_old']) {
$python_class = 'warning';
if (str_starts($python_version, '2.')) {
$python_version = generate_tooltip_link(NULL, $python_version, 'Recommended version is greater than or equal to: ' . OBS_MIN_PYTHON2_VERSION . ' or ' . OBS_MIN_PYTHON3_VERSION);
} else {
$python_version = generate_tooltip_link(NULL, $python_version, 'Recommended version is greater than or equal to: ' . OBS_MIN_PYTHON3_VERSION);
}
} else {
$python_class = '';
$python_version = escape_html($python_version);
}
if ($GLOBALS['cache']['versions']['mysql_old']) {
$mysql_class = 'warning';
if ($mysql_name === 'MariaDB') {
$mysql_version = generate_tooltip_link(NULL, $mysql_version, 'Recommended version is greater than or equal to: ' . OBS_MIN_MARIADB_VERSION);
} else {
$mysql_version = generate_tooltip_link(NULL, $mysql_version, 'Recommended version is greater than or equal to: ' . OBS_MIN_MYSQL_VERSION);
}
} else {
$mysql_class = '';
$mysql_version = escape_html($mysql_version);
}
if ($GLOBALS['cache']['versions']['rrdtool_old']) {
$rrdtool_class = 'error';
$rrdtool_version = generate_tooltip_link(NULL, $rrdtool_version, 'Minimum supported: ' . OBS_MIN_RRD_VERSION);
} else {
$rrdtool_class = '';
$rrdtool_version = escape_html($rrdtool_version);
}
echo generate_box_open(array('title' => 'Version Information'));
echo('
<table class="table table-striped table-condensed-more">
<tbody>
<tr><td><b>'.escape_html(OBSERVIUM_PRODUCT).'</b></td><td>'.escape_html(OBSERVIUM_VERSION).' ('.escape_html($observium_date).')</td></tr>
<tr><td><b>OS</b></td><td>'.escape_html($os_version).'</td></tr>
<tr><td><b>Apache</b></td><td>'.escape_html($http_version).'</td></tr>
<tr class="'.$php_class.'"><td><b>PHP</b></td><td>'.$php_version.' (Memory: '.$php_memory_limit_text.')</td></tr>
<tr class="'.$python_class.'"><td><b>Python</b></td><td>'.$python_version.'</td></tr>
<tr class="'.$mysql_class.'"><td><b>'.$mysql_name.'</b></td><td>'.$mysql_version.'</td></tr>
<tr><td><b>SNMP</b></td><td>'.escape_html($snmp_version).'</td></tr>
<tr class="'.$rrdtool_class.'"><td><b>RRDtool</b></td><td>'.$rrdtool_version.'</td></tr>
<tr><td><b>Fping</b></td><td>'.escape_html($fping_version).'</td></tr>
</tbody>
</table>'.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 return compressed query.
*
* @param string $query
* @param string $format
*/
function print_sql($query, $format = 'compress') {
if (PHP_VERSION_ID < 70000) {
// SqlFormatter support only php 7+
return rtrim($query);
}
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 = '<p>' . $output . '</p>';
} 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 (isset($GLOBALS['cli']) && $GLOBALS['cli']) {
if (function_exists('rt')) {
ref::config('shortcutFunc', array('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);
}
} else {
if (function_exists('r'))
{
ref::config('shortcutFunc', array('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 {
print_r($vars);
}
}
}
/**
* 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
list(, $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);
list($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('/(?<year>\d+)-(?<mon>\d+)-(?<day>\d+)(?:,(?<hour>\d+):(?<min>\d+):(?<sec>\d+)(?<millisec>\.\d+)?(?:,(?<tzs>[+\-]?)(?<tzh>\d+):(?<tzm>\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);
} else {
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
list(, $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 betwen 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 = array();
$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'] = 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 (array('php', 'mysql') as $entry)
{
if (!isset($timezone[$entry])) { continue; } // skip mysql if OBS_DB_SKIP
$sign = $timezone[$entry][0];
list($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 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'];
}
// DOCME needs phpdoc block
function humanspeed($speed)
{
if ($speed == '')
{
return '-';
} else {
return formatRates($speed);
}
}
/**
* 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)) {
$mac_clean = $mac;
} else {
// Strip out non-hex digits (Not sure when this required, copied for compat with old format_mac())
$mac = preg_replace('/[[:^xdigit:]]/', '', $mac);
$mac_clean = (strlen($mac) === 12) ? $mac : NULL;
//$mac_clean = NULL;
}
return $mac_clean;
}
/**
* Format any mac address string.
*
* @param string $mac MAC address string in any known format
* @param string $split_char Allowed split chars for specific formats: ':', '', ' ', '0x'
*
* @return 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;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function format_number_short($number, $sf) {
// This formats a number so that we only send back three digits plus an optional decimal point.
// Example: 723.42 -> 723, 72.34 -> 72.3, 2.23 -> 2.23
list($whole, $decimal) = explode('.', $number);
$whole_len = strlen($whole);
if (is_numeric($number) && abs($number) > 0 && preg_match('/^\-?0$/', $whole) &&
preg_match('/^(?<zero>\-?0\.0+)(?<numeric>\d+?)0*$/', (string) $number, $matches)) {
// prevent show small values as zero
// ie 0.000627 as 0.00
//r($orig);
//r($value);
$numeric = substr($matches['numeric'], 0, $sf) . '.' . substr($matches['numeric'], $sf);
$number = $matches['zero'] . round(rtrim($numeric, '.'));
//r($value);
} elseif ($whole_len >= $sf || !is_numeric($decimal)) {
$number = $whole;
} elseif ($whole_len < $sf) {
$number = $whole;
$diff = $sf - $whole_len;
$decimal = rtrim(substr($decimal, 0, $diff), '0');
if (strlen($decimal)) {
$number .= '.' . $decimal;
}
}
return $number;
}
/**
* Detect if required exec functions available
*
* @return boolean TRUE if proc_open/proc_get_status available and not in safe mode.
*/
function is_exec_available()
{
// Detect that function ini_get() not disabled too
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 = array('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;
}
}
/*
// Detect safe mode
$safe_mode = ini_get('safe_mode');
if (strtolower($safe_mode) != 'off')
{
return FALSE;
}
*/
return TRUE;
}
function pipe_open($command, &$pipes, $cwd = NULL, $env = []) {
$descriptorspec = array(
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;
}
function pipe_read($command, &$pipes, $fullread = TRUE) {
// $pipes now looks 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(10000);
$line = fgets($pipes[1], 1024);
$stdout .= $line;
$iter++;
}
}
return preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
}
function pipe_close($process, &$pipes) {
if (is_resource($pipes[0])) {
fclose($pipes[0]);
}
fclose($pipes[1]);
fclose($pipes[2]);
// It is important that you close any pipes before calling
// proc_close in order to avoid a deadlock
return proc_close($process);
}
// DOCME needs phpdoc block
function external_exec($command, $timeout = NULL) {
global $exec_status;
$command = trim($command);
// Debug the command *before* we run it!
$debug = defined('OBS_DEBUG') ? OBS_DEBUG : 0;
if ($debug > 0) {
$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 auth params from debug cmd out,
// for help users who want send debug output to developers
$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, ie in WMIC
$pattern = "/ --(user(?:name)?|password)=(\S+|\'[^\']*\')/";
$debug_command = preg_replace($pattern, ' --\1=***', $debug_command);
}
print_message(PHP_EOL . 'CMD[%y' . $debug_command . '%n]' . PHP_EOL, 'console');
}
$exec_status = [
'command' => $command,
'exitdelay' => 0
];
if ($command === '') {
// Hardcode exec_status if empty command passed
if (isset($GLOBALS['snmp_command'])) {
$exec_status['command'] = $GLOBALS['snmp_command'];
unset($GLOBALS['snmp_command']); // Now clean this global var
}
$exec_status['exitcode'] = -1;
$exec_status['endtime'] = microtime(TRUE); // store end unixtime with microseconds
$exec_status['runtime'] = 0;
$exec_status['stderr'] = 'Empty command passed';
$exec_status['stdout'] = '';
if ($debug > 0) {
print_message('CMD EXITCODE['.($exec_status['exitcode'] !== 0 ? '%r' : '%g').$exec_status['exitcode'].'%n]'.PHP_EOL.
'CMD RUNTIME['.($exec_status['runtime'] > 7 ? '%r' : '%g').round($exec_status['runtime'], 4).'s%n]', 'console');
print_message("STDOUT[\n\n]", 'console', FALSE);
if ($exec_status['exitcode'] && $exec_status['stderr']) {
// Show stderr if exitcode not 0
print_message("STDERR[\n".$exec_status['stderr']."\n]", 'console', FALSE);
}
}
return '';
}
if (is_numeric($timeout) && $timeout > 0) {
$timeout_usec = $timeout * 1000000;
$timeout = 0;
} else {
// set timeout to null (not to 0!), see stream_select() description
$timeout_usec = NULL;
$timeout = NULL;
}
$descriptorspec = [
//0 => [ 'pipe', 'r' ], // stdin
1 => [ 'pipe', 'w' ], // stdout
2 => [ 'pipe', 'w' ] // stderr
];
//$process = proc_open($command, $descriptorspec, $pipes);
$process = proc_open('exec ' . $command, $descriptorspec, $pipes); // exec prevent to use shell
$stdout = $stderr = '';
$runtime = 0;
if (is_resource($process)) {
//stream_set_blocking($pipes[0], 0); // Make stdin/stdout/stderr non-blocking
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
$start = microtime(TRUE);
//while ($status['running'] !== FALSE)
//while (feof($pipes[1]) === FALSE || feof($pipes[2]) === FALSE)
while (TRUE) {
$read = array();
if (!feof($pipes[1])) { $read[] = $pipes[1]; }
if (!feof($pipes[2])) { $read[] = $pipes[2]; }
if (empty($read)) { break; }
$write = NULL;
$except = NULL;
stream_select($read, $write, $except, $timeout, $timeout_usec);
// Read the contents from the buffers
foreach ($read as $pipe) {
if ($pipe === $pipes[1]) {
$stdout .= fread($pipe, 8192);
} elseif ($pipe === $pipes[2]) {
$stderr .= fread($pipe, 8192);
}
}
$runtime = microtime(TRUE) - $start;
// Get the status of the process
$status = proc_get_status($process);
// Break from this loop if the process exited before timeout
if (!$status['running']) {
if (feof($pipes[1]) === FALSE) {
// Very rare situation, seems as next proc_get_status() bug
if (!isset($status_fix)) { $status_fix = $status; }
if ($debug > 1) {
print_error("Wrong process status! Issue in proc_get_status(), see: https://bugs.php.net/bug.php?id=69014");
}
} else {
//var_dump($status);
break;
}
}
// Break from this loop if the process exited by timeout
if ($timeout !== NULL) {
$timeout_usec -= $runtime * 1000000;
if ($timeout_usec < 0) {
$status['running'] = FALSE;
$status['exitcode'] = -1;
break;
}
}
}
if ($status['running']) {
// Fix sometimes wrong status, wait for 10 milliseconds
$delay = 0;
$delay_step = 10000; // 10ms
$delay_max = 300000; // 300ms
while ($status['running'] && $delay < $delay_max) {
usleep($delay_step);
$status = proc_get_status($process);
$delay += $delay_step;
}
$exec_status['exitdelay'] = (int)($delay / 1000); // Convert to ms
} elseif (isset($status_fix)) {
// See fixed proc_get_status() above
$status = $status_fix;
}
$exec_status['exitcode'] = (int)$status['exitcode'];
$exec_status['stderr'] = rtrim($stderr);
$stdout = preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
proc_terminate($process, 9);
//fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
} else {
// That happened on low memory for process
//logfile('debug_exec.log', "Failed run external_exec(): '$command'.");
//logfile('debug_exec.log', var_export($process, TRUE));
$stdout = FALSE;
$exec_status['stderr'] = '';
$exec_status['exitcode'] = -1;
}
$exec_status['endtime'] = $start + $runtime; // store end unixtime with microseconds
$exec_status['runtime'] = $runtime;
$exec_status['stdout'] = $stdout;
if ($debug > 0) {
print_message('CMD EXITCODE['.($exec_status['exitcode'] !== 0 ? '%r' : '%g').$exec_status['exitcode'].'%n]'.PHP_EOL.
'CMD RUNTIME['.($runtime > 7 ? '%r' : '%g').round($runtime, 4).'s%n]', 'console');
if ($exec_status['exitdelay'] > 0)
{
print_message("CMD EXITDELAY[%r".$exec_status['exitdelay']."ms%n]", 'console');
}
//print_message("STDOUT[\n".$stdout."\n]", 'console', FALSE);
echo("STDOUT[\n".$stdout."\n]\n"); // Do not colouring stdout
if ($exec_status['exitcode'] && $exec_status['stderr'])
{
// Show stderr if exitcode not 0
//print_message("STDERR[\n".$exec_status['stderr']."\n]", 'console', FALSE);
echo("STDERR[\n".$exec_status['stderr']."\n]\n"); // Do not colouring stdout
}
}
return $stdout;
}
/**
* 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 = array('PID' => "$pid");
$ps_stat = explode(" ", file_get_contents("/proc/$pid/stat"));
//echo PHP_EOL; print_vars($ps_stat); echo PHP_EOL;
//echo PHP_EOL; print_vars($pid_stat); echo PHP_EOL;
$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, 1); // Set timeout 1sec for exec
$ps = explode("\n", rtrim($ps));
if ($GLOBALS['exec_status']['exitcode'] === 127) {
print_debug("/bin/ps command not found, not possible to get process info.");
return NULL;
}
if ($GLOBALS['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_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 = array();
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 = array('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 = array('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 = array('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` = ?', array($process['process_id']));
}
}
return $check;
}
/**
* Determine array is associative?
*
* @param array $array
* @return boolean
*/
function is_array_assoc($array) {
if (PHP_VERSION_ID >= 80100) {
return is_array($array) && !array_is_list($array);
}
return is_array($array) && $array !== array_values($array);
}
/**
* Determine array is sequential list?
*
* @param array $array
* @return boolean
*/
function is_array_list($array) {
if (PHP_VERSION_ID >= 80100) {
return is_array($array) && array_is_list($array);
}
return is_array($array) && $array === array_values($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));
}
/**
* 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 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;
$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 || strpos($string, $needle) !== FALSE;
}
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 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;
$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 === '';
}
// PHP 8.0 simplify for case sensitive compare
if (PHP_VERSION_ID >= 80000) {
return str_starts_with((string)$string, $needle);
}
// Case-sensitive
return $string[0] === $needle[0] && strncmp($string, $needle, strlen($needle)) === 0;
}
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 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;
$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;
}
// PHP 8.0 simplify for case sensitive compare
if (!$case_insensitivity && PHP_VERSION_ID >= 80000) {
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
* @param string $encode Encoding for return string: hex or base64 (shorten)
*
* @return string
*/
function str_compress($string, $encode = 'base64') {
if (!is_string($string)) { return $string; }
if ($compressed = gzdeflate($string, 9)) {
$compressed = gzdeflate($compressed, 9);
if (OBS_DEBUG > 1) {
$compressed = $encode === 'hex' ? bin2hex($compressed) : safe_base64_encode($compressed);
print_cli("String '$string' [".strlen($string)."] compressed to '".$compressed."' [".strlen($compressed)."].");
return $compressed;
}
return $encode === 'hex' ? bin2hex($compressed) : 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) &&
// COMPAT with old style, encoded by hex (can be removed in 2023 @mike)
!(ctype_xdigit($compressed) && ($bin = hex2bin($compressed)) && ($string = gzinflate(gzinflate($bin))))) {
// Not an compressed string?
//var_dump(hex2bin($compressed));
//var_dump(gzinflate(hex2bin($compressed)));
//var_dump($string);
return FALSE;
}
if (OBS_DEBUG > 1) {
print_cli("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']));
}
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;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function print_prompt($text, $default_yes = FALSE) {
if (is_cli()) {
if (cli_is_piped()) {
// If now not have interactive TTY skip any prompts, return default
return TRUE && $default_yes;
}
$question = ($default_yes ? 'Y/n' : 'y/N');
echo trim($text), " [$question]: ";
$handle = fopen ('php://stdin', 'r');
$line = strtolower(trim(fgets($handle, 3)));
fclose($handle);
if ($default_yes)
{
$return = ($line === 'no' || $line === 'n');
} else {
$return = ($line === 'yes' || $line === 'y');
}
} else {
// Here placeholder for web prompt
$return = TRUE && $default_yes;
}
return $return;
}
/**
* This function echoes text with style 'debug', see print_message().
* Here checked constant OBS_DEBUG, if OBS_DEBUG not set output - empty.
*
* @param string $text
* @param boolean $strip Stripe special characters (for web) or html tags (for cli)
*/
function print_debug($text, $strip = FALSE)
{
if (defined('OBS_DEBUG') && OBS_DEBUG > 0)
{
print_message($text, 'debug', $strip);
}
}
/**
* This function echoes text with style 'error', see print_message().
*
* @param string $text
* @param boolean $strip Stripe special characters (for web) or html tags (for cli)
*/
function print_error($text, $strip = TRUE)
{
print_message($text, 'error', $strip);
}
/**
* This function echoes text with style 'warning', see print_message().
*
* @param string $text
* @param boolean $strip Stripe special characters (for web) or html tags (for cli)
*/
function print_warning($text, $strip = TRUE)
{
print_message($text, 'warning', $strip);
}
/**
* This function echoes text with style 'success', see print_message().
*
* @param string $text
* @param boolean $strip Stripe special characters (for web) or html tags (for cli)
*/
function print_success($text, $strip = TRUE)
{
print_message($text, 'success', $strip);
}
/**
* This function echoes text with specific styles (different for cli and web output).
*
* @param string $text
* @param string $type Supported types: default, success, warning, error, debug
* @param boolean $strip Stripe special characters (for web) or html tags (for cli)
*/
function print_message($text, $type='', $strip = TRUE)
{
global $config;
// Do nothing if input text not any string (like NULL, array or other). (Empty string '' still printed).
if (!is_string($text) && !is_numeric($text)) { return NULL; }
$type = strtolower(trim($type));
switch ($type)
{
case 'success':
$color = array('cli' => '%g', // green
'cli_color' => FALSE, // by default cli coloring disabled
'class' => 'alert alert-success'); // green
$icon = 'oicon-tick-circle';
break;
case 'warning':
$color = array('cli' => '%b', // blue
'cli_color' => FALSE, // by default cli coloring disabled
'class' => 'alert alert-warning'); // yellow
$icon = 'oicon-bell';
break;
case 'error':
$color = array('cli' => '%r', // red
'cli_color' => FALSE, // by default cli coloring disabled
'class' => 'alert alert-danger'); // red
$icon = 'oicon-exclamation-red';
break;
case 'debug':
$color = array('cli' => '%r', // red
'cli_color' => FALSE, // by default cli coloring disabled
'class' => 'alert alert-danger'); // red
$icon = 'oicon-exclamation-red';
break;
case 'color':
$color = array('cli' => '', // none
'cli_color' => TRUE, // allow using coloring
'class' => 'alert alert-info'); // blue
$icon = 'oicon-information';
break;
case 'console':
// This is special type used nl2br conversion for display console messages on WUI with correct line breaks
$color = array('cli' => '', // none
'cli_color' => TRUE, // allow using coloring
'class' => 'alert alert-suppressed'); // purple
$icon = 'oicon-information';
break;
default:
$color = array('cli' => '%W', // bold
'cli_color' => FALSE, // by default cli coloring disabled
'class' => 'alert alert-info'); // blue
$icon = 'oicon-information';
break;
}
if (is_cli())
{
if ($strip)
{
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); // Convert special HTML entities back to characters
$text = str_ireplace(array('<br />', '<br>', '<br/>'), PHP_EOL, $text); // Convert html <br> to into newline
$text = strip_tags($text);
}
// Store to global var
if ($type !== 'debug') { $GLOBALS['last_message'] = $text; }
if ($type === 'debug' && !$color['cli_color'])
{
// For debug just echo message.
echo($text . PHP_EOL);
} else {
print_cli($color['cli'].$text.'%n'.PHP_EOL, $color['cli_color']);
}
} else {
$GLOBALS['last_message'] = $text;
if ($text === '') {
// Do not web output if the string is empty
return NULL;
}
if ($strip)
{
if ($text == strip_tags($text))
{
// Convert special characters to HTML entities only if text not have html tags
$text = escape_html($text);
}
if ($color['cli_color'])
{
// Replace some Pear::Console_Color2 color codes with html styles
$replace = array('%', // '%%'
'</span>', // '%n'
'<span class="label label-warning">', // '%y'
'<span class="label label-success">', // '%g'
'<span class="label label-danger">', // '%r'
'<span class="label label-primary">', // '%b'
'<span class="label label-info">', // '%c'
'<span class="label label-default">', // '%W'
'<span class="label label-default" style="color:black;">', // '%k'
'<span style="font-weight: bold;">', // '%_'
'<span style="text-decoration: underline;">', // '%U'
);
} else {
$replace = array('%', '');
}
$text = str_replace(array('%%', '%n', '%y', '%g', '%r', '%b', '%c', '%W', '%k', '%_', '%U'), $replace, $text);
}
$msg = PHP_EOL.' <div class="'.$color['class'].'">';
if ($type != 'warning' && $type != 'error')
{
$msg .= '<button type="button" class="close" data-dismiss="alert">&times;</button>';
}
if ($type == 'console')
{
$text = nl2br(trim($text)); // Convert newline to <br /> for console messages with line breaks
}
$msg .= '
<div>'.$text.'</div>
</div>'.PHP_EOL;
echo($msg);
}
}
function get_last_message() {
if (!isset($GLOBALS['last_message'])) { return NULL; }
$text = str_replace(array('%%', '%n', '%y', '%g', '%r', '%b', '%c', '%W', '%k', '%_', '%U'), '', $GLOBALS['last_message']);
// Reset message for prevent errors in loops
//unset($GLOBALS['last_message']);
if (preg_match('/^[A-Z\_ ]+\[(.+)\]$/s', $text, $matches)) {
// CLI messages like:
// RESPONSE ERROR[You must use an API key to authenticate each request]
return $matches[1];
}
return strip_tags($text);
}
function print_cli($text, $colour = TRUE)
{
$msg = new Console_Color2();
// Always append reset colour at text end
if ($colour && str_contains($text, '%')) {
$text .= '%n';
}
echo $msg->convert($text, $colour);
}
/**
* Determine if poller/discovery module enabled for device
*
* @param array $device Device array
* @param string $module Module name
* @param string|null $process Process name (poller, discovery). Try to detect if not passed.
*
* @return bool
*/
function is_module_enabled($device, $module, $process = NULL, $settings = TRUE) {
global $config;
// Detect used process (poller, discovery, etc)
if (is_null($process) || !in_array($process, [ 'poller', 'discovery' ])) {
list($process) = explode('.', basename($GLOBALS['argv'][0]));
}
if (!in_array($process, [ 'poller', 'discovery' ])) {
print_debug("Module [$module] skipped. Not specified process name (poller or discovery).");
return FALSE;
}
// Detect if need to check submodule, ie: ports_ifstats
list($module, $submodule) = explode('_', $module, 2);
/* Allow specify module name with process prefix (poller_, discovery_), currently disabled
if (in_array($module, ['poller', 'discovery']))
{
$process = $module;
list($module, $submodule) = explode('_', $submodule, 2);
}
*/
//print_vars($process);
//print_vars($module);
//print_vars($submodule);
// Pre check if module is known (discovery_modules or poller_modules global config)
if (!isset($config[$process . '_modules'][$module])) {
print_debug("Module [$module] not exist.");
return FALSE;
}
if (!safe_empty($submodule)) {
// Check enabled only submodule
// Ie: $config['enable_ports_ipifstats']
$module_name = $module.'_'.$submodule;
$setting_name = 'enable_'.$module_name;
if (isset($config[$setting_name])) {
// Globally enabled/disabled submodule
$ok = (bool)$config[$setting_name];
$debug_reason = "disabled globally by config";
} else {
// Unknown Submodule
print_debug("Submodule [$module_name] not exist.");
return FALSE;
}
if ($settings) {
$attrib = get_entity_attrib('device', $device, $setting_name);
}
if ($module_name === 'ports_junoseatmvp' && $device['os'] !== 'junose') {
// Some hardcoded excludes
$ok = FALSE;
$debug_reason = "excluded for device os";
} elseif ($settings && !safe_empty($attrib)) {
// Submodule set by device setting
$ok = (bool)$attrib;
$debug_reason = "disabled by device setting";
} elseif (isset($config['os'][$device['os']]['modules'][$module_name])) {
// Submodule set by os definition
$ok = (bool)$config['os'][$device['os']]['modules'][$module_name];
$debug_reason = "disabled by os definition";
} else {
$model = get_model_array($device);
if (isset($model[$module_name])) {
// Submodule set by model definition
$ok = (bool)$model[$module_name];
$debug_reason = "disabled by model definition";
} elseif ($module_name === 'ports_separate_walk' && isset($config['os'][$device['os']][$module_name])) {
// FIXME. Need refactor:
// $config['os'][$os]['ports_separate_walk'] -> $config['os'][$os]['modules']['ports_separate_walk']
// Submodule set by os definition
$ok = (bool)$config['os'][$device['os']][$module_name];
$debug_reason = "disabled by os definition";
if ($device['os'] === 'junos' && !safe_empty($device['version']) && strnatcmp($device['version'], '17') > 0) {
// Hardcoded exclude for JunOS greater than 17+
// I not know how set this in definition, currently need only for junos
$ok = FALSE;
$debug_reason = "forced to disable for JunOS greater than 17+";
}
}
}
//print_warning("Module: $module_name = ".($ok ? 'ENABLED' : 'DISABLED'));
} else {
// Check main module
$module_name = $module;
// Globally enabled/disabled module
$ok = (bool)$config[$process . '_modules'][$module];
$debug_reason = "disabled globally by config";
if ($process === 'poller') {
if ($module === 'os' || $module === 'system') {
// os, system modules always enabled for poller
//print_debug("Module [$module] always enabled in process [$process].");
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['enabled'][] = $module;
return TRUE;
}
if (!function_exists('poller_module_excluded')) {
// In discovery this function missed
include_once($config['install_dir'] . "/includes/polling/functions.inc.php");
}
if (poller_module_excluded($device, $module)) {
// Module excluded
print_debug("Module [$module] excluded in process [$process].");
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['excluded'][] = $module;
return FALSE;
}
$setting_name = 'poll_'.$module;
} elseif ($process === 'discovery') {
if ($module === 'os') { // || $module == 'mibs')
// os module always enabled for discovery
//print_debug("Module [$module] always enabled in process [$process].");
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['enabled'][] = $module;
return TRUE;
}
$setting_name = 'discover_'.$module;
}
if ($settings) {
$attrib = get_entity_attrib('device', $device, $setting_name);
}
if ($settings && !safe_empty($attrib)) {
// Module set by device setting
$ok = (bool)$attrib;
$debug_reason = "disabled by device setting";
} elseif (in_array($module, (array)$config['os'][$device['os']][$process . '_blacklist'], TRUE)) {
// Module blacklisted by os definition
// $config['os'][$os]['poller_blacklist'] (array) list of blacklisted poller modules
// $config['os'][$os]['discovery_blacklist'] (array) list of blacklisted discovery modules
//$ok = FALSE;
//$debug_reason = "blacklisted by os definition";
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['excluded'][] = $module;
print_debug("Module [$module] blacklisted by os definition in process [$process].");
return FALSE;
} elseif ($ok && isset($config['os'][$device['os']]['modules'][$module_name])) {
// use os definition if module globally enabled (only), ie unix-agent: globally disabled, default disabled byt enabled on unix group
// Module set by os definition
$ok = (bool)$config['os'][$device['os']]['modules'][$module_name];
$debug_reason = "disabled by os definition";
} elseif ($module === 'wifi' && !in_array($device['type'], [ 'network', 'firewall', 'wireless' ], TRUE)) {
// WiFi by default allowed only for network/wireless/firewall types
$ok = FALSE;
$debug_reason = "disabled for device type '${device['type']}'.";
} /* else {
$model = get_model_array($device);
if (isset($model[$module_name])) {
// Submodule set by model definition
$ok = (bool)$model[$module_name];
$debug_reason = "disabled by model definition";
}
} */
}
if (!$ok) {
print_debug("Module [$module_name] $debug_reason in process [$process].");
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['disabled'][] = $module;
} else {
$GLOBALS['cache']['devices'][$process.'_modules'][$device['device_id']]['enabled'][] = $module;
}
return $ok;
}
// TESTME needs unit testing
/**
* Print an discovery/poller module stats
*
* @global array $GLOBALS['module_stats']
* @param array $device Device array
* @param string $module Module name
*/
function print_module_stats($device, $module)
{
$log_event = FALSE;
$stats_msg = array();
foreach (array('added', 'updated', 'deleted', 'unchanged') as $key)
{
if ($GLOBALS['module_stats'][$module][$key])
{
$stats_msg[] = (int)$GLOBALS['module_stats'][$module][$key].' '.$key;
if ($key != 'unchanged') { $log_event = TRUE; }
}
}
if (count($GLOBALS['module_stats'][$module])) { echo(PHP_EOL); }
if (count($stats_msg)) { print_cli_data("Changes", implode(', ', $stats_msg)); }
if ($GLOBALS['module_stats'][$module]['time'])
{
print_cli_data("Duration", $GLOBALS['module_stats'][$module]['time']."s");
}
if ($log_event) { log_event(nicecase($module).': '.implode(', ', $stats_msg).'.', $device, 'device', $device['device_id']); }
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function print_obsolete_config($filter = '')
{
global $config;
$list = array();
foreach ($config['obsolete_config'] as $entry)
{
if ($filter && strpos($entry['old'], $filter) === FALSE) { continue; }
$old = explode('->', $entry['old']);
// Founded obsolete config in overall $config
$found = !is_null(array_get_nested($config, $entry['old']));
if ($found && is_null(array_get_nested(get_defined_settings(), $entry['old'])))
{
// Check if this is in config.php or in DB config
$found = FALSE;
// FIXME need migrate old DB config to new
}
if ($found)
{
$new = explode('->', $entry['new']);
$info = strlen($entry['info']) ? ' ('.$entry['info'].')' : '';
$list[] = " %r\$config['".implode("']['", $old)."']%n -> %g\$config['".implode("']['", $new)."']%n".$info;
}
}
if ($list)
{
$msg = "%WWARNING%n Obsolete configuration(s) found in config.php file, please rename respectively:\n".implode(PHP_EOL, $list);
print_message($msg, 'color');
return TRUE;
} else {
return FALSE;
}
}
// Check if php extension exist, than warn or fail
// DOCME needs phpdoc block
// TESTME needs unit testing
function check_extension_exists($extension, $text = FALSE, $fatal = FALSE)
{
$extension = strtolower($extension);
if (isset($GLOBALS['cache']['extension'][$extension]))
{
// Cached
$exist = $GLOBALS['cache']['extension'][$extension];
} else {
$extension_functions = array(
'ldap' => 'ldap_connect',
'mysql' => 'mysql_connect',
'mysqli' => 'mysqli_connect',
'mbstring' => 'mb_detect_encoding',
'mcrypt' => 'mcrypt_encrypt', // CLEANME, mcrypt not used anymore (deprecated since php 7.1, removed since php 7.2)
'posix' => 'posix_isatty',
'session' => 'session_name',
'svn' => 'svn_log'
);
if (isset($extension_functions[$extension]))
{
$exist = @function_exists($extension_functions[$extension]);
} else {
$exist = @extension_loaded($extension);
}
// Cache
$GLOBALS['cache']['extension'][$extension] = $exist;
}
if (!$exist)
{
// Print error (only if $text not equals to FALSE)
if ($text === '' || $text === TRUE)
{
// Generic message
print_error("The extension '$extension' is missing. Please check your PHP configuration.");
}
elseif ($text !== FALSE)
{
// Custom message
print_error("The extension '$extension' is missing. $text");
} else {
// Debug message
print_debug("The extension '$extension' is missing. Please check your PHP configuration.");
}
// Exit if $fatal set to TRUE
if ($fatal) { exit(2); }
}
return $exist;
}
// TESTME needs unit testing
/**
* Sign function
*
* This function extracts the sign of the number.
* Returns -1 (negative), 0 (zero), 1 (positive)
*
* @param integer $int
* @return integer
*/
function sgn($int)
{
if ($int < 0)
{
return -1;
} elseif ($int == 0) {
return 0;
} else {
return 1;
}
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function get_bill_by_id($bill_id)
{
$bill = dbFetchRow("SELECT * FROM `bills` WHERE `bill_id` = ?", array($bill_id));
if (is_array($bill))
{
return $bill;
} else {
return FALSE;
}
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function get_application_by_id($application_id)
{
if (is_numeric($application_id))
{
$application = dbFetchRow("SELECT * FROM `applications` WHERE `app_id` = ?", array($application_id));
}
if (is_array($application))
{
return $application;
} else {
return FALSE;
}
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// MOVEME to includes/functions.inc.php
function accesspoint_by_id($ap_id, $refresh = '0')
{
$ap = dbFetchRow("SELECT * FROM `accesspoints` WHERE `accesspoint_id` = ?", array($ap_id));
return $ap;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function truncate($substring, $max = 50, $rep = '...') {
if ($rep === '...' && !is_cli()) {
// in html use entities triple dot
$rep_len = 1;
//$rep = '&hellip;';
$rep = '&mldr;';
} else {
$rep_len = strlen($rep);
}
if (strlen($substring) < 1) {
$string = $rep;
} else {
$string = $substring;
}
$leave = $max - $rep_len;
if (strlen($string) > $max) {
return substr_replace($string, $rep, $leave);
}
return $string;
}
/**
* Wrapper to htmlspecialchars()
*
* @param string $string
* @param int $flags
*
* @return string
*/
// TESTME needs unit testing
function escape_html($string, $flags = ENT_QUOTES) {
$string = htmlspecialchars($string, $flags, 'UTF-8');
// Un-escape allowed tags
if (str_contains($string, '&lt;') && str_contains_array($string, $GLOBALS['config']['escape_html']['tags'])) {
foreach ($GLOBALS['config']['escape_html']['tags'] as $tag) {
$string = str_replace([ '&lt;' . $tag . '&gt;', '&lt;/' . $tag . '&gt;', '&lt;' . $tag . ' /&gt;', '&lt;' . $tag . '/&gt;' ],
[ '<' . $tag . '>', '</' . $tag . '>', '<' . $tag . ' />', '<' . $tag . '/>' ],
$string);
}
}
// Un-escape allowed entities
if (str_contains($string, '&amp;') && str_contains_array($string, $GLOBALS['config']['escape_html']['entities'])) {
foreach ($GLOBALS['config']['escape_html']['entities'] as $tag) {
$string = str_replace('&amp;' . $tag . ';', '&' . $tag . ';', $string);
}
}
return $string;
}
// Return random string with optional character list
// DOCME needs phpdoc block
// TESTME needs unit testing
function generate_random_string($max = 16, $characters = NULL)
{
if (!$characters || !is_string($characters))
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
$randstring = '';
$length = strlen($characters) - 1;
for ($i = 0; $i < $max; $i++)
{
$randstring .= $characters[random_int(0, $length)]; // Require PHP 7.x or random_compat
}
return $randstring;
}
// Backward compatible random string generator
// DOCME needs phpdoc block
// TESTME needs unit testing
function strgen($length = 16)
{
return generate_random_string($length);
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function safename($filename)
{
return preg_replace('/[^a-zA-Z0-9._\-]/', '_', $filename);
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function zeropad($num, $length = 2)
{
return str_pad($num, $length, '0', STR_PAD_LEFT);
}
// OBSOLETE, remove when all function calls will be deleted
function get_dev_attrib($device, $attrib_type)
{
// Call to new function
return get_entity_attrib('device', $device, $attrib_type);
}
/**
* Return model array from definitions, based on device sysObjectID
*
* @param array $device Device array required keys -> os, sysObjectID
* @param string $sysObjectID_new If passed, then use "new" sysObjectID instead from device array
* @return array|FALSE Model array or FALSE if no model specific definitions
*/
function get_model_array($device, $sysObjectID_new = NULL) {
global $config, $cache;
if (isset($config['os'][$device['os']]['model'])) {
$model = $config['os'][$device['os']]['model'];
if (!isset($config['model'][$model])) { return FALSE; }
$models = $config['model'][$model];
$set_cache = FALSE;
if ($sysObjectID_new && preg_match('/^\.\d[\d\.]+$/', $sysObjectID_new)) {
// Use passed as param sysObjectID
$sysObjectID = $sysObjectID_new;
} elseif (isset($cache['devices']['model'][$device['device_id']])) {
// Return already cached array if no passed param sysObjectID
return $cache['devices']['model'][$device['device_id']];
} elseif (preg_match('/^\.\d[\d\.]+$/', $device['sysObjectID'])) {
// Use sysObjectID from device array
$sysObjectID = $device['sysObjectID'];
$set_cache = TRUE;
} else {
// Just random non empty string
$sysObjectID = 'empty_sysObjectID_3948ffakc';
$set_cache = TRUE;
}
if ($set_cache && (!is_numeric($device['device_id']) || defined('__PHPUNIT_PHAR__'))) {
// Do not set cache for unknown device_id (not added device) or phpunit
$set_cache = FALSE;
}
// Exactly sysObjectID match
if (isset($models[$sysObjectID])) {
// Check by device params, ie os version (see Juniper EX model definitions)
if (isset($models[$sysObjectID]['test'])) {
// Single associated array with test condition
$models[$sysObjectID] = [ $models[$sysObjectID] ];
}
if (!is_array_assoc($models[$sysObjectID])) {
// For cases when multiple test conditions for same sysObjectID
$model = FALSE;
foreach ($models[$sysObjectID] as $def) {
if (discovery_check_requires($device, $def, $device, 'device')) { continue; }
$model = $def;
break;
}
$models[$sysObjectID] = $model;
}
if ($set_cache) {
$cache['devices']['model'][$device['device_id']] = $models[$sysObjectID];
}
//r($models[$sysObjectID]);
return $models[$sysObjectID];
}
// Resort sysObjectID array by oids with from high to low order!
//krsort($config['model'][$model]);
uksort($config['model'][$model], 'compare_numeric_oids_reverse');
foreach ($config['model'][$model] as $key => $entry) {
if (strpos($sysObjectID, $key) === 0) {
// Check by device params, ie os version (see Juniper EX model definitions)
if (isset($entry['test'])) {
// Single associated array with test condition
$entry = [ $entry ];
}
if (!is_array_assoc($entry)) {
// For cases when multiple test conditions for same sysObjectID
$model = FALSE;
foreach ($entry as $def) {
if (discovery_check_requires($device, $def, $device, 'device')) { continue; }
$model = $def;
break;
}
$entry = $model;
}
if ($set_cache) {
$cache['devices']['model'][$device['device_id']] = $entry;
}
return $entry;
break;
}
}
// If model array not found, set cache entry to FALSE,
// for do not search again
if ($set_cache) {
$cache['devices']['model'][$device['device_id']] = FALSE;
}
}
return FALSE;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function formatRates($value, $round = 2, $sf = 3)
{
$value = format_si($value, $round, $sf) . "bps";
return $value;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function formatStorage($value, $round = 2, $sf = 3)
{
$value = format_bi($value, $round, $sf) . 'B';
return $value;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function format_si($value, $round = 2, $sf = 3) {
if (!is_numeric($value)) {
print_debug("Passed incorrect value to ".__FUNCTION__."()");
print_debug_vars($value);
//return FALSE;
return '0'; // incorrect, but for keep compatibility
}
if ($value < 0) {
$neg = TRUE;
$value *= -1;
} else {
$neg = FALSE;
}
// https://physics.nist.gov/cuu/Units/prefixes.html
if ($value >= 0.1) {
$sizes = [ '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
$ext = $sizes[0];
for ($i = 1; (($i < count($sizes)) && ($value >= 1000)); $i++) {
$value /= 1000;
$ext = $sizes[$i];
}
} else {
$sizes = [ '', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y' ];
$ext = $sizes[0];
for ($i = 1; (($i < count($sizes)) && ($value != 0) && ($value <= 0.1)); $i++) {
$value *= 1000;
$ext = $sizes[$i];
}
}
if ($neg) { $value *= -1; }
//print_warning("$value " . round($value, $round));
return format_number_short(round($value, $round), $sf).$ext;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function format_bi($value, $round = 2, $sf = 3) {
if (!is_numeric($value)) {
print_debug("Passed incorrect value to ".__FUNCTION__."()");
print_debug_vars($value);
//return FALSE;
return '0'; // incorrect, but for keep compatibility
}
if ($value < 0) {
$neg = TRUE;
$value *= -1;
} else {
$neg = FALSE;
}
$sizes = [ '', 'k', 'M', 'G', 'T', 'P', 'E' ];
$ext = $sizes[0];
for ($i = 1; (($i < count($sizes)) && ($value >= 1024)); $i++) {
$value /= 1024;
$ext = $sizes[$i];
}
if ($neg) { $value *= -1; }
return format_number_short(round($value, $round), $sf).$ext;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function format_number($value, $base = '1000', $round = 2, $sf = 3)
{
if ($base == '1000')
{
return format_si($value, $round, $sf);
} else {
return format_bi($value, $round, $sf);
}
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function format_value($value, $format = '', $round = 2, $sf = 3) {
switch (strtolower((string)$format)) {
case 'si':
case '1000':
$value = format_si($value, $round, $sf);
break;
case 'bi':
case '1024':
$value = format_bi($value, $round, $sf);
break;
case 'shorttime':
$value = format_uptime($value, 'short');
break;
case 'uptime':
case 'time':
$value = format_uptime($value);
break;
default:
if (is_numeric($value)) {
$orig = $value;
$value = sprintf("%01.{$round}f", $value);
if (abs($orig) > 0 && preg_match('/^\-?0\.0+$/', $value)) {
// prevent show small values as zero
// ie 0.000627 as 0.00
//r($orig);
//r($value);
$value = format_number_short($orig, $sf);
//r($value);
} else {
$value = preg_replace(array( '/\.0+$/', '/(\.\d)0+$/' ), '\1', $value);
}
}
elseif ($value === TRUE) { $value = 'TRUE'; }
elseif ($value === FALSE) { $value = 'FALSE'; }
elseif (is_null($value)) { $value = 'NULL'; }
elseif ($value === '') { $value = '""'; }
}
return $value;
}
/**
* Is Valid Hostname
*
* See: http://stackoverflow.com/a/4694816
* http://stackoverflow.com/a/2183140
*
* The Internet standards (Request for Comments) for protocols mandate that
* component hostname labels may contain only the ASCII letters 'a' through 'z'
* (in a case-insensitive manner), the digits '0' through '9', and the hyphen
* ('-'). The original specification of hostnames in RFC 952, mandated that
* labels could not start with a digit or with a hyphen, and must not end with
* a hyphen. However, a subsequent specification (RFC 1123) permitted hostname
* labels to start with digits. No other symbols, punctuation characters, or
* white space are permitted. While a hostname may not contain other characters,
* such as the underscore character (_), other DNS names may contain the underscore
*
* @param string $hostname
* @param bool $fqdn
*
* @return bool
*/
function is_valid_hostname($hostname, $fqdn = FALSE)
{
// Pre check if hostname is FQDN
if ($fqdn && !preg_match('/\.(xn\-\-[a-z0-9]{2,}|[a-z]{2,})$/i', $hostname))
{
return FALSE;
}
return (preg_match("/^(_?[a-z\d](-*[_a-z\d])*)(\.(_?[a-z\d](-*[_a-z\d])*))*$/i", $hostname) // valid chars check
&& preg_match("/^.{1,253}$/", $hostname) // overall length check
&& preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $hostname)); // length of each label
}
/**
* Correct alternative for is_int(), is_integer() and ctype_digit() for validate integer numbers.
* Work with string and numbers.
*
* @param string|mixed $value
*
* @return bool
*/
function is_intnum($value) {
if (!is_numeric($value)) { return FALSE; }
$value = (string)$value;
if ($value[0] === '-') { $value = substr($value, 1); } // negative number
return ctype_digit($value);
}
/**
* Validate returned values for common parameters like hardware/version/serial/location.
*
* @param string $string
* @param string $type
*
* @return bool
*/
function is_valid_param($string, $type = '') {
// Empty or not string is invalid
if (!(is_string($string) || is_numeric($string)) || safe_empty($string)) {
print_debug("Detected empty value for param '$type'.");
return FALSE;
}
// --, **, .., **--.--**
$poor_default_pattern = '/^[\*\.\-]+$/';
switch (strtolower($type)) {
case 'asset_tag':
case 'serial':
$valid = ctype_print($string) &&
!(str_istarts($string, [ 'Not Avail', 'Not Specified', 'To be filled by O.E.M.' ]) ||
str_contains_array($string, [ 'denied' ]) || preg_match($poor_default_pattern, $string) ||
in_array($string, [ '<EMPTY>', 'empty', 'n/a', 'N/A', '1234567890', '0123456789',
'No Asset Tag', 'Tag 12345', 'sim', 'Unknown' ], TRUE));
break;
case 'location':
case 'syslocation':
$poor_locations_pattern = 'unknown|private|none|office|location|snmplocation|Sitting on the Dock of the Bay|Not Available';
$valid = strlen($string) > 4 && !preg_match('/^[<\\\(]?('.$poor_locations_pattern.')[>\\\)]?$/i', $string);
break;
case 'contact':
case 'syscontact':
$valid = !(in_array($string, [ 'Uninitialized', 'not set', '<none>', '(none)', 'SNMPv2', 'Unknown', '?', '<private>' ]) ||
preg_match($poor_default_pattern, $string));
break;
case 'sysobjectid':
$valid = preg_match('/^\.?\d+(\.\d+)*$/', $string);
break;
case 'type':
$valid = array_key_exists($string, (array)$GLOBALS['config']['devicetypes']);
break;
case 'port':
case 'snmp_port':
// port 0 also valid, but we exclude because it reserved
$valid = is_intnum($string) && $string > 0 && $string <= 65353;
break;
case 'path':
// https://stackoverflow.com/questions/6416065/c-sharp-regex-for-file-paths-e-g-c-test-test-exe/42036026#42036026
$unix_pattern = '!^\/$|(^(?=\/)|^\.|^\.\.)(\/(?=[^/\0\n\r])[^/\0\n\r]+)*\/?$!';
$valid = preg_match($unix_pattern, $string);
if (!$valid) {
$windows_pattern = '@(^([a-z]|[A-Z]):(?=\\(?![\0-\37<>:"/\\|?*])|\/(?![\0-\37<>:"/\\|?*])|$)|^\\(?=[\\\/][^\0-\37<>:"/\\|?*]+)|^(?=(\\|\/)$)|^\.(?=(\\|\/)$)|^\.\.(?=(\\|\/)$)|^(?=(\\|\/)[^\0-\37<>:"/\\|?*]+)|^\.(?=(\\|\/)[^\0-\37<>:"/\\|?*]+)|^\.\.(?=(\\|\/)[^\0-\37<>:"/\\|?*]+))((\\|\/)[^\0-\37<>:"/\\|?*]+|(\\|\/)$)*()$@';
$valid = preg_match($windows_pattern, $string);
}
break;
case 'posix_username':
// strict posix (https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users):
// ^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$
$valid = preg_match('/^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$/', $string);
break;
case 'username':
// posix 32 chars
// windows/ldap 20 chars
// pre-windows2000 256 chars
$valid = strlen($string) <= 256 && preg_match('/^\w[\w\-\\\\]+\$?$/u', $string); // allow utf8 usernames
break;
case 'password':
$valid = preg_match('/^[[:print:]]+$/u', $string); // allow any printable utf8
break;
case 'snmp_community':
$valid = preg_match('/^[\w#@=:\(\)\ \-\.\/]{1,32}$/', $string);
break;
case 'snmp_timeout':
$valid = is_intnum($string) && $string > 0 && $string <= 120;
break;
case 'snmp_retries':
$valid = is_intnum($string) && $string > 0 && $string <= 10;
break;
case 'snmp_authalgo':
// MD5|SHA|SHA-224|SHA-256|SHA-384|SHA-512
$valid = preg_match('/^(md5|sha(\-?(224|256|384|512))?)$/i', $string);
break;
case 'snmp_cryptoalgo':
// DES|AES|AES-192|AES-192-C|AES-256|AES-256-C
$valid = preg_match('/^(des|aes(\-?(192|256)(\-?c)?)?)$/i', $string);
break;
default:
// --, **, .., **--.--**
$valid = !preg_match($poor_default_pattern, $string);
}
if (!$valid) {
print_debug("Detected invalid value '$string' for param '$type'.");
}
return $valid;
}
/**
* BOOLEAN safe function to check if hostname resolves as IPv4 or IPv6 address
*
* @param string $hostname
* @param int $flags
*
* @return bool
*/
function is_domain_resolves($hostname, $flags = OBS_DNS_ALL) {
return (is_valid_hostname($hostname) && gethostbyname6($hostname, $flags));
}
/**
* Get $host record from /etc/hosts
*
* @param string $host
* @param int $flags
*
* @return false|mixed|string|null
*/
function ip_from_hosts($host, $flags = OBS_DNS_ALL) {
$host = strtolower($host);
try {
foreach (new SplFileObject('/etc/hosts') as $line) {
// skip empty and comments
if (str_contains($line, '#')) {
// remove inline comments
list($line,) = explode('#', $line, 2);
}
$line = trim($line);
if (safe_empty($line)) { continue; }
$hosts = preg_split('/\s/', strtolower($line), -1, PREG_SPLIT_NO_EMPTY);
//print_debug_vars($hosts);
$ip = array_shift($hosts);
//$hosts = array_map('strtolower', $d);
if (in_array($host, $hosts, TRUE)) {
if ((is_flag_set(OBS_DNS_A, $flags) && str_contains($ip, '.')) ||
(is_flag_set(OBS_DNS_AAAA, $flags) && str_contains($ip, ':'))) {
print_debug("Host '$host' found in hosts: $ip");
return $ip;
}
}
}
} catch (Exception $e) {
print_warning("Could not open the file /etc/hosts! This file should be world readable, also check that SELinux is not in enforcing mode.");
}
return FALSE;
}
/**
* Same as gethostbyname(), but work with both IPv4 and IPv6.
* Get the IPv4 or IPv6 address corresponding to a given Internet hostname.
* By default, return IPv4 address (A record) if exist,
* else IPv6 address (AAAA record) if exist.
* For get only IPv6 record use gethostbyname6($hostname, OBS_DNS_AAAA).
*
* @param string $host
* @param int $flags
*
* @return false|mixed
*/
function gethostbyname6($host, $flags = OBS_DNS_ALL) {
// get AAAA record for $host
// if flag OBS_DNS_A is set, if AAAA fails, it tries for A
// the first match found is returned
// otherwise returns FALSE
$flags |= OBS_DNS_FIRST; // Set return only first found dns record (do not request all A/AAAA/hosts records)
$dns = gethostbynamel6($host, $flags);
if (safe_count($dns)) {
return array_shift($dns);
}
return FALSE;
}
/**
* Same as gethostbynamel(), but work with both IPv4 and IPv6.
* By default, returns both IPv4/6 addresses (A and AAAA records),
* for get only IPv6 addresses use gethostbynamel6($hostname, OBS_DNS_AAAA).
*
* @param string $host
* @param int $flags
*
* @return array|false
*/
function gethostbynamel6($host, $flags = OBS_DNS_ALL) {
// get AAAA records for $host,
// if $try_a is true, if AAAA fails, it tries for A
// results are returned in an array of ips found matching type
// otherwise returns FALSE
$ip6 = [];
$ip4 = [];
$try_a = is_flag_set(OBS_DNS_A, $flags);
$try_aaaa = is_flag_set(OBS_DNS_AAAA, $flags);
$first = is_flag_set(OBS_DNS_FIRST, $flags); // Return first found record, when flag set
if ($try_a === TRUE) {
// First try /etc/hosts (v4)
$etc4 = ip_from_hosts($host, OBS_DNS_A);
if ($etc4) {
$ip4[] = $etc4;
if ($first) { return $ip4; }
}
// Second try /etc/hosts (v6)
$etc6 = $try_aaaa ? ip_from_hosts($host, OBS_DNS_AAAA) : FALSE;
if ($etc6) {
$ip6[] = $etc6;
if ($first) { return $ip6; }
}
// Separate A and AAAA queries, see: https://www.mail-archive.com/observium@observium.org/msg09239.html
$dns = dns_get_record($host, DNS_A);
print_debug_vars($dns);
if (!is_array($dns)) { $dns = []; }
// Request AAAA record (when requested only first record and A record exist, skip)
if ($try_aaaa && !($first && count($dns))) {
$dns6 = dns_get_record($host, DNS_AAAA);
print_debug_vars($dns6);
if (is_array($dns6)) {
$dns = array_merge($dns, $dns6);
}
}
} elseif ($try_aaaa) {
// First try /etc/hosts (v6)
$etc6 = ip_from_hosts($host, OBS_DNS_AAAA);
if ($etc6) {
$ip6[] = $etc6;
if ($first) { return $ip6; }
}
$dns = dns_get_record($host, DNS_AAAA);
print_debug_vars($dns);
} else {
// Not A or AAAA record requested
return FALSE;
}
foreach ($dns as $record) {
switch ($record['type']) {
case 'A':
$ip4[] = $record['ip'];
break;
case 'AAAA':
$ip6[] = $record['ipv6'];
break;
}
}
if ($try_a && count($ip4)) {
// Merge ipv4 & ipv6
$ip6 = array_merge($ip4, $ip6);
}
if (count($ip6)) {
return $ip6;
}
return FALSE;
}
/**
* Get hostname by IP (both IPv4/IPv6)
* Return PTR or FALSE.
*
* @param string $ip
*
* @return string|false
*/
function gethostbyaddr6($ip) {
$ptr = FALSE;
$resolver = new Net_DNS2_Resolver();
try {
$response = $resolver->query($ip, 'PTR');
if ($response) {
$ptr = $response->answer[0]->ptrdname;
}
} catch (Net_DNS2_Exception $e) {}
return $ptr;
}
// DOCME needs phpdoc block
// TESTME needs unit testing
// CLEANME DEPRECATED
function add_service($device, $service, $descr)
{
$insert = array('device_id' => $device['device_id'], 'service_ip' => $device['hostname'], 'service_type' => $service,
'service_changed' => array('UNIX_TIMESTAMP(NOW())'), 'service_desc' => $descr, 'service_param' => "", 'service_ignore' => "0");
echo dbInsert($insert, 'services');
}
/**
* Request an http(s) url.
* Note. If first runtime request exit with timeout,
* than will be set constant OBS_HTTP_REQUEST as FALSE
* and all other requests will skipped with FALSE response!
*
* @param string $request Requested URL
* @param array $context Set additional HTTP context options, see http://php.net/manual/en/context.http.php
* @param int|boolean $rate_limit Rate limit per day for specified domain (in url). If FALSE no limits
* @global array $config
* @global array $GLOBALS['response_headers'] Response headers with keys:
* code (HTTP code status), status (HTTP status description) and all other
* @global boolean $GLOBALS['request_status'] TRUE if response code is 2xx or 3xx
*
* @return string|boolean Return response content or FALSE
*/
function get_http_request($request, $context = [], $rate_limit = FALSE) {
global $config;
$ok = TRUE;
if (defined('OBS_HTTP_REQUEST') && OBS_HTTP_REQUEST === FALSE) {
print_debug("HTTP requests skipped since previous request exit with timeout");
$ok = FALSE;
$GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
} elseif (!ini_get('allow_url_fopen')) {
print_debug('HTTP requests disabled, since PHP config option "allow_url_fopen" set to off. Please enable this option in your PHP config.');
$ok = FALSE;
$GLOBALS['response_headers'] = array('code' => 400, 'descr' => 'HTTP Method Disabled');
} elseif (str_istarts($request, 'https') && !check_extension_exists('openssl')) {
// Check if Secure requests allowed, but ssl extension not exist
print_debug(__FUNCTION__.'() wants to connect with https but https is not enabled on this server. Please check your PHP settings, the openssl extension must exist and be enabled.');
logfile(__FUNCTION__.'() wants to connect with https but https is not enabled on this server. Please check your PHP settings, the openssl extension must exist and be enabled.');
$ok = FALSE;
$GLOBALS['response_headers'] = array('code' => 400, 'descr' => 'HTTPS Method Disabled');
}
if ($ok && $rate_limit && is_numeric($rate_limit) && $rate_limit >= 0) {
// Check limit rates to this domain (per/day)
if (preg_match('/^https?:\/\/([\w\.]+[\w\-\.]*(:\d+)?)/i', $request, $matches)) {
$date = format_unixtime(get_time(), 'Y-m-d');
$domain = $matches[0]; // base domain (with http(s)): https://test-me.com/ -> https://test-me.com
$rate_db = safe_json_decode(get_obs_attrib('http_rate_' . $domain));
//print_vars($date); print_vars($rate_db);
if (is_array($rate_db) && isset($rate_db[$date])) {
$rate_count = $rate_db[$date];
} else {
$rate_count = 0;
}
$rate_count++;
set_obs_attrib('http_rate_' . $domain, safe_json_encode(array($date => $rate_count)));
if ($rate_count > $rate_limit) {
print_debug("HTTP requests skipped because the rate limit $rate_limit/day for domain '$domain' is exceeded (count: $rate_count)");
$GLOBALS['response_headers'] = array('code' => 429, 'descr' => 'Too Many Requests');
$ok = FALSE;
} elseif (OBS_DEBUG > 1) {
print_debug("HTTP rate count for domain '$domain': $rate_count ($rate_limit/day)");
}
} else {
$rate_limit = FALSE;
}
}
if (OBS_DEBUG > 0) {
$debug_request = $request;
if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) {
$debug_request = preg_replace('/&stats=.+/', '&stats=***', $debug_request);
}
$debug_msg = PHP_EOL . 'REQUEST[%y' . $debug_request . '%n]';
}
if (!$ok) {
if (OBS_DEBUG > 0) {
print_message($debug_msg . PHP_EOL .
'REQUEST STATUS[%rFALSE%n]' . PHP_EOL .
'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
}
// Set GLOBAL var $request_status for use as validate status of last responce
$GLOBALS['request_status'] = FALSE;
return FALSE;
}
$response = '';
// Add common http context
$opts = array('http' => generate_http_context_defaults($context));
// Force IPv4 or IPv6
if (isset($config['http_ip_version']))
{
$bindto = str_contains($config['http_ip_version'], '6') ? '[::]:0' : '0:0';
$opts['socket'] = [ 'bindto' => $bindto ];
//$opts['socket'] = [ 'bindto' => '0:0' ]; // IPv4
//$opts['socket'] = [ 'bindto' => '[::]:0' ]; // IPv6
}
// HTTPS
// if ($parse_url = parse_url($request))
// {
// if ($parse_url['scheme'] == 'https')
// {
// $opts['ssl'] = [ 'SNI_enabled' => TRUE, 'SNI_server_name' => $parse_url['host'] ];
// }
// }
// DEBUG, generate curl cmd for testing:
if (defined('OBS_DEBUG') && OBS_DEBUG) {
$curl_cmd = 'curl';
if (OBS_DEBUG > 1) {
// Show response headers
$curl_cmd .= ' -i';
}
if (isset($config['http_ip_version'])) {
$curl_cmd .= str_contains($config['http_ip_version'], '6') ? ' -6' : ' -4';
}
if (isset($opts['http']['timeout'])) {
$curl_cmd .= ' --connect-timeout '.$opts['http']['timeout'];
}
if (isset($opts['http']['method']))
{
$curl_cmd .= ' -X '.$opts['http']['method'];
}
if (isset($opts['http']['header'])) {
foreach (explode("\r\n", $opts['http']['header']) as $curl_header) {
if (!strlen($curl_header)) { continue; }
$curl_cmd .= ' -H \'' . $curl_header . '\'';
}
}
if (isset($opts['http']['content'])) {
$curl_cmd .= ' -d \''.$opts['http']['content'].'\'';
}
// Proxy
// -x, --proxy <[protocol://][user:password@]proxyhost[:port]>
// -U, --proxy-user <user:password>
if (isset($config['http_proxy']) && $config['http_proxy']) {
$http_proxy = $config['http_proxy'];
// Basic proxy auth
if (isset($config['proxy_user'], $config['proxy_password']) && $config['proxy_user']) {
$http_proxy = $config['proxy_user'].':'.$config['proxy_password'].'@'.$http_proxy;
}
$curl_cmd .= ' -x '.$http_proxy;
}
print_debug("HTTP CURL cmd:\n$curl_cmd $request");
}
// Process http request and calculate runtime
$start = utime();
$context = stream_context_create($opts);
$response = file_get_contents($request, FALSE, $context);
$end = utime();
$runtime = $end - $start;
// Request end unixtime and runtime
$GLOBALS['request_unixtime'] = $end;
$GLOBALS['request_runtime'] = $runtime;
// Parse response headers
// Note: $http_response_header - see: http://php.net/manual/en/reserved.variables.httpresponseheader.php
$head = [];
foreach ($http_response_header as $k => $v) {
$t = explode(':', $v, 2);
if (isset($t[1])) {
// Date: Sat, 12 Apr 2008 17:30:38 GMT
$head[trim($t[0])] = trim($t[1]);
} elseif (preg_match("!HTTP/([\d\.]+)\s+(\d+)(.*)!", $v, $matches)) {
// HTTP/1.1 200 OK
$head['http'] = $matches[1];
$head['code'] = (int)$matches[2];
$head['descr'] = trim($matches[3]);
} else {
$head[] = $v;
}
}
$GLOBALS['response_headers'] = $head;
// Set GLOBAL var $request_status for use as validate status of last responce
if (isset($head['code']) && ($head['code'] < 200 || $head['code'] >= 400)) {
$GLOBALS['request_status'] = FALSE;
} elseif ($response === FALSE) {
// An error in get response
$GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
$GLOBALS['request_status'] = FALSE;
} else {
// Valid statuses: 2xx Success, 3xx Redirection or head code not set (ie response not correctly parsed)
$GLOBALS['request_status'] = TRUE;
}
// Set OBS_HTTP_REQUEST for skip all other requests (FALSE for skip all other requests)
if (!defined('OBS_HTTP_REQUEST')) {
if ($response === FALSE && empty($http_response_header)) {
// Derp, no way for get proxy headers
if ($runtime < 1 &&
isset($config['http_proxy']) && $config['http_proxy'] &&
!(isset($config['proxy_user']) || isset($config['proxy_password']))) {
$GLOBALS['response_headers'] = array('code' => 407, 'descr' => 'Proxy Authentication Required');
} else {
$GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
}
$GLOBALS['request_status'] = FALSE;
// Validate host from request and check if it timeout request
if (gethostbyname6(parse_url($request, PHP_URL_HOST))) {
// Timeout error, only if not received response headers
define('OBS_HTTP_REQUEST', FALSE);
print_debug(__FUNCTION__.'() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
logfile(__FUNCTION__.'() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
}
} else {
define('OBS_HTTP_REQUEST', TRUE);
}
}
// FIXME. what if first request fine, but second broken?
//else if ($response === FALSE)
//{
// if (function_exists('runkit_constant_redefine')) { runkit_constant_redefine('OBS_HTTP_REQUEST', FALSE); }
//}
if (defined('OBS_DEBUG') && OBS_DEBUG) {
// Hide extended stats in normal debug level = 1
if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) { $request = preg_replace('/&stats=.+/', '&stats=***', $request); }
// Show debug info
print_message($debug_msg . PHP_EOL .
'REQUEST STATUS[' . ($GLOBALS['request_status'] ? '%gTRUE' : '%rFALSE') . '%n]' . PHP_EOL .
'REQUEST RUNTIME['.($runtime > 3 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL .
'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
if (OBS_DEBUG > 1) {
echo("RESPONSE[\n".$response."\n]");
print_vars($http_response_header);
print_vars($opts);
}
}
return $response;
}
/**
* Process HTTP request by definition array and process it for valid status.
* Used definition params in response key.
*
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param string $response Response from get_http_request()
* @return boolean Return TRUE if request processed with valid HTTP code (2xx, 3xx) and API response return valid param
*/
function test_http_request($def, $response) {
$response = trim($response);
if (is_string($def)) {
// Get transport definition for responses
$def = $GLOBALS['config']['transports'][$def]['notification'];
}
// Response is array (or xml)?
$is_response_array = strtolower($def['response_format']) === 'json';
// Set status by response status
$success = get_http_last_status();
// If response return valid code and content, additional parse for specific defined tests
if ($success) {
// Decode if request OK
if ($is_response_array) {
$response = safe_json_decode($response);
}
// else additional formats?
// Check if call succeeded
if (isset($def['response_test'])) {
// Convert single test condition to multi-level condition
if (isset($def['response_test']['operator'])) {
$def['response_test'] = array($def['response_test']);
}
// Compare all definition fields with response,
// if response param not equals to expected, set not success
// multilevel keys should written with '->' separator, ie: $a[key][some][0] - key->some->0
foreach ($def['response_test'] as $test) {
if ($is_response_array) {
$field = array_get_nested($response, $test['field']);
} else {
// RAW response
$field = $response;
}
if (test_condition($field, $test['operator'], $test['value']) === FALSE) {
print_debug("Response [".$field."] not valid: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
$success = FALSE;
break;
} else {
print_debug("Response [".$field."] valid: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
}
}
}
} elseif ($is_response_array && isset($def['response_fields']['message'], $def['response_fields']['status'])) {
// Decode response for useful error reports also for bad statuses
$response = safe_json_decode($response);
}
if (!$success) {
if (isset($def['response_fields']['message'], $def['response_fields']['status']) && is_array($response)) {
echo PHP_EOL;
if (isset($def['response_fields']['status'])) {
if ($def['response_fields']['status'] === 'raw') {
$status = get_http_last_code();
} else {
$status = array_get_nested($response, $def['response_fields']['status']);
}
if (OBS_DEBUG) {
print_message("%WRESPONSE STATUS%n[%r".$status."%n]", 'console');
}
}
$msg = array_get_nested($response, $def['response_fields']['message']);
if (isset($def['response_fields']['info']) &&
$info = array_get_nested($response, $def['response_fields']['info'])) {
$msg .= " ($info)";
}
if (OBS_DEBUG) { print_message("%WRESPONSE ERROR%n[%y".$msg ."%n]\n", 'console'); }
$GLOBALS['last_message'] = $msg;
} elseif (is_string($response) && $response && !get_http_last_status()) {
if (OBS_DEBUG) {
echo PHP_EOL;
print_message("%WRESPONSE STATUS%n[%r" . get_http_last_code() . "%n]", 'console');
print_message("%WRESPONSE ERROR%n[%y" . $response . "%n]\n", 'console');
}
$GLOBALS['last_message'] = $response;
}
}
print_debug_vars($response, 1);
return $success;
}
/**
* Return HTTP return code for last request by get_http_request()
*
* @return integer HTTP code
*/
function get_http_last_code() {
return $GLOBALS['response_headers']['code'];
}
/**
* Return HTTP return code for last request by get_http_request()
*
* @return boolean HTTP status TRUE if response code 2xx or 3xx
*/
function get_http_last_status() {
return $GLOBALS['request_status'];
}
/**
* Generate HTTP specific context with some defaults for proxy, timeout, user-agent.
* Used in get_http_request().
*
* @param array $context HTTP specified context, see http://php.net/manual/ru/function.stream-context-create.php
* @return array HTTP context array
*/
function generate_http_context_defaults($context = array())
{
global $config;
if (!is_array($context)) { $context = array(); } // Fix context if not array passed
// Defaults
if (!isset($context['timeout'])) {
$context['timeout'] = '15';
}
// HTTP/1.1
$context['protocol_version'] = 1.1;
// get the entire body of the response in case of error (HTTP/1.1 400, for example)
if (OBS_DEBUG) {
$context['ignore_errors'] = TRUE;
}
// User agent (required for some type of queries, ie geocoding)
if (!isset($context['header']))
{
$context['header'] = ''; // Avoid 'undefined index' when concatting below
}
$context['header'] .= 'User-Agent: ' . OBSERVIUM_PRODUCT . '/' . OBSERVIUM_VERSION . "\r\n";
if (isset($config['http_proxy']) && $config['http_proxy'])
{
$context['proxy'] = 'tcp://' . $config['http_proxy'];
$context['request_fulluri'] = isset($config['proxy_fulluri']) ? (bool)$config['proxy_fulluri'] : TRUE;
}
// Basic proxy auth
if (isset($config['proxy_user']) && $config['proxy_user'] && isset($config['proxy_password']))
{
$auth = base64_encode($config['proxy_user'].':'.$config['proxy_password']);
$context['header'] .= 'Proxy-Authorization: Basic ' . $auth . "\r\n";
}
print_debug_vars($context);
return $context;
}
/**
* Generate HTTP context based on passed params, tags and definition.
* This context will used in get_http_request_test() (or get_http_request())
*
* @global array $config
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param array $tags (optional) Contact array and other tags
* @param array $params (optional) Array of requested params with key => value entries (used with request method POST)
* @return array HTTP Context which can used in get_http_request_test() or get_http_request()
*/
function generate_http_context($def, $tags = [], $params = [])
{
global $config;
if (is_string($def)) {
// Get transport definition for requests
$def = $config['transports'][$def]['notification'];
}
$context = array(); // Init
// Request method POST/GET
if ($def['method']) {
$context['method'] = strtoupper($def['method']);
}
// Request timeout
if (is_intnum($def['timeout']) || $def['timeout'] >= 1 || $def['timeout'] <= 300) {
$context['timeout'] = $def['timeout'];
}
// Content and headers
$header = "Connection: close\r\n";
// Add encode $params for POST request inside http headers
if ($context['method'] === 'POST' || $context['method'] === 'PUT') {
if (isset($def['request_params_key'])) {
// Key based link to request params, see google-chat notification
$key = 'request_params_' . strtolower(array_tag_replace($tags, $def['request_params_key']));
if (isset($def[$key])) {
$request_params = &$def[$key];
} else {
$request_params = &$def['request_params'];
}
} else {
// Common default request_params
$request_params = &$def['request_params'];
}
// Generate request params
foreach ((array)$request_params as $param => $entry) {
// Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
if (is_array($entry)) {
// ie teams and pagerduty
$params[$param] = array_merge((array)$params[$param], array_tag_replace($tags, $entry));
} elseif (!isset($params[$param]) || $params[$param] === '') {
$params[$param] = array_tag_replace($tags, $entry);
}
// Clean empty params
if (safe_empty($params[$param])) { unset($params[$param]); }
}
if (strtolower($def['request_format']) === 'json') {
// Encode params as json string
$data = safe_json_encode($params);
$header .= "Content-Type: application/json; charset=utf-8\r\n";
} else {
// Encode params as url encoded string
$data = http_build_query($params);
// https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
//$header .= "Content-Type: multipart/form-data\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
}
$header .= "Content-Length: ".strlen($data)."\r\n";
// Encoded content data
$context['content'] = $data;
}
// Basic auth
if (isset($def['request_user'])) {
$basic_auth = $def['request_user'];
if (isset($def['request_password'])) {
$basic_auth .= ':' . $def['request_password'];
}
$basic_auth = array_tag_replace($tags, $basic_auth);
$header .= 'Authorization: Basic ' . \base64_encode($basic_auth) . "\r\n";
}
// Additional headers with contact params
foreach ($def['request_header'] as $entry) {
// Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
$header .= array_tag_replace($tags, $entry) . "\r\n";
}
$context['header'] = $header;
return $context;
}
/**
* Generate URL based on passed params, tags and definition.
* This context will used in get_http_request_test() (or get_http_request())
*
* @global array $config
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param array $tags (optional) Contact array, used only if transport required additional headers (ie hipchat)
* @param array $params (optional) Array of requested params with key => value entries (used with request method GET)
* @return string URL which can used in get_http_request_test() or get_http_request()
*/
function generate_http_url($def, $tags = array(), $params = array())
{
global $config;
if (is_string($def))
{
// Get definition for transport API
$def = $config['transports'][$def]['notification'];
}
$url = ''; // Init
// Append (if set $def['url_param']) or set hardcoded url for transport
if (isset($def['url'])) {
// Try to find all keys in URL like %bot_hash% matched with same key in $endpoint array
$url .= array_tag_replace($tags, $def['url']);
}
// Add GET params to url
if ($def['method'] === 'GET' || $def['method'] === 'DELETE') {
if (isset($def['request_params_key'])) {
// Key based link to request params, see google-chat notification
$key = 'request_params_' . strtolower(array_tag_replace($tags, $def['request_params_key']));
if (isset($def[$key])) {
$request_params = &$def[$key];
} else {
$request_params = &$def['request_params'];
}
} else {
// Common default request_params
$request_params = &$def['request_params'];
}
// Generate request params
foreach ((array)$request_params as $param => $entry) {
// Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
if (is_array($entry)) {
// ie teams and pagerduty
$params[$param] = array_merge((array)$params[$param], array_tag_replace($tags, $entry));
} elseif (!isset($params[$param]) || $params[$param] === '') {
$params[$param] = array_tag_replace($tags, $entry);
}
// Clean empty params
if (safe_empty($params[$param])) { unset($params[$param]); }
}
// Append params to url
if (count($params)) {
$data = http_build_query($params);
if (str_contains($url, '?')) {
// Append additional params to url string
$url .= '&' . $data;
} else {
// Add get params as first time
$url .= '?' . $data;
}
}
}
return $url;
}
/**
* Return named unix times from now
*
* @param string $str Named time from now, ie: fiveminute, twelvehour, year
* @param bool $future If TRUE, return unix time in future.
*
* @return int
*/
function get_time($str = 'now', $future = FALSE) {
global $config;
// Set some times needed by loads of scripts (it's dynamic, so we do it here!)
$config['time']['now'] = time();
/*
$config['time']['fiveminute'] = $config['time']['now'] - 300; //time() - (5 * 60);
$config['time']['fourhour'] = $config['time']['now'] - 14400; //time() - (4 * 60 * 60);
$config['time']['sixhour'] = $config['time']['now'] - 21600; //time() - (6 * 60 * 60);
$config['time']['twelvehour'] = $config['time']['now'] - 43200; //time() - (12 * 60 * 60);
$config['time']['day'] = $config['time']['now'] - 86400; //time() - (24 * 60 * 60);
$config['time']['twoday'] = $config['time']['now'] - 172800; //time() - (2 * 24 * 60 * 60);
$config['time']['week'] = $config['time']['now'] - 604800; //time() - (7 * 24 * 60 * 60);
$config['time']['twoweek'] = $config['time']['now'] - 1209600; //time() - (2 * 7 * 24 * 60 * 60);
$config['time']['month'] = $config['time']['now'] - 2678400; //time() - (31 * 24 * 60 * 60);
$config['time']['twomonth'] = $config['time']['now'] - 5356800; //time() - (2 * 31 * 24 * 60 * 60);
$config['time']['threemonth'] = $config['time']['now'] - 8035200; //time() - (3 * 31 * 24 * 60 * 60);
$config['time']['sixmonth'] = $config['time']['now'] - 16070400; //time() - (6 * 31 * 24 * 60 * 60);
$config['time']['year'] = $config['time']['now'] - 31536000; //time() - (365 * 24 * 60 * 60);
$config['time']['twoyear'] = $config['time']['now'] - 63072000; //time() - (2 * 365 * 24 * 60 * 60);
$config['time']['threeyear'] = $config['time']['now'] - 94608000; //time() - (3 * 365 * 24 * 60 * 60);
*/
$str = strtolower(trim($str));
if ($str === 'now' || empty($str)) {
return $config['time']['now'];
}
$time = $config['time']['now'];
$multipliers = [
'two' => 2, 'three' => 3, 'four' => 4,
'five' => 5, 'six' => 6, 'seven' => 7,
'eight' => 8, 'nine' => 9, 'ten' => 10,
'eleven' => 11, 'twelve' => 12
];
$times = [
'year' => 31536000,
'month' => 2678400,
'week' => 604800,
'day' => 86400,
'hour' => 3600,
'minute' => 60
];
$time_pattern = '/^(?<multiplier>' . implode('|', array_keys($multipliers)) . ')?(?<time>' . implode('|', array_keys($times)) . ')$/';
//$time_pattern = '/^(?<multiplier>two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)?(?<time>year|month|week|day|hour|minute)$/';
if (preg_match($time_pattern, $str, $matches)) {
$multiplier = isset($multipliers[$matches['multiplier']]) ? $multipliers[$matches['multiplier']] : 1;
$diff = $multiplier * $times[$matches['time']];
if ($future) {
$time += $diff;
} else {
$time -= $diff;
}
}
return (int)$time;
}
/**
* Format date string.
* If passed 'now' return formatted current datetime.
*
* This function convert date/time string to format from
* config option $config['timestamp_format'].
* If date/time not detected in string, function return original string.
* Example conversions to format 'd-m-Y H:i':
* '2012-04-18 14:25:01' -> '18-04-2012 14:25'
* 'Star wars' -> 'Star wars'
*
* @param string $str
*
* @return string
*/
// TESTME needs unit testing
function format_timestamp($str) {
global $config;
if ($str === 'now') {
// Use for get formatted current time
$timestamp = get_time($str);
} elseif (($timestamp = strtotime($str)) === FALSE) {
return $str;
}
return date($config['timestamp_format'], $timestamp);
}
/**
* Format unixtime.
*
* This function convert unixtime string to format from
* config option $config['timestamp_format'].
* Can take an optional format parameter, which is passed to date();
*
* @param string $time Unixtime in seconds since the Unix Epoch (also allowed microseconds)
* @param string $format Common date format
* @return string
*/
// TESTME needs unit testing
function format_unixtime($time, $format = NULL) {
list($sec, $usec) = explode('.', (string)$time);
if (!safe_empty($usec)) {
$date = date_create_from_format('U.u', number_format($time, 6, '.', ''));
} else {
$date = date_create_from_format('U', $sec);
$usec = 0;
}
// If something wrong with create data object, just return empty string (and yes, we never use zero unixtime)
if (!$date || $time == 0) { return ''; }
// Set correct timezone
$tz = get_timezone();
//r($tz);
try {
$date_timezone = new DateTimeZone(str_replace(':', '', $tz['php']));
//$date_timezone = new DateTimeZone($tz['php_name']);
$date->setTimeZone($date_timezone);
} catch(Throwable $throwable) {
print_debug($throwable->getMessage());
} catch(Exception $exception) {
print_debug($exception->getMessage());
}
//r($date);
if (safe_empty($format)) {
$format = $GLOBALS['config']['timestamp_format'];
}
$return = date_format($date, (string)$format);
if (PHP_VERSION_ID < 70300 && str_contains($return, 'v')) {
// The 'v' format specifier has been added in php 7.3
$msec = $usec ? substr(str_pad((string)$usec, 6, '0'), 0, 3) : '000';
//echo sprintf("%s -> %s\n", $usec, $msec);
return str_replace('v', $msec, $return);
}
return $return;
}
/**
* Reformat US-based dates to display based on $config['date_format']
*
* Supported input formats:
* DD/MM/YYYY
* DD/MM/YY
*
* Handling of YY -> YYYY years is passed on to PHP's strtotime, which
* is currently cut off at 1970/2069.
*
* @param string $date Erroneous date format
* @return string $date
*/
function reformat_us_date($date) {
global $config;
$date = trim($date);
if (preg_match('!^\d{1,2}/\d{1,2}/(\d{2}|\d{4})$!', $date)) {
// Only date
$format = $config['date_format'];
} elseif (preg_match('!^\d{1,2}/\d{1,2}/(\d{2}|\d{4})\s+\d{1,2}:\d{1,2}(:\d{1,2})?$!', $date)) {
// Date + time
$format = $config['timestamp_format'];
} else {
return $date;
}
return date($format, strtotime($date));
}
/**
* This function convert human written Uptime to seconds.
* Opposite function for format_uptime().
*
* Also applicable for some uptime formats in MIB, like EigrpUpTimeString:
* 'hh:mm:ss', reflecting hours, minutes, and seconds
* If the up time is greater than 24 hours, is less precise and
* the minutes and seconds are not reflected. Instead only the days
* and hours are shown and the string will be formatted like this: 'xxxdxxh'
*
* @param string $uptime Uptime in human readable string or timetick
* @return int Uptime in seconds
*/
function uptime_to_seconds($uptime) {
if (str_contains($uptime, 'Wrong Type')) {
// Wrong Type (should be Timeticks): 1632295600
$seconds = timeticks_to_sec($uptime);
} elseif (str_contains($uptime, ':') &&
!preg_match('/[a-zA-Z]/', $uptime)) { // exclude strings: 315 days18:50:04
$seconds = timeticks_to_sec($uptime);
} else {
$uptime = preg_replace('/^[a-z]+ */i', '', $uptime); // Clean "up" string
$seconds = age_to_seconds($uptime);
}
return $seconds;
}
/**
* Convert age string to seconds.
*
* This function convert age string to seconds.
* If age is numeric than it in seconds.
* The supplied age accepts values such as 31d, 240h, 1.5d etc.
* Accepted age scales are:
* y (years), M (months), w (weeks), d (days), h (hours), m (minutes), s (seconds).
* NOTE, for month use CAPITAL 'M'
* With wrong and negative returns 0
*
* '3y 4M 6w 5d 3h 1m 3s' -> 109191663
* '184 days 22 hrs 02 min 38 sec' ->
* '3y4M6w5d3h1m3s' -> 109191663
* '1.5w' -> 907200
* -886732 -> 0
* 'Star wars' -> 0
*
* @param string|int $age
* @return int
*/
// TESTME needs unit testing
function age_to_seconds($age) {
$age = trim($age);
if (is_numeric($age)) {
$age = (int)$age;
if ($age > 0) {
return $age;
}
return 0;
}
$pattern = '/^';
$pattern .= '(?:(?<years>\d+(?:\.\d)*)\ ?(?:[yY][eE][aA][rR][sS]?|[yY])[,\ ]*)*'; // y (years)
$pattern .= '(?:(?<months>\d+(?:\.\d)*)\ ?(?:[mM][oO][nN][tT][hH][sS]?|M)[,\ ]*)*'; // M (months)
$pattern .= '(?:(?<weeks>\d+(?:\.\d)*)\ ?(?:[wW][eE][eE][kK][sS]?|[wW])[,\ ]*)*'; // w (weeks)
$pattern .= '(?:(?<days>\d+(?:\.\d)*)\ ?(?:[dD][aA][yY][sS]?|[dD])[,\ ]*)*'; // d (days)
$pattern .= '(?:(?:';
$pattern .= '(?:(?<hours>\d+(?:\.\d)*)\ ?(?:[hH][oO][uU][rR][sS]?|[hH][rR][sS]|[hH])[,\ ]*)*'; // h (hours)
$pattern .= '(?:(?<minutes>\d+(?:\.\d)*)\ ?(?:[mM][iI][nN][uU][tT][eE][sS]?|[mM][iI][nN]|m)[,\ ]*)*'; // m (minutes)
$pattern .= '(?:(?<seconds>\d+(?:\.\d)*)\ ?(?:[sS][eE][cC][oO][nN][dD][sS]?|[sS][eE][cC]|[sS]))*'; // s (seconds)
$pattern .= '|(?:(?<hours>\d{1,2}):(?<minutes>\d{1,2}):(?<seconds>\d{1,2}(?:\.\d+)*))'; // hh:mm:ss.s
$pattern .= '))';
$pattern .= '$/J';
//print_vars($pattern); echo PHP_EOL;
if (!empty($age) && preg_match($pattern, $age, $matches)) {
$ages = [
'years' => 31536000, // year = 365 * 24 * 60 * 60
'months' => 2628000, // month = year / 12
'weeks' => 604800, // week = 7 days
'days' => 86400, // day = 24 * 60 * 60
'hours' => 3600, // hour = 60 * 60
'minutes' => 60 // minute = 60
];
$seconds = isset($matches['seconds']) ? (float)$matches['seconds'] : 0;
foreach ($ages as $period => $scale) {
if (isset($matches[$period])) {
$seconds += (float)$matches[$period] * $scale;
}
}
return (int)$seconds;
}
return 0;
}
/**
* Convert age string to unixtime.
*
* This function convert age string to unixtime.
*
* Description and notes same as for age_to_seconds()
*
* Additional check if $age more than minimal age in seconds
*
* '3y 4M 6w 5d 3h 1m 3s' -> time() - 109191663
* '3y4M6w5d3h1m3s' -> time() - 109191663
* '1.5w' -> time() - 907200
* -886732 -> 0
* 'Star wars' -> 0
*
* @param string|int $age
* @param string|int $min_age
*
* @return int
*/
// TESTME needs unit testing
function age_to_unixtime($age, $min_age = 1) {
$age = age_to_seconds($age);
if ($age >= $min_age) {
return time() - $age;
}
return 0;
}
/**
* Convert an variable to base64 encoded string
*
* This function converts any array or other variable to encoded string
* which can be used in urls.
* Can use serialize and json(default) methods.
*
* NOTE. In PHP < 5.4 json converts UTF-8 characters to Unicode escape sequences
* also json rounds float numbers (98172397.1234567890 ==> 98172397.123457)
*
* @param mixed $var
* @param string $method
* @return string
*/
function var_encode($var, $method = 'json')
{
switch ($method)
{
case 'serialize':
$string = base64_encode(serialize($var));
break;
default:
//$tmp = json_encode($var, OBS_JSON_ENCODE);
//echo PHP_EOL . 'precision = ' . ini_get('precision') . "\n";
//echo 'serialize_precision = ' . ini_get('serialize_precision');
//echo("\n---\n"); var_dump($var); echo("\n---\n"); var_dump($tmp);
$string = base64_encode(json_encode($var, OBS_JSON_ENCODE));
break;
}
return $string;
}
/**
* Decode an previously encoded string by var_encode() to original variable
*
* This function converts base64 encoded string to original variable.
* Can use serialize and json(default) methods.
* If json/serialize not detected returns original var
*
* NOTE. In PHP < 5.4 json converts UTF-8 characters to Unicode escape sequences,
* also json rounds float numbers (98172397.1234567890 ==> 98172397.123457)
*
* @param string $string
* @param string $method
*
* @return mixed
*/
function var_decode($string, $method = 'json') {
if (!is_string($string)) {
// Decode only string vars
return $string;
}
if ((strlen($string) % 4) > 0)
{
// BASE64 length must be multiple by 4
return $string;
}
$value = base64_decode($string, TRUE);
if ($value === FALSE)
{
// This is not base64 string, return original var
return $string;
}
switch ($method)
{
case 'serialize':
case 'unserialize':
if ($value === 'b:0;') { return FALSE; }
$decoded = safe_unserialize($value);
if ($decoded !== FALSE)
{
// Serialized encoded string detected
return $decoded;
}
break;
default:
if ($string === 'bnVsbA==') { return NULL; }
if (OBS_JSON_DECODE > 0)
{
$decoded = @json_decode($value, TRUE, 512, OBS_JSON_DECODE);
} else {
// Prevent to broke on old php (5.3), where supported only 3 params
$decoded = @json_decode($value, TRUE, 512);
}
switch (json_last_error())
{
case JSON_ERROR_STATE_MISMATCH:
case JSON_ERROR_SYNTAX:
// Critical json errors, return original string
break;
case JSON_ERROR_NONE:
default:
if ($decoded !== NULL)
{
// JSON encoded string detected
return $decoded;
}
}
break;
}
// In all other cases return original var
return $string;
}
/**
* Convert CSV like string to array or keep as is if not csv.
*
* @param mixed $str String probably CSV list or encoded.
* @param bool $encoded If TRUE and string is encoded (by var_encode()) decode it
*
* @return array|mixed
*/
function get_var_csv($str, $encoded = FALSE)
{
if (is_string($str))
{
// Better to understand quoted vars
$values = str_getcsv($str);
if (count($values) === 1)
{
// not comma list, but can be quoted value
$values = $values[0];
// Try decode var if original value not csv
if ($encoded && $str === $values)
{
$values = var_decode($str);
} else {
$values = var_comma_safe($values);
}
} else {
$values = var_comma_safe($values);
}
} else {
// If variable already array, keep as is
$values = $str;
}
return $values;
}
function var_comma_safe($value)
{
if (is_array($value))
{
foreach ($value as &$entry)
{
$entry = var_comma_safe($entry);
}
return $value;
}
if (strpos($value, '%1F') !== FALSE)
{
$value = str_replace('%1F', ',', $value); // %1F (US, unit separator) - not defined in HTML 4 standard
}
return $value;
}
/**
* Parse number with units to numeric.
*
* This function converts numbers with units (e.g. 100MB) to their value
* in bytes (e.g. 104857600).
*
* @param string $str
* @param int Use custom rigid unit base (1000 or 1024)
* @return int
*/
function unit_string_to_numeric($str, $unit_base = NULL)
{
$value = is_string($str) ? trim($str) : $str;
// If it's already a number, return original value
if (is_numeric($value)) { return (float)$value; }
// Any not numeric values return as is (array, booleans)
if (!is_string($value)) { return $str; }
//preg_match('/(\d+\.?\d*)\ ?(\w+)/', $str, $matches);
$pattern = '/^(?<number>\d+\.?\d*)\ ?(?<prefix>[kmgtpezy]i?)?(?<unit>[a-z]*)$/i';
preg_match($pattern, $value, $matches);
// Error, return original string
if (!is_numeric($matches['number'])) { return $str; }
// Unit base 1000 or 1024
$prefix_len = strlen($matches['prefix']);
if (in_array($unit_base, [ 1000, 1024 ]))
{
// Use rigid unit base, this interprets any units with hard multiplier base
$base = $unit_base;
}
elseif ($prefix_len === 2)
{
// IEC prefixes Ki, Gi, Ti, etc
$base = 1024;
} else {
switch ($matches['unit'])
{
case '':
case 'B':
case 'Byte':
case 'byte':
$base = 1024;
break;
case 'b':
case 'Bps':
case 'bit':
case 'bps':
$base = 1000;
break;
default:
$base = 1024;
}
}
// https://en.wikipedia.org/wiki/Binary_prefix
$prefixes = [ //'b' => 0,
'k' => 1, 'ki' => 1,
'm' => 2, 'mi' => 2,
'g' => 3, 'gi' => 3,
't' => 4, 'ti' => 4,
'p' => 5, 'pi' => 5,
'e' => 6, 'ei' => 6,
'z' => 7, 'zi' => 7,
'y' => 8, 'yi' => 8 ];
$power = 0;
if ($prefix_len)
{
$prefix = strtolower($matches['prefix']);
if (isset($prefixes[$prefix]))
{
$power = $prefixes[$prefix];
} else {
// incorrect prefixes, return original value
return $str;
}
}
switch ($matches['unit'])
{
case '':
case 'B':
case 'Byte':
case 'Bytes':
case 'byte':
case 'bytes':
$base = isset($base) ? $base : 1024;
break;
case 'b':
case 'Bps':
case 'bit':
case 'bits':
case 'bps':
$base = isset($base) ? $base : 1000;
break;
default:
// unknown unit, return original value
return $str;
}
$multiplier = pow($base, $power);
return (float)($matches[1] * $multiplier);
}
/**
* Generate Unique ID from string, based on crc32b hash. This ID unique for specific string and not changed over next call.
*
* @param string $string String
* @return int Unique ID
*/
function string_to_id($string)
{
return hexdec(hash("crc32b", $string));
}
/**
* Convert value of sensor from known unit to defined SI unit (used in poller/discovery)
*
* @param float|string $value Value in non standard unit
* @param string $unit Unit name/symbol
* @param string|null $type Type of value (optional, if same unit can used for multiple types)
*
* @return float|string Value converted to standard (SI) unit
*/
function value_to_si($value, $unit, $type = NULL) {
if (!is_numeric($value)) { return $value; } // Just return original value if not numeric
$unit_lower = strtolower($unit);
$case_units = [
'c' => 'C',
'celsius' => 'C',
'f' => 'F',
'fahrenheit' => 'F',
'k' => 'K',
'kelvin' => 'K',
'w' => 'W',
'watts' => 'W',
'dbm' => 'dBm',
'mpsi' => 'Mpsi',
'mmhg' => 'mmHg',
'inhg' => 'inHg',
];
// set correct unit case (required for external lib)
if (isset($case_units[$unit_lower])) {
$unit = $case_units[$unit_lower];
}
switch ($unit_lower) {
case 'f':
case 'fahrenheit':
case 'k':
case 'kelvin':
try {
$tmp = \PhpUnitsOfMeasure\PhysicalQuantity\Temperature::getUnit($unit);
} catch(Throwable $e) {
//PHP 7+
$unit = $unit_lower;
} catch (Exception $e) {
// PHP 5.x
$unit = $unit_lower;
}
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Temperature($value, $unit);
$si_value = $value_from->toUnit('C');
if ($si_value < -273.15) {
// Physically incorrect value
$si_value = FALSE;
}
$type = 'temperature';
$from = $value . " $unit";
$to = $si_value . ' Celsius';
break;
case 'c':
case 'celsius':
// not convert, just keep correct value
$type = 'temperature';
break;
case 'w':
case 'watts':
if ($type === 'dbm') {
// Used when Power convert to dBm
// https://en.wikipedia.org/wiki/DBm
// https://www.everythingrf.com/rf-calculators/watt-to-dbm
if ($value > 0) {
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, 'W');
$si_value = $value_from->toUnit('dBm');
$from = $value . " $unit";
$to = $si_value . ' dBm';
} elseif (strlen($value) && $value == 0) {
// See: https://jira.observium.org/browse/OBS-3200
$si_value = -99; // This is incorrect, but minimum possible value for dBm
$from = $value . ' W';
$to = $si_value . ' dBm';
} else {
$si_value = FALSE;
$from = $value . ' W';
$to = 'FALSE';
}
} else {
// not convert, just keep correct value
$type = 'power';
}
break;
case 'dbm':
if ($type === 'power') {
// Used when Power convert to dBm
// https://en.wikipedia.org/wiki/DBm
// https://www.everythingrf.com/rf-calculators/dbm-to-watts
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, $unit);
$si_value = $value_from->toUnit('W');
$from = $value . " $unit";
$to = $si_value . ' W';
} else {
// not convert, just keep correct value
$type = 'dbm';
}
break;
case 'psi':
case 'ksi':
case 'mpsi':
case 'mmhg':
case 'inhg':
case 'bar':
case 'atm':
// https://en.wikipedia.org/wiki/Pounds_per_square_inch
try {
$tmp = \PhpUnitsOfMeasure\PhysicalQuantity\Pressure::getUnit($unit);
} catch(Throwable $e) {
//PHP 7+
$unit = $unit_lower;
} catch (Exception $e) {
// PHP 5.x
$unit = $unit_lower;
}
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Pressure($value, $unit);
$si_value = $value_from->toUnit('Pa');
$type = 'pressure';
$from = $value . " $unit";
$to = $si_value . ' Pa';
break;
case 'ft/s':
case 'fps':
case 'ft/min':
case 'fpm':
case 'lfm': // linear feet per minute
case 'mph': // Miles per hour
case 'mps': // Miles per second
case 'm/min': // Meter per minute
case 'km/h': // Kilometer per hour
try {
$tmp = \PhpUnitsOfMeasure\PhysicalQuantity\Velocity::getUnit($unit);
} catch(Throwable $e) {
//PHP 7+
$unit = $unit_lower;
} catch (Exception $e) {
// PHP 5.x
$unit = $unit_lower;
}
// Any velocity units:
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Velocity($value, $unit);
$si_value = $value_from->toUnit('m/s');
$type = 'velocity';
$from = $value . " $unit";
$to = $si_value . ' m/s';
break;
case 'ft3/s':
case 'cfs':
case 'ft3/min':
case 'cfm':
case 'gpd': // US (gallon per day)
case 'gpm': // US (gallon per min)
case 'l/min':
case 'lpm':
case 'cmh':
case 'm3/h':
case 'cmm':
case 'm3/min':
try {
$tmp = \PhpUnitsOfMeasure\PhysicalQuantity\VolumeFlow::getUnit($unit);
} catch(Throwable $e) {
//PHP 7+
$unit = $unit_lower;
} catch (Exception $e) {
// PHP 5.x
$unit = $unit_lower;
}
if ($type === 'waterflow') {
// Waterflow default unit is L/s
$si_unit = 'L/s';
} elseif ($type === 'airflow') {
// Use for Airflow imperial unit CFM (Cubic foot per minute) as more common industry standard
$si_unit = 'CFM';
} else {
// For future
$si_unit = 'm^3/s';
}
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\VolumeFlow($value, $unit);
$si_value = $value_from->toUnit($si_unit);
$from = $value . " $unit";
$to = $si_value . " $si_unit";
break;
default:
// Ability to use any custom function to convert value based on unit name
$function_name = 'value_unit_'.$unit_lower; // ie: value_unit_ekinops_dbm1($value) or value_unit_accuenergy($value)
if (function_exists($function_name)) {
$si_value = call_user_func_array($function_name, array($value));
//$type = $unit;
$from = $value . " $unit";
$to = $si_value;
} elseif ($type === 'pressure' && str_ends($unit_lower, [ 'pa', 'si' ])) {
// Any of pressure unit, like hPa
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Pressure($value, $unit);
$si_value = $value_from->toUnit('Pa');
$from = $value . " $unit";
$to = $si_value . ' Pa';
}
}
if (isset($si_value)) {
print_debug('Converted '.strtoupper($type).' value: '.$from.' -> '.$to);
return $si_value;
}
return $value; // Fallback original value
}
/**
* Convert value of sensor from known unit to defined SI unit (used in poller/discovery)
*
* @param float|string $value Value
* @param string $unit_from Unit name/symbol for value
* @param string $class Type of value
* @param string|array $unit_to Unit name/symbol for convert value (by default used sensor class default unit)
* @return array Array with values converted to unit_from
*/
function value_to_units($value, $unit_from, $class, $unit_to = [])
{
global $config;
// Convert symbols to supported by lib units
$unit_from = str_replace(['<sup>', '</sup>'], ['^', ''], $unit_from); // I.e. mg/m<sup>3</sup> => mg/m^3
$unit_from = html_entity_decode($unit_from); // I.e. &deg;C => °C
// Non numeric values
if (!is_numeric($value))
{
return [$unit_from => $value];
}
switch ($class)
{
case 'temperature':
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Temperature($value, $unit_from);
break;
case 'pressure':
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Pressure($value, $unit_from);
break;
case 'power':
case 'dbm':
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, $unit_from);
break;
case 'waterflow':
case 'airflow':
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\VolumeFlow($value, $unit_from);
break;
case 'velocity':
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Velocity($value, $unit_from);
break;
case 'lifetime':
case 'uptime':
case 'time':
if ($unit_from == '') { $unit_from = 's'; }
$value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Time($value, $unit_from);
break;
default:
// Unknown, return original value
return [ $unit_from => $value ];
}
// Use our default unit (if not passed)
if (empty($unit_to) && isset($config['sensor_types'][$class]['symbol']))
{
$unit_to = $config['sensor_types'][$class]['symbol'];
}
// Convert to units
$units = [];
foreach ((array)$unit_to as $to)
{
// Convert symbols to supported by lib units
$tou = str_replace(['<sup>', '</sup>'], ['^', ''], $to); // I.e. mg/m<sup>3</sup> => mg/m^3
$tou = html_entity_decode($tou); // I.e. &deg;C => °C
$units[$to] = $value_from->toUnit($tou);
}
return $units;
}
/**
* Replace all newlines in string to space char (except string begin and end)
*
* @param string $string Input string
* @return string Output string without NL characters
*/
function nl2space($string)
{
if (!is_string($string) || $string == '')
{
return $string;
}
$string = trim($string, "\n\r");
return preg_replace('/ ?(\r\n|\r|\n) ?/', ' ', $string);
}
/**
* This noob function replace windows/mac newline character to unix newline
*
* @param string $string Input string
* @return string Clean output string
*/
function nl2nl($string)
{
if (!is_string($string) || $string == '')
{
return $string;
}
return preg_replace('/\r\n|\r/', PHP_EOL, $string);
}
/**
* Microtime
*
* This function returns the current Unix timestamp seconds, accurate to the
* nearest microsecond.
*
* @return float
*/
function utime()
{
return microtime(TRUE);
}
/**
* Bitwise checking if flags set
*
* Examples:
* if (is_flag_set(FLAG_A, some_var)) // eg: some_var = 0b01100000000010
* if (is_flag_set(FLAG_A | FLAG_F | FLAG_L, some_var)) // to check if at least one flag is set
* if (is_flag_set(FLAG_A | FLAG_J | FLAG_M | FLAG_D, some_var, TRUE)) // to check if all flags are set
*
* @param int $flag Checked flags
* @param int $param Parameter for checking
* @param bool $all Check all flags
* @return bool
*/
function is_flag_set($flag, $param, $all = FALSE)
{
$set = $flag & $param;
if ($set and !$all) { return TRUE; } // at least one of the flags passed is set
elseif ($all and ($set == $flag)) { return TRUE; } // to check that all flags are set
return FALSE;
}
/**
* Checks if a string is composed solely of lower case letters.
* Primarily used to sanitise strings used for file inclusion
*
* @param $string
* @return false|int
*/
function is_alpha($string) {
return preg_match(OBS_PATTERN_ALPHA, $string);
}
// DOCME needs phpdoc block
// TESTME needs unit testing
function is_ssl() {
if (isset($_SERVER['HTTPS'])) {
if ('on' === strtolower($_SERVER['HTTPS']) || '1' === $_SERVER['HTTPS']) {
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']) && ('443' == $_SERVER['SERVER_PORT'])) {
return TRUE;
}
//elseif (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') { return TRUE; }
return FALSE;
}
/**
* Convert "smart quotes" to real quotes and emdash into a hyphen.
*
* @url https://stackoverflow.com/questions/1262038/how-to-replace-microsoft-encoded-quotes-in-php
* @param string $string
*
* @return string
*/
function smart_quotes($string) {
if (!is_string($string)) { return $string; }
$quotes = [
"\xC2\xAB" => '"', // « (U+00AB) in UTF-8
"\xC2\xBB" => '"', // » (U+00BB) in UTF-8
"\xE2\x80\x98" => "'", // (U+2018) in UTF-8
"\xE2\x80\x99" => "'", // (U+2019) in UTF-8
"\xE2\x80\x9A" => "'", // (U+201A) in UTF-8
"\xE2\x80\x9B" => "'", // (U+201B) in UTF-8
"\xE2\x80\x9C" => '"', // “ (U+201C) in UTF-8
"\xE2\x80\x9D" => '"', // ” (U+201D) in UTF-8
"\xE2\x80\x9E" => '"', // „ (U+201E) in UTF-8
"\xE2\x80\x9F" => '"', // ‟ (U+201F) in UTF-8
"\xE2\x80\xB9" => "'", // (U+2039) in UTF-8
"\xE2\x80\xBA" => "'", // (U+203A) in UTF-8
"\xE2\x80\xB2" => "'", // (U+2032) in UTF-8
"\xE2\x80\xB3" => '"', // ″ (U+2033) in UTF-8
// dashes
"\xE2\x80\x94" => '-', // — (U+2014) in UTF-8
"\xEF\xB9\x98" => '-', // (U+FE58) in UTF-8
];
return strtr($string, $quotes);
}
// JSON escaping for JS see:
// https://stackoverflow.com/questions/7462394/php-json-string-escape-double-quotes-for-js-output
function json_escape($str) {
if (!is_string($str)) { return $str; }
$str = str_replace([ '\"', '"' ], [ '"', '\"' ], smart_quotes($str));
//$str = str_replace("\u0022", "\\\"", $str );
//$str = str_replace("\u0027", "\\'", $str );
return $str;
}
function safe_json_encode($var, $options = 0) {
$options |= OBS_JSON_ENCODE; // JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
$str = @json_encode($var, $options);
if (OBS_DEBUG && json_last_error() !== JSON_ERROR_NONE) {
print_message('JSON ENCODE[%r'.json_last_error_msg().'%n]');
echo("JSON VAR[\n".print_vars($var)."\n]\n");
}
return $str;
}
function safe_json_decode($str, $options = 0) {
if (!is_string($str)) {
// When not string passed return original variable
// This is not same as json_decode do, but better for us
if (OBS_DEBUG) {
print_message('JSON DECODE[%rNot string passed%n]');
echo("JSON RAW[\n");
print_vars($str);
echo("\n]\n");
}
//r($str);
return $str;
}
$options |= OBS_JSON_DECODE; // JSON_BIGINT_AS_STRING
$json = @json_decode(smart_quotes($str), TRUE, 512, $options);
$json_error = json_last_error();
if ($json_error !== JSON_ERROR_NONE) {
$msg = json_last_error_msg();
if ($json_error === JSON_ERROR_CTRL_CHAR) {
// Try fix "Control character error, possibly incorrectly encoded"
$str_fix = preg_replace('/[[:cntrl:]]/', '', smart_quotes($str));
print_debug_vars($str_fix);
} else {
// Try fix utf errors
$str_fix = fix_json_unicode(smart_quotes($str));
print_debug_vars($str_fix);
}
$json_fix = @json_decode($str_fix, TRUE, 512, $options);
if (json_last_error() === JSON_ERROR_NONE) {
//print_vars(smart_quotes(fix_json_unicode($str)));
//print_vars($json_fix);
return $json_fix;
}
if (OBS_DEBUG) {
print_message('JSON DECODE[%r' . $msg . '%n]');
echo("JSON[\n" . $str . "\n]\n");
}
}
return $json;
}
/*
* PHP json_decode not correctly convert UTF8 encoded chars, but correct decode escaped unicode :/
* "ËЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ" ->
* "\u00cb\u0419\u0426\u0423\u041a\u0415\u041d\u0413\u0428\u0429\u0417\u0425\u042a\u0424\u042b\u0412\u0410\u041f\u0420\u041e\u041b\u0414\u0416\u042d\u042f\u0427\u0421\u041c\u0418\u0422\u042c\u0411\u042e"
*/
function fix_json_unicode($string) {
$ret = '';
$len = strlen($string);
$offset = 0;
while ($offset < $len) {
$code = ord($string[$offset]);
if ($code >= 128) { //otherwise 0xxxxxxx
if ($code < 224) {
$bytesnumber = 2; //110xxxxx
} elseif ($code < 240) {
$bytesnumber = 3; //1110xxxx
} elseif ($code < 248) {
$bytesnumber = 4; //11110xxx
} else {
$bytesnumber = 0;
}
$codetemp = $code - 192 - ($bytesnumber > 2 ? 32 : 0) - ($bytesnumber > 3 ? 16 : 0);
for ($i = 2; $i <= $bytesnumber; $i++) {
$offset++;
$code2 = ord($string[$offset]) - 128; //10xxxxxx
$codetemp = $codetemp * 64 + $code2;
}
$code = $codetemp;
$ret .= '\u' . str_pad(dechex($code), 4, '0', STR_PAD_LEFT);
} else {
$ret .= $string[$offset];
}
$offset++;
}
return $ret;
}
function safe_unserialize($str) {
if (is_array($str)) { return NULL; }
if (PHP_VERSION_ID >= 70000) {
return @unserialize($str, [ 'allowed_classes' => FALSE ]);
}
return @unserialize($str);
}
function safe_count($array, $mode = COUNT_NORMAL) {
if (is_countable($array)) {
return count($array, $mode);
}
return 0;
}
/**
* Report if var empty (only empty array [], string '' and NULL)
* Note FALSE, 0 and '0' return TRUE (not empty)
*
* @param $var
*
* @return bool
*/
function safe_empty($var) {
return $var !== 0 && $var !== '0' && $var !== FALSE && empty($var);
}
/**
* This function return object with recursive directory iterator.
*
* @param string $dir
* @param integer $max_depth The maximum allowed subdirectory depth. -1 is used for any depth.
*
* @return RecursiveIteratorIterator
*/
function get_recursive_directory_iterator($dir, $max_depth = -1)
{
$directory = new RecursiveDirectoryIterator($dir, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::LEAVES_ONLY, RecursiveIteratorIterator::CATCH_GET_CHILD);
$iterator->setMaxDepth($max_depth);
return $iterator;
}
// Nice PHP (7.3) compat functions
if (PHP_VERSION_ID < 70300) {
/**
* Gets the first key of an array
*
* @param array $array
* @return mixed
*/
function array_key_first($array) {
if (!is_array($array) || empty($array)) {
return NULL;
}
foreach($array as $key => $unused) {
return $key;
}
return NULL;
}
/* Use official polifyll
function array_key_first($array) {
return $array && is_array($array) ? array_keys($array)[0] : NULL;
}
*/
/**
* Gets the last key of an array
*
* @param array $array
* @return mixed
*/
function array_key_last($array) {
if (!is_array($array) || empty($array)) {
return NULL;
}
end($array);
return key($array);
//return $array && is_array($array) ? array_keys($array)[count($array) - 1] : NULL;
}
function is_countable($var) {
return (is_array($var) || $var instanceof Countable);
}
}
// Nice PHP (8.0) compat functions
if (PHP_VERSION_ID < 80000) {
// Note. We use better implementations str_contains_array() and str_icontains_array()
function str_contains($haystack, $needle) {
return '' === $needle || FALSE !== strpos($haystack, $needle);
}
/*
function str_starts_with($haystack, $needle) {
return 0 === strncmp($haystack, $needle, strlen($needle));
}
function str_ends_with($haystack, $needle) {
return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, -strlen($needle)));
}
*/
function fdiv($dividend, $divisor) {
return @($dividend / $divisor);
}
}
// EOF