1<?php
2
3/**
4 * Observium
5 *
6 *   This file is part of Observium.
7 *
8 * @package    observium
9 * @subpackage common
10 * @author     Adam Armstrong <adama@observium.org>
11 * @copyright  (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited
12 *
13 */
14
15// Common Functions
16/// FIXME. There should be functions that use only standard php (and self) functions.
17
18/**
19 * Autoloader for Classes used in Observium
20 *
21 */
22function observium_autoload($class_name)
23{
24  //var_dump($class_name);
25  $base_dir    = $GLOBALS['config']['install_dir'] . '/libs/';
26
27  $class_array = explode('\\', $class_name);
28  $class_file  = str_replace('_', '/', implode($class_array, '/')) . '.php';
29  //print_vars($class_array);
30  switch ($class_array[0])
31  {
32    case 'cli':
33      include_once($base_dir . 'cli/cli.php'); // Cli classes required base functions
34      $class_file = str_replace('/Table/', '/table/', $class_file);
35      //var_dump($class_file);
36      break;
37
38    case 'Psr':
39      // Legacy for phpFastCache
40      if ($class_array[1] == 'Cache')
41      {
42        $class_file = array_pop($class_array) . '.php';
43        $class_file = 'phpFastCache/legacy/' . implode($class_array, '/') . '/src/' . $class_file;
44        // print_vars($class_file);
45      }
46      break;
47
48    case 'Flight':
49      $class_file = array_pop($class_array) . '.php';
50      $class_file = 'flight/flight/' . $class_file;
51      break;
52
53    case 'Ramsey':
54      $class_file = str_replace('Ramsey/', '', $class_file);
55      break;
56
57    case 'Defuse':
58      $class_file = str_replace('Defuse/', '', $class_file);
59      break;
60
61    case 'PhpUnitsOfMeasure':
62      include_once($base_dir . 'PhpUnitsOfMeasure/UnitOfMeasureInterface.php');
63      break;
64
65    default:
66      if (is_file($base_dir . 'pear/' . $class_file))
67      {
68        // By default try Pear file
69        $class_file = 'pear/' . $class_file;
70      }
71      else if (is_dir($base_dir . 'pear/' . $class_name))
72      {
73        // And Pear dir
74        $class_file = 'pear/' . $class_name . '/' . $class_file;
75      }
76      //else if (!is_cli() && is_file($GLOBALS['config']['html_dir'] . '/includes/' . $class_file))
77      //{
78      //  // For WUI check class files in html_dir
79      //  $base_dir   = $GLOBALS['config']['html_dir'] . '/includes/';
80      //}
81  }
82  $full_path = $base_dir . $class_file;
83
84  $status = is_file($full_path);
85  if ($status)
86  {
87    $status = include_once($full_path);
88  }
89  if (OBS_DEBUG > 1)
90  {
91    print_message("%WLoad class '$class_name' from '$full_path': " . ($status ? '%gOK' : '%rFAIL'), 'console');
92  }
93  return $status;
94}
95// Register autoload function
96spl_autoload_register('observium_autoload');
97
98// DOCME needs phpdoc block
99// TESTME needs unit testing
100// MOVEME to includes/functions.inc.php
101function del_obs_attrib($attrib_type)
102{
103  if (isset($GLOBALS['cache']['attribs'])) { unset($GLOBALS['cache']['attribs']); } // Reset attribs cache
104
105  return dbDelete('observium_attribs', "`attrib_type` = ?", array($attrib_type));
106}
107
108// DOCME needs phpdoc block
109// TESTME needs unit testing
110// MOVEME to includes/functions.inc.php
111function set_obs_attrib($attrib_type, $attrib_value)
112{
113  if (isset($GLOBALS['cache']['attribs'])) { unset($GLOBALS['cache']['attribs']); } // Reset attribs cache
114
115  //if (dbFetchCell("SELECT COUNT(*) FROM `observium_attribs` WHERE `attrib_type` = ?;", array($attrib_type)))
116  if (dbExist('observium_attribs', '`attrib_type` = ?', array($attrib_type)))
117  {
118    $status = dbUpdate(array('attrib_value' => $attrib_value), 'observium_attribs', "`attrib_type` = ?", array($attrib_type));
119  } else {
120    $status = dbInsert(array('attrib_type' => $attrib_type, 'attrib_value' => $attrib_value), 'observium_attribs');
121    if ($status !== FALSE) { $status = TRUE; } // Note dbInsert return IDs if exist or 0 for not indexed tables
122  }
123  return $status;
124}
125
126// DOCME needs phpdoc block
127// TESTME needs unit testing
128// MOVEME to includes/functions.inc.php
129function get_obs_attribs($type_filter)
130{
131  if (!isset($GLOBALS['cache']['attribs']))
132  {
133    $attribs = array();
134    foreach (dbFetchRows("SELECT * FROM `observium_attribs`") as $entry)
135    {
136      $attribs[$entry['attrib_type']] = $entry['attrib_value'];
137    }
138    $GLOBALS['cache']['attribs'] = $attribs;
139  }
140
141  if (strlen($type_filter))
142  {
143    $attribs = array();
144    foreach ($GLOBALS['cache']['attribs'] as $type => $value)
145    {
146      if (strpos($type, $type_filter) !== FALSE)
147      {
148        $attribs[$type] = $value;
149      }
150    }
151    return $attribs; // Return filtered attribs
152  }
153
154  return $GLOBALS['cache']['attribs']; // All cached attribs
155}
156
157// DOCME needs phpdoc block
158// TESTME needs unit testing
159// MOVEME to includes/functions.inc.php
160function get_obs_attrib($attrib_type)
161{
162  if (isset($GLOBALS['cache']['attribs'][$attrib_type]))
163  {
164    return $GLOBALS['cache']['attribs'][$attrib_type];
165  }
166
167  if ($row = dbFetchRow("SELECT `attrib_value` FROM `observium_attribs` WHERE `attrib_type` = ?;", array($attrib_type)))
168  {
169    return $row['attrib_value'];
170  } else {
171    return NULL;
172  }
173}
174
175// FIXME. Function temporary placed here, since cache_* functions currently included in WUI only.
176// MOVEME includes/cache.inc.php
177/**
178 * Add clear cache attrib, this will request for clering cache in next request.
179 *
180 * @param string $target Clear cache target: wui or cli (default if wui)
181 */
182function set_cache_clear($target = 'wui')
183{
184  if (OBS_DEBUG || OBS_CACHE_DEBUG)
185  {
186    print_error('<span class="text-warning">CACHE CLEAR SET.</span> Cache clear set.');
187  }
188  if (!$GLOBALS['config']['cache']['enable'])
189  {
190    // Cache not enabled
191    return;
192  }
193
194  switch (strtolower($target))
195  {
196    case 'cli':
197      // Add clear CLI cache attrib. Currently not used
198      set_obs_attrib('cache_cli_clear', get_request_id());
199      break;
200    default:
201      // Add clear WUI cache attrib
202      set_obs_attrib('cache_wui_clear', get_request_id());
203  }
204}
205
206function set_status_var($var, $value)
207{
208  $GLOBALS['cache']['status_vars'][$var] = $value;
209  return TRUE;
210}
211
212function isset_status_var($var)
213{
214  return in_array($var, array_keys($GLOBALS['cache']['status_vars']));
215}
216
217function get_status_var($var)
218{
219  return $GLOBALS['cache']['status_vars'][$var];
220}
221
222/**
223 * Generate and store Unique ID for current system. Store in DB at first run.
224 *  IDs is RFC 4122 version 4 (without dashes, varchar(32)), i.e. c39b2386c4e8487fad4a87cd367b279d
225 *
226 * @return string Unique system ID
227 */
228function get_unique_id()
229{
230  if (!defined('OBS_UNIQUE_ID'))
231  {
232    $unique_id = get_obs_attrib('unique_id');
233
234    if (!strlen($unique_id))
235    {
236      // Old, low entropy
237      //$unique_id = str_replace('.', '', uniqid(NULL, TRUE)); // i.e. 55b24d7f1fa57330542020
238      // Generate a version 4 (random) UUID object
239      $uuid4 = Ramsey\Uuid\Uuid::uuid4();
240      //$unique_id = $uuid4->toString(); // i.e. c39b2386-c4e8-487f-ad4a-87cd367b279d
241      $unique_id = $uuid4->getHex();   // i.e. c39b2386c4e8487fad4a87cd367b279d
242      dbInsert(array('attrib_type' => 'unique_id', 'attrib_value' => $unique_id), 'observium_attribs');
243    }
244    define('OBS_UNIQUE_ID', $unique_id);
245  }
246
247  return OBS_UNIQUE_ID;
248}
249
250/**
251 * Generate and store Unique Request ID for current script/page.
252 * ID unique between 2 different requests or page loads
253 *  IDs is RFC 4122 version 4, i.e. 25769c6c-d34d-4bfe-ba98-e0ee856f3e7a
254 *
255 * @return string Unique Request ID
256 */
257function get_request_id()
258{
259  if (!defined('OBS_REQUEST_ID'))
260  {
261    // Generate a version 4 (random) UUID object
262    $uuid4 = Ramsey\Uuid\Uuid::uuid4();
263    $request_id = $uuid4->toString(); // i.e. 25769c6c-d34d-4bfe-ba98-e0ee856f3e7a
264    define('OBS_REQUEST_ID', $request_id);
265  }
266
267  return OBS_REQUEST_ID;
268}
269
270/**
271 * Set new DB Schema version
272 *
273 * @param integer $db_rev New DB schema revision
274 * @param boolean $schema_insert Update (by default) or insert by first install db schema
275 * @return boolean Status of DB schema update
276 */
277function set_db_version($db_rev, $schema_insert = FALSE)
278{
279  if ($db_rev >= 211) // Do not remove this check, since before this revision observium_attribs table not exist!
280  {
281    $status = set_obs_attrib('dbSchema', $db_rev);
282  } else {
283    if ($schema_insert)
284    {
285      $status = dbInsert(array('version' => $db_rev), 'dbSchema');
286      if ($status !== FALSE) { $status = TRUE; } // Note dbInsert return IDs if exist or 0 for not indexed tables
287    } else {
288      $status = dbUpdate(array('version' => $db_rev), 'dbSchema');
289    }
290  }
291
292  if ($status)
293  {
294    $GLOBALS['cache']['db_version'] = $db_rev; // Cache new db version
295  }
296
297  return $status;
298}
299
300/**
301 * Get current DB Schema version
302 *
303 * @return string DB schema version
304 */
305// TESTME needs unit testing
306function get_db_version()
307{
308  if (!isset($GLOBALS['cache']['db_version']))
309  {
310    if ($db_rev = @get_obs_attrib('dbSchema')) {} else
311    {
312      // CLEANME remove fallback at r7000
313      // not r7000, but after one next CE release!
314      if ($db_rev = @dbFetchCell("SELECT `version` FROM `dbSchema` ORDER BY `version` DESC LIMIT 1")) {} else
315      {
316        $db_rev = 0;
317      }
318    }
319    $db_rev = (int)$db_rev;
320    if ($db_rev > 0)
321    {
322      $GLOBALS['cache']['db_version'] = $db_rev;
323    } else {
324      // Do not cache zero value
325      return $db_rev;
326    }
327  }
328  return $GLOBALS['cache']['db_version'];
329}
330
331/**
332 * Get current DB Size
333 *
334 * @return string DB size in bytes
335 */
336// TESTME needs unit testing
337function get_db_size()
338{
339  $db_size = dbFetchCell('SELECT SUM(`data_length` + `index_length`) AS `size` FROM `information_schema`.`tables` WHERE `table_schema` = ?;', array($GLOBALS['config']['db_name']));
340  return $db_size;
341}
342
343/**
344 * Get local hostname
345 *
346 * @return string FQDN local hostname
347 */
348function get_localhost()
349{
350  global $cache;
351
352  if (!isset($cache['localhost']))
353  {
354    $cache['localhost'] = php_uname('n');
355    if (!strpos($cache['localhost'], '.'))
356    {
357      // try use hostname -f for get FQDN hostname
358      $localhost_t = external_exec('/bin/hostname -f');
359      if (strpos($localhost_t, '.'))
360      {
361        $cache['localhost'] = $localhost_t;
362      }
363    }
364  }
365
366  return $cache['localhost'];
367}
368
369/**
370 * Get the directory size
371 *
372 * @param string $directory
373 * @return integer Directory size in bytes
374 */
375function get_dir_size($directory)
376{
377  $size = 0;
378
379  foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), RecursiveIteratorIterator::LEAVES_ONLY, RecursiveIteratorIterator::CATCH_GET_CHILD) as $file)
380  {
381    if ($file->getFileName() != '..') { $size += $file->getSize(); }
382  }
383
384  return $size;
385}
386
387// DOCME needs phpdoc block
388// TESTME needs unit testing
389// MOVEME to includes/alerts.inc.php
390function get_alert_entry_by_id($id)
391{
392  return dbFetchRow("SELECT * FROM `alert_table`".
393                    //" LEFT JOIN `alert_table-state` ON  `alert_table`.`alert_table_id` =  `alert_table-state`.`alert_table_id`".
394                    " WHERE  `alert_table`.`alert_table_id` = ?", array($id));
395}
396
397/**
398 * Percent Class
399 *
400 * Given a percentage value return a class name (for CSS).
401 *
402 * @param int|string $percent
403 * @return string
404 */
405function percent_class($percent)
406{
407  if ($percent < "25")
408  {
409    $class = "info";
410  } elseif ($percent < "50") {
411    $class = "";
412  } elseif ($percent < "75") {
413    $class = "success";
414  } elseif ($percent < "90") {
415    $class = "warning";
416  } else {
417    $class = "danger";
418  }
419
420  return $class;
421}
422
423/**
424 * Percent Colour
425 *
426 * This function returns a colour based on a 0-100 value
427 * It scales from green to red from 0-100 as default.
428 *
429 * @param integer $percent
430 * @param integer $brightness
431 * @param integer $max
432 * @param integer $min
433 * @param integer $thirdColorHex
434 * @return string
435 */
436function percent_colour($value, $brightness = 128, $max = 100, $min = 0, $thirdColourHex = '00')
437{
438  if ($value > $max) { $value = $max; }
439  if ($value < $min) { $value = $min; }
440
441  // Calculate first and second colour (Inverse relationship)
442  $first = (1-($value/$max))*$brightness;
443  $second = ($value/$max)*$brightness;
444
445  // Find the influence of the middle Colour (yellow if 1st and 2nd are red and green)
446  $diff = abs($first-$second);
447  $influence = ($brightness-$diff)/2;
448  $first = intval($first + $influence);
449  $second = intval($second + $influence);
450
451  // Convert to HEX, format and return
452  $firstHex = str_pad(dechex($first),2,0,STR_PAD_LEFT);
453  $secondHex = str_pad(dechex($second),2,0,STR_PAD_LEFT);
454
455  return '#'.$secondHex . $firstHex . $thirdColourHex;
456
457  // alternatives:
458  // return $thirdColourHex . $firstHex . $secondHex;
459  // return $firstHex . $thirdColourHex . $secondHex;
460}
461
462/**
463 * Convert sequence of numbers in an array to range of numbers.
464 * Example:
465 *  array(1,2,3,4,5,6,7,8,9,10)    -> '1-10'
466 *  array(1,2,3,5,7,9,10,11,12,14) -> '1-3,5,7,9-12,14'
467 *
468 * @param array $arr Array with sequence of numbers
469 * @param string $separator Use this separator for list
470 * @param bool $sort Sort input array or not
471 * @return string
472 */
473function range_to_list($arr, $separator = ',', $sort = TRUE)
474{
475  if ($sort) { sort($arr, SORT_NUMERIC); }
476
477  for ($i = 0; $i < count($arr); $i++)
478  {
479    $rstart = $arr[$i];
480    $rend  = $rstart;
481    while (isset($arr[$i+1]) && $arr[$i+1] - $arr[$i] == 1)
482    {
483      $rend = $arr[$i+1];
484      $i++;
485    }
486    if (is_numeric($rstart) && is_numeric($rend))
487    {
488      $ranges[] = ($rstart == $rend) ? $rstart : $rstart.'-'.$rend;
489    } else {
490      return ''; // Not numeric value(s)
491    }
492  }
493  $list = implode($separator, $ranges);
494
495  return $list;
496}
497
498// DOCME needs phpdoc block
499// Write a line to the specified logfile (or default log if not specified)
500// We open & close for every line, somewhat lower performance but this means multiple concurrent processes could write to the file.
501// Now marking process and pid, if things are running simultaneously you can still see what's coming from where.
502// TESTME needs unit testing
503function logfile($filename, $string = NULL)
504{
505  global $config, $argv;
506
507  // Use default logfile if none specified
508  if ($string == NULL) { $string = $filename; $filename = $config['log_file']; }
509
510  // Place logfile in log directory if no path specified
511  if (basename($filename) == $filename) { $filename = $config['log_dir'] . '/' . $filename; }
512  // Create logfile if not exist
513  if (is_file($filename))
514  {
515    if (!is_writable($filename))
516    {
517      print_debug("Log file '$filename' is not writable, check file permissions.");
518      return FALSE;
519    }
520    $fd = fopen($filename, 'a');
521  } else {
522    $fd = fopen($filename, 'wb');
523    // Check writable file (only after creation for speedup)
524    if (!is_writable($filename))
525    {
526      print_debug("Log file '$filename' is not writable or not created.");
527      fclose($fd);
528      return FALSE;
529    }
530  }
531
532  //$string = '[' . date('Y/m/d H:i:s O') . '] ' . basename($argv[0]) . '(' . getmypid() . '): ' . trim($string) . PHP_EOL;
533  $string = '[' . date('Y/m/d H:i:s O') . '] ' . basename($_SERVER['SCRIPT_FILENAME']) . '(' . getmypid() . '): ' . trim($string) . PHP_EOL;
534  fputs($fd, $string);
535  fclose($fd);
536}
537
538/**
539 * Get used system versions
540 * @return	array
541 */
542function get_versions()
543{
544  if (isset($GLOBALS['cache']['versions']))
545  {
546    // Already cached
547    return $GLOBALS['cache']['versions'];
548  }
549  $versions = array(); // Init
550
551  // Local system OS version
552  if (is_executable($GLOBALS['config']['install_dir'].'/scripts/distro'))
553  {
554    $os = explode('|', external_exec($GLOBALS['config']['install_dir'].'/scripts/distro'), 6);
555    $versions['os_system']         = $os[0];
556    $versions['os_version']        = $os[1];
557    $versions['os_arch']           = $os[2];
558    $versions['os_distro']         = $os[3];
559    $versions['os_distro_version'] = $os[4];
560    $versions['os_virt']           = $os[5];
561    $versions['os_text']           = $os[0].' '.$os[1].' ['.$os[2].'] ('.$os[3].' '.$os[4].')';
562  }
563
564  // PHP
565  $php_version = PHP_VERSION;
566  $versions['php_full'] = $php_version;
567  $versions['php_version'] = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
568  // PHP OPcache
569  $versions['php_opcache'] = FALSE;
570  if (extension_loaded('Zend OPcache'))
571  {
572    $opcache = ini_get('opcache.enable');
573    $php_version  .= ' (OPcache: ';
574    if ($opcache && is_cli() && ini_get('opcache.enable_cli')) // CLI
575    {
576      $php_version  .= 'ENABLED)';
577      $versions['php_opcache'] = 'ENABLED';
578    }
579    else if ($opcache && !is_cli()) // WUI
580    {
581      $php_version  .= 'ENABLED)';
582      $versions['php_opcache'] = 'ENABLED';
583    } else {
584      $php_version  .= 'DISABLED)';
585      $versions['php_opcache'] = 'DISABLED';
586    }
587  }
588  $versions['php_text'] = $php_version;
589  /*
590  // PHP memory_limit
591  $php_memory_limit = ini_get('memory_limit');
592  $versions['php_memory_limit'] = $php_memory_limit;
593  if ($php_memory_limit < 0)
594  {
595    $versions['php_memory_limit_text'] = 'Unlimited';
596  } else {
597    $versions['php_memory_limit_text'] = formatStorage($php_memory_limit);
598  }
599  */
600
601  // Python
602  $python_version  = str_replace('Python ', '', external_exec('/usr/bin/env python --version 2>&1'));
603  $versions['python_version'] = $python_version;
604  $versions['python_text']    = $python_version;
605
606  // MySQL
607  $mysql_client    = dbClientInfo();
608  if (preg_match('/(\d+\.[\d\w\.\-]+)/', $mysql_client, $matches))
609  {
610    $mysql_client  = $matches[1];
611  }
612  $versions['mysql_client']  = $mysql_client;
613  $mysql_version   = dbFetchCell("SELECT version();");
614  $versions['mysql_full']    = $mysql_version;
615  list($versions['mysql_version']) = explode('-', $mysql_version);
616  $mysql_version  .= ' (extension: ' . OBS_DB_EXTENSION . ' ' . $mysql_client . ')';
617  $versions['mysql_text']    = $mysql_version;
618
619  // SNMP
620  if (is_executable($GLOBALS['config']['snmpget']))
621  {
622    $snmp_version  = str_replace(' version:', '', external_exec($GLOBALS['config']['snmpget'] . " --version 2>&1"));
623  } else {
624    $snmp_version  = 'not found';
625  }
626  $versions['snmp_version'] = str_replace('NET-SNMP ', '', $snmp_version);
627  $versions['snmp_text']    = $snmp_version;
628
629  // RRDtool
630  if (is_executable($GLOBALS['config']['rrdtool']))
631  {
632    list(,$rrdtool_version) = explode(' ', external_exec($GLOBALS['config']['rrdtool'] . ' --version | head -n1'));
633    $versions['rrdtool_version'] = $rrdtool_version;
634
635    if (strlen($GLOBALS['config']['rrdcached']))
636    {
637      if (OBS_RRD_NOLOCAL)
638      {
639        // Remote rrdcached daemon (unknown version)
640        $rrdtool_version .= ' (rrdcached remote: ' . $GLOBALS['config']['rrdcached'] . ')';
641      } else {
642        $rrdcached_exec = str_replace('rrdtool', 'rrdcached', $GLOBALS['config']['rrdtool']);
643        if (!is_executable($rrdcached_exec))
644        {
645          $rrdcached_exec = '/usr/bin/env rrdcached -h';
646        }
647        list(,$versions['rrdcached_version']) = explode(' ', external_exec($rrdcached_exec . ' -h | head -n1'));
648        $rrdtool_version .= ' (rrdcached ' . $versions['rrdcached_version'] . ': ' . $GLOBALS['config']['rrdcached'] . ')';
649      }
650    }
651  } else {
652    $rrdtool_version = 'not found';
653    $versions['rrdtool_version'] = $rrdtool_version;
654  }
655  $versions['rrdtool_text'] = $rrdtool_version;
656
657  // Fping
658  $fping_version = 'not found';
659  if (is_executable($GLOBALS['config']['fping']))
660  {
661    $fping  = external_exec($GLOBALS['config']['fping'] . " -v 2>&1");
662    if (preg_match('/Version\s+(\d\S+)/', $fping, $matches))
663    {
664      $fping_version = $matches[1];
665      $fping_text    = $fping_version;
666
667      if (is_executable($GLOBALS['config']['fping6']))
668      {
669        $fping_text .= ' (IPv4 and IPv6)';
670      } else {
671        $fping_text .= ' (IPv4 only)';
672      }
673    }
674  }
675  $versions['fping_version'] = $fping_version;
676  $versions['fping_text']    = $fping_text;
677
678  // Apache (or any http used?)
679  if (is_cli())
680  {
681    foreach (array('apache2', 'httpd') as $http_cmd)
682    {
683      if (is_executable('/usr/sbin/'.$http_cmd))
684      {
685        $http_cmd = '/usr/sbin/'.$http_cmd;
686      } else {
687        $http_cmd = '/usr/bin/env '.$http_cmd;
688      }
689      $http_version = external_exec($http_cmd.' -v | awk \'/Server version:/ {print $3}\'');
690
691      if ($http_version) { break; }
692    }
693    if (empty($http_version))
694    {
695      $http_version  = 'not found';
696    }
697    $versions['http_full']    = $http_version;
698  } else {
699    $versions['http_full']    = $_SERVER['SERVER_SOFTWARE'];
700  }
701  $versions['http_version'] = str_replace('Apache/', '', $versions['http_full']);
702  $versions['http_text']    = $versions['http_version'];
703
704  $GLOBALS['cache']['versions'] = $versions;
705  //print_vars($GLOBALS['cache']['versions']);
706
707  return $versions;
708}
709
710/**
711 * Print version information about used Observium and additional softwares.
712 *
713 * @return NULL
714 */
715function print_versions()
716{
717  get_versions();
718
719  $os_version      = $GLOBALS['cache']['versions']['os_text'];
720  $php_version     = $GLOBALS['cache']['versions']['php_text'];
721  $python_version  = $GLOBALS['cache']['versions']['python_text'];
722  $mysql_version   = $GLOBALS['cache']['versions']['mysql_text'];
723  $snmp_version    = $GLOBALS['cache']['versions']['snmp_text'];
724  $rrdtool_version = $GLOBALS['cache']['versions']['rrdtool_text'];
725  $fping_version   = $GLOBALS['cache']['versions']['fping_text'];
726  $http_version    = $GLOBALS['cache']['versions']['http_text'];
727
728  if (is_cli())
729  {
730    $timezone      = get_timezone();
731    //print_vars($timezone);
732
733    $mysql_mode    = dbFetchCell("SELECT @@SESSION.sql_mode;");
734    $mysql_charset = dbShowVariables('SESSION', "LIKE 'character_set_connection'");
735
736    // PHP memory_limit
737    //$php_memory_limit      = $GLOBALS['cache']['versions']['php_memory_limit'];
738    //$php_memory_limit_text = $GLOBALS['cache']['versions']['php_memory_limit_text'];
739
740    $php_memory_limit = unit_string_to_numeric(ini_get('memory_limit'));
741    if ($php_memory_limit < 0)
742    {
743      $php_memory_limit_text = 'Unlimited';
744    } else {
745      $php_memory_limit_text = formatStorage($php_memory_limit);
746    }
747
748    echo(PHP_EOL);
749    print_cli_heading("Software versions");
750    print_cli_data("OS",      $os_version);
751    print_cli_data("Apache",  $http_version);
752    print_cli_data("PHP",     $php_version);
753    print_cli_data("Python",  $python_version);
754    print_cli_data("MySQL",   $mysql_version);
755    print_cli_data("SNMP",    $snmp_version);
756    print_cli_data("RRDtool", $rrdtool_version);
757    print_cli_data("Fping",   $fping_version);
758
759    // Additionally in CLI always display Memory Limit, MySQL Mode and Charset info
760
761    echo(PHP_EOL);
762    print_cli_heading("Memory Limit", 3);
763    print_cli_data("PHP",     ($php_memory_limit >= 0 && $php_memory_limit < 268435456 ? '%r' : '') . $php_memory_limit_text, 3);
764
765    echo(PHP_EOL);
766    print_cli_heading("MySQL mode", 3);
767    print_cli_data("MySQL",   $mysql_mode, 3);
768
769    echo(PHP_EOL);
770    print_cli_heading("Charset info", 3);
771    print_cli_data("PHP",     ini_get("default_charset"), 3);
772    print_cli_data("MySQL",   $mysql_charset['character_set_connection'], 3);
773
774    echo(PHP_EOL);
775    print_cli_heading("Timezones info", 3);
776    print_cli_data("Date",    date("l, d-M-y H:i:s T"), 3);
777    print_cli_data("PHP",     $timezone['php'], 3);
778    print_cli_data("MySQL",   ($timezone['diff'] !== 0 ? '%r' : '') . $timezone['mysql'], 3);
779    echo(PHP_EOL);
780
781  } else {
782    $observium_date  = format_unixtime(strtotime(OBSERVIUM_DATE), 'jS F Y');
783
784    echo generate_box_open(array('title' => 'Version Information'));
785    echo('
786        <table class="table table-striped table-condensed-more">
787          <tbody>
788            <tr><td><b>'.escape_html(OBSERVIUM_PRODUCT).'</b></td><td>'.escape_html(OBSERVIUM_VERSION).' ('.escape_html($observium_date).')</td></tr>
789            <tr><td><b>OS</b></td><td>'.escape_html($os_version).'</td></tr>
790            <tr><td><b>Apache</b></td><td>'.escape_html($http_version).'</td></tr>
791            <tr><td><b>PHP</b></td><td>'.escape_html($php_version).'</td></tr>
792            <tr><td><b>Python</b></td><td>'.escape_html($python_version).'</td></tr>
793            <tr><td><b>MySQL</b></td><td>'.escape_html($mysql_version).'</td></tr>
794            <tr><td><b>SNMP</b></td><td>'.escape_html($snmp_version).'</td></tr>
795            <tr><td><b>RRDtool</b></td><td>'.escape_html($rrdtool_version).'</td></tr>
796            <tr><td><b>Fping</b></td><td>'.escape_html($fping_version).'</td></tr>
797          </tbody>
798        </table>'.PHP_EOL);
799    echo generate_box_close();
800  }
801}
802
803// DOCME needs phpdoc block
804// Observium's SQL debugging. Chooses nice output depending upon web or cli
805// TESTME needs unit testing
806function print_sql($query)
807{
808  if ($GLOBALS['cli'])
809  {
810    print_vars($query);
811  } else {
812    if (class_exists('SqlFormatter'))
813    {
814      // Hide it under a "database icon" popup.
815      #echo overlib_link('#', '<i class="oicon-databases"> </i>', SqlFormatter::highlight($query));
816      $query = SqlFormatter::compress($query);
817      echo '<p>',SqlFormatter::highlight($query),'</p>';
818    } else {
819      print_vars($query);
820    }
821  }
822}
823
824// DOCME needs phpdoc block
825// Observium's variable debugging. Chooses nice output depending upon web or cli
826// TESTME needs unit testing
827function print_vars($vars, $trace = NULL)
828{
829  if ($GLOBALS['cli'])
830  {
831    if (function_exists('rt'))
832    {
833      ref::config('shortcutFunc', array('print_vars', 'print_debug_vars'));
834      ref::config('showUrls', FALSE);
835      if (OBS_DEBUG > 0)
836      {
837        if (is_null($trace))
838        {
839          $backtrace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
840        } else {
841          $backtrace = $trace;
842        }
843        ref::config('Backtrace', $backtrace); // pass original backtrace
844        ref::config('showStringMatches',  FALSE);
845      } else {
846        ref::config('showBacktrace',      FALSE);
847        ref::config('showResourceInfo',   FALSE);
848        ref::config('showStringMatches',  FALSE);
849        ref::config('showMethods',        FALSE);
850      }
851      rt($vars);
852    } else {
853      print_r($vars);
854    }
855  } else {
856    if (function_exists('r'))
857    {
858      ref::config('shortcutFunc', array('print_vars', 'print_debug_vars'));
859      ref::config('showUrls',     FALSE);
860      if (OBS_DEBUG > 0)
861      {
862        if (is_null($trace))
863        {
864          $backtrace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
865        } else {
866          $backtrace = $trace;
867        }
868        ref::config('Backtrace', $backtrace); // pass original backtrace
869      } else {
870        ref::config('showBacktrace',      FALSE);
871        ref::config('showResourceInfo',   FALSE);
872        ref::config('showStringMatches',  FALSE);
873        ref::config('showMethods',        FALSE);
874      }
875      //ref::config('stylePath',  $GLOBALS['config']['html_dir'] . '/css/ref.css');
876      //ref::config('scriptPath', $GLOBALS['config']['html_dir'] . '/js/ref.js');
877      r($vars);
878    } else {
879      print_r($vars);
880    }
881  }
882}
883
884/**
885 * Call to print_vars in debug mode only
886 * By default var displayed only for debug level 2
887 *
888 * @param mixed $vars Variable to print
889 * @param integer $debug_level Minimum debug level, default 2
890 */
891function print_debug_vars($vars, $debug_level = 2)
892{
893  // For level 2 display always (also empty), for level 1 only non empty vars
894  if (OBS_DEBUG && OBS_DEBUG >= $debug_level && (OBS_DEBUG > 1 || count($vars)))
895  {
896    $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
897    print_vars($vars, $trace);
898  }
899}
900
901/**
902 * Convert SNMP timeticks string into seconds
903 *
904 * SNMP timeticks can be in two different normal formats:
905 *  - "(2105)"       == 21.05 sec
906 *  - "0:0:00:21.05" == 21.05 sec
907 * Sometime devices return wrong type or numeric instead timetick:
908 *  - "Wrong Type (should be Timeticks): 1632295600" == 16322956 sec
909 *  - "1546241903" == 15462419.03 sec
910 * Parse the timeticks string and convert it to seconds.
911 *
912 * @param string $timetick
913 * @param bool $float - Return a float with microseconds
914 *
915 * @return int|float
916 */
917function timeticks_to_sec($timetick, $float = FALSE)
918{
919  if (strpos($timetick, 'Wrong Type') !== FALSE)
920  {
921    // Wrong Type (should be Timeticks): 1632295600
922    list(, $timetick) = explode(': ', $timetick, 2);
923  }
924
925  $timetick = trim($timetick, " \t\n\r\0\x0B\"()"); // Clean string
926  if (is_numeric($timetick))
927  {
928    // When "Wrong Type" or timetick as an integer, than time with count of ten millisecond ticks
929    $time = $timetick / 100;
930    return ($float ? (float)$time : (int)$time);
931  }
932  if (!preg_match('/^[\d\.: ]+$/', $timetick)) { return FALSE; }
933
934  $timetick_array = explode(':', $timetick);
935  if (count($timetick_array) == 1 && is_numeric($timetick))
936  {
937    $secs = $timetick;
938    $microsecs = 0;
939  } else {
940    //list($days, $hours, $mins, $secs) = $timetick_array;
941    $secs  = array_pop($timetick_array);
942    $mins  = array_pop($timetick_array);
943    $hours = array_pop($timetick_array);
944    $days  = array_pop($timetick_array);
945    list($secs, $microsecs) = explode('.', $secs);
946
947    $hours += $days  * 24;
948    $mins  += $hours * 60;
949    $secs  += $mins  * 60;
950
951    // Sometime used non standard years counter
952    if (count($timetick_array))
953    {
954      $years = array_pop($timetick_array);
955      $secs  += $years * 31536000; // 365 * 24 * 60 * 60;
956    }
957    //print_vars($timetick_array);
958  }
959  $time  = ($float ? (float)$secs + $microsecs/100 : (int)$secs);
960
961  return $time;
962}
963
964/**
965 * Convert SNMP DateAndTime string into unixtime
966 *
967 * field octets contents range
968 * ----- ------ -------- -----
969 * 1 1-2 year 0..65536
970 * 2 3 month 1..12
971 * 3 4 day 1..31
972 * 4 5 hour 0..23
973 * 5 6 minutes 0..59
974 * 6 7 seconds 0..60
975 * (use 60 for leap-second)
976 * 7 8 deci-seconds 0..9
977 * 8 9 direction from UTC '+' / '-'
978 * 9 10 hours from UTC 0..11
979 * 10 11 minutes from UTC 0..59
980 *
981 * For example, Tuesday May 26, 1992 at 1:30:15 PM EDT would be displayed as:
982 * 1992-5-26,13:30:15.0,-4:0
983 *
984 * Note that if only local time is known, then timezone information (fields 8-10) is not present.
985 *
986 * @param string $datetime DateAndTime string
987 * @param boolean $use_gmt Return unixtime converted to GMT or Local (by default)
988 *
989 * @return integer Unixtime
990 */
991function datetime_to_unixtime($datetime, $use_gmt = FALSE)
992{
993  $timezone = get_timezone();
994
995  $datetime = trim($datetime);
996  if (preg_match('/(?<year>\d+)-(?<mon>\d+)-(?<day>\d+)(?:,(?<hour>\d+):(?<min>\d+):(?<sec>\d+)(?<millisec>\.\d+)?(?:,(?<tzs>[+\-]?)(?<tzh>\d+):(?<tzm>\d+))?)/', $datetime, $matches))
997  {
998    if (isset($matches['tzh']))
999    {
1000      // Use TZ offset from datetime string
1001      $offset = $matches['tzs'] . ($matches['tzh'] * 3600 + $matches['tzm'] * 60); // Offset from GMT in seconds
1002    } else {
1003      // Or use system offset
1004      $offset = $timezone['php_offset'];
1005    }
1006    $time_tmp = mktime($matches['hour'], $matches['min'], $matches['sec'], $matches['mon'], $matches['day'], $matches['year']); // Generate unixtime
1007
1008    $time_gmt   = $time_tmp + ($offset * -1);            // Unixtime from string in GMT
1009    $time_local = $time_gmt + $timezone['php_offset'];   // Unixtime from string in local timezone
1010  } else {
1011    $time_local = time();                                // Current unixtime with local timezone
1012    $time_gmt   = $time_local - $timezone['php_offset']; // Current unixtime in GMT
1013  }
1014
1015  if (OBS_DEBUG > 1)
1016  {
1017    $debug_msg  = 'UNIXTIME from DATETIME "' . ($time_tmp ? $datetime : 'time()') . '": ';
1018    $debug_msg .= 'LOCAL (' . format_unixtime($time_local) . '), GMT (' . format_unixtime($time_gmt) . '), TZ (' . $timezone['php'] . ')';
1019    print_message($debug_msg);
1020  }
1021
1022  if ($use_gmt)
1023  {
1024    return ($time_gmt);
1025  } else {
1026    return ($time_local);
1027  }
1028}
1029
1030// DOCME needs phpdoc block
1031# If a device is up, return its uptime, otherwise return the
1032# time since the last time we were able to poll it.  This
1033# is not very accurate, but better than reporting what the
1034# uptime was at some time before it went down.
1035// TESTME needs unit testing
1036function deviceUptime($device, $format = "long")
1037{
1038  if ($device['status'] == 0) {
1039    if ($device['last_polled'] == 0) {
1040      return "Never polled";
1041    }
1042    $since = time() - strtotime($device['last_polled']);
1043    //$reason = isset($device['status_type']) && $format == 'long' ? '('.strtoupper($device['status_type']).') ' : '';
1044    $reason = isset($device['status_type']) ? '('.strtoupper($device['status_type']).') ' : '';
1045
1046    return "Down $reason" . format_uptime($since, $format);
1047  } else {
1048    return format_uptime($device['uptime'], $format);
1049  }
1050}
1051
1052/**
1053 * Format seconds to requested time format.
1054 *
1055 * Default format is "long".
1056 *
1057 * Supported formats:
1058 *   long    => '1 year, 1 day, 1h 1m 1s'
1059 *   longest => '1 year, 1 day, 1 hour 1 minute 1 second'
1060 *   short-3 => '1y 1d 1h'
1061 *   short-2 => '1y 1d'
1062 *   shorter => *same as short-2 above
1063 *   (else)  => '1y 1d 1h 1m 1s'
1064 *
1065 * @param int|string $uptime Time is seconds
1066 * @param string $format Optional format
1067 *
1068 * @return string
1069 */
1070function format_uptime($uptime, $format = "long")
1071{
1072  $uptime = intval(round($uptime, 0));
1073  if ($uptime <= 0) { return '0s'; }
1074
1075  $up['y'] = floor($uptime / 31536000);
1076  $up['d'] = floor($uptime % 31536000 / 86400);
1077  $up['h'] = floor($uptime % 86400 / 3600);
1078  $up['m'] = floor($uptime % 3600 / 60);
1079  $up['s'] = floor($uptime % 60);
1080
1081  $result = '';
1082
1083  if ($format == 'long' || $format == 'longest')
1084  {
1085    if ($up['y'] > 0) {
1086      $result .= $up['y'] . ' year'. ($up['y'] != 1 ? 's' : '');
1087      if ($up['d'] > 0 || $up['h'] > 0 || $up['m'] > 0 || $up['s'] > 0) { $result .= ', '; }
1088    }
1089
1090    if ($up['d'] > 0)  {
1091      $result .= $up['d']  . ' day' . ($up['d'] != 1 ? 's' : '');
1092      if ($up['h'] > 0 || $up['m'] > 0 || $up['s'] > 0) { $result .= ', '; }
1093    }
1094
1095    if ($format == 'longest')
1096    {
1097      if ($up['h'] > 0) { $result .= $up['h'] . ' hour'   . ($up['h'] != 1 ? 's ' : ' '); }
1098      if ($up['m'] > 0) { $result .= $up['m'] . ' minute' . ($up['m'] != 1 ? 's ' : ' '); }
1099      if ($up['s'] > 0) { $result .= $up['s'] . ' second' . ($up['s'] != 1 ? 's ' : ' '); }
1100    } else {
1101      if ($up['h'] > 0) { $result .= $up['h'] . 'h '; }
1102      if ($up['m'] > 0) { $result .= $up['m'] . 'm '; }
1103      if ($up['s'] > 0) { $result .= $up['s'] . 's '; }
1104    }
1105  } else {
1106    $count = 6;
1107    if ($format == 'short-3') { $count = 3; }
1108    elseif ($format == 'short-2' || $format == 'shorter') { $count = 2; }
1109
1110    foreach ($up as $period => $value)
1111    {
1112      if ($value == 0) { continue; }
1113      $result .= $value.$period.' ';
1114      $count--;
1115      if ($count == 0) { break; }
1116    }
1117  }
1118
1119  return trim($result);
1120}
1121
1122/**
1123 * This function convert human written Uptime to seconds.
1124 * Opposite function for format_uptime().
1125 *
1126 * Also applicable for some uptime formats in MIB, like EigrpUpTimeString:
1127 *  'hh:mm:ss', reflecting hours, minutes, and seconds
1128 *  If the up time is greater than 24 hours, is less precise and
1129 *  the minutes and seconds are not reflected. Instead only the days
1130 *  and hours are shown and the string will be formatted like this: 'xxxdxxh'
1131 *
1132 * @param string $uptime Uptime in human readable string or timetick
1133 * @return int Uptime in seconds
1134 */
1135function uptime_to_seconds($uptime)
1136{
1137  if (str_contains($uptime, ':')) {
1138    $seconds = timeticks_to_sec($uptime);
1139  } else {
1140    $seconds = age_to_seconds($uptime);
1141  }
1142
1143  return $seconds;
1144}
1145
1146/**
1147 * Get current timezones for mysql and php.
1148 * Use this function when need display timedate from mysql
1149 * for fix diffs betwen this timezones
1150 *
1151 * Example:
1152 * Array
1153 * (
1154 *  [mysql] => +03:00
1155 *  [php] => +03:00
1156 *  [php_abbr] => MSK
1157 *  [php_offset] => +10800
1158 *  [mysql_offset] => +10800
1159 *  [diff] => 0
1160 * )
1161 *
1162 * @return array Timezones info
1163 */
1164// MOVEME to includes/functions.inc.php
1165function get_timezone()
1166{
1167  global $cache;
1168
1169  if (!isset($cache['timezone']))
1170  {
1171    $timezone = array();
1172    $timezone['system'] = external_exec('date "+%:z"');                         // return '+03:00'
1173    if (!OBS_DB_SKIP)
1174    {
1175      $timezone['mysql']  = dbFetchCell('SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP);'); // return '03:00:00'
1176      if ($timezone['mysql'][0] != '-')
1177      {
1178        $timezone['mysql'] = '+'.$timezone['mysql'];
1179      }
1180      $timezone['mysql']       = preg_replace('/:00$/', '', $timezone['mysql']);  // convert to '+03:00'
1181    }
1182    $timezone['php']         = date('P');                                       // return '+03:00'
1183    $timezone['php_abbr']    = date('T');                                       // return 'MSK'
1184    $timezone['php_name']    = date('e');                                       // return 'Europe/Moscow'
1185    $timezone['php_daylight'] = date('I');                                      // return '0'
1186
1187    foreach (array('php', 'mysql') as $entry)
1188    {
1189      if (!isset($timezone[$entry])) { continue; } // skip mysql if OBS_DB_SKIP
1190
1191      $sign = $timezone[$entry][0];
1192      list($hours, $minutes) = explode(':', $timezone[$entry]);
1193      $timezone[$entry . '_offset'] = $sign . (abs($hours) * 3600 + $minutes * 60); // Offset from GMT in seconds
1194    }
1195
1196    if (OBS_DB_SKIP)
1197    {
1198      // If mysql skipped, just return system/php timezones without caching
1199      return $timezone;
1200    }
1201
1202    // Get get the difference in sec between mysql and php timezones
1203    $timezone['diff'] = (int)$timezone['mysql_offset'] - (int)$timezone['php_offset'];
1204    $cache['timezone'] = $timezone;
1205  }
1206
1207  return $cache['timezone'];
1208}
1209
1210// DOCME needs phpdoc block
1211function humanspeed($speed)
1212{
1213  if ($speed == '')
1214  {
1215    return '-';
1216  } else {
1217    return formatRates($speed);
1218  }
1219}
1220
1221/**
1222 * Convert common MAC strings to fixed 12 char string
1223 * @param  string $mac MAC string (ie: 66:c:9b:1b:62:7e, 00 02 99 09 E9 84)
1224 * @return string      Cleaned MAC string (ie: 00029909e984)
1225 */
1226function mac_zeropad($mac)
1227{
1228  $mac = strtolower(trim($mac));
1229  if (strpos($mac, ':') !== FALSE)
1230  {
1231    // STRING: 66:c:9b:1b:62:7e
1232    $mac_parts = explode(':', $mac);
1233    if (count($mac_parts) == 6)
1234    {
1235      $mac = '';
1236      foreach ($mac_parts as $part)
1237      {
1238        $mac .= zeropad($part);
1239      }
1240    }
1241  } else {
1242    // Hex-STRING: 00 02 99 09 E9 84
1243    // Cisco MAC:  1234.5678.9abc
1244    // Some other: 0x123456789ABC
1245    $mac = str_replace(array(' ', '.', '0x'), '', $mac);
1246  }
1247
1248  if (strlen($mac) == 12 && ctype_xdigit($mac))
1249  {
1250    $mac_clean = $mac;
1251  } else {
1252    $mac_clean = NULL;
1253  }
1254
1255  return $mac_clean;
1256}
1257
1258// DOCME needs phpdoc block
1259// TESTME needs unit testing
1260function format_mac($mac)
1261{
1262  // Strip out non-hex digits
1263  $mac = preg_replace('/[[:^xdigit:]]/', '', strtolower($mac));
1264  // Add colons
1265  $mac = preg_replace('/([[:xdigit:]]{2})(?!$)/', '$1:', $mac);
1266  // Convert fake MACs to IP
1267  //if (preg_match('/ff:fe:([[:xdigit:]]+):([[:xdigit:]]+):([[:xdigit:]]+):([[:xdigit:]]{1,2})/', $mac, $matches))
1268  if (preg_match('/ff:fe:([[:xdigit:]]{2}):([[:xdigit:]]{2}):([[:xdigit:]]{2}):([[:xdigit:]]{2})/', $mac, $matches))
1269  {
1270    if ($matches[1] == '00' && $matches[2] == '00')
1271    {
1272      $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)
1273    } else {
1274      $mac = hexdec($matches[1]).'.'.hexdec($matches[2]).'.'.hexdec($matches[3]).'.'.hexdec($matches[4]);
1275    }
1276  }
1277
1278  return $mac;
1279}
1280
1281// DOCME needs phpdoc block
1282// TESTME needs unit testing
1283function format_number_short($number, $sf)
1284{
1285  // This formats a number so that we only send back three digits plus an optional decimal point.
1286  // Example: 723.42 -> 723    72.34 -> 72.3    2.23 -> 2.23
1287
1288  list($whole, $decimal) = explode('.', $number);
1289  $whole_len = strlen($whole);
1290
1291  if ($whole_len >= $sf || !is_numeric($decimal))
1292  {
1293    $number = $whole;
1294  }
1295  else if ($whole_len < $sf)
1296  {
1297    $number  = $whole;
1298    $diff    = $sf - $whole_len;
1299    $decimal = rtrim(substr($decimal, 0, $diff), '0');
1300    if (strlen($decimal))
1301    {
1302      $number .= '.' . $decimal;
1303    }
1304  }
1305  return $number;
1306}
1307
1308/**
1309 * Detect if required exec functions available
1310 *
1311 * @return boolean TRUE if proc_open/proc_get_status available and not in safe mode.
1312 */
1313function is_exec_available()
1314{
1315  // Detect that function ini_get() not disabled too
1316  if (!function_exists('ini_get'))
1317  {
1318    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.');
1319
1320    return TRUE; // NOTE, this is not a critical function for functionally
1321  }
1322
1323  $required_functions = array('proc_open', 'proc_get_status');
1324  $disabled_functions = explode(',', ini_get('disable_functions'));
1325  foreach ($required_functions as $func)
1326  {
1327    if (in_array($func, $disabled_functions))
1328    {
1329      print_error('ERROR: Function ' . $func . '() is disabled via the `disable_functions` option in php.ini configuration file. Please clean this function from this list.');
1330      return FALSE;
1331    }
1332  }
1333
1334  /*
1335  // Detect safe mode
1336  $safe_mode = ini_get('safe_mode');
1337  if (strtolower($safe_mode) != 'off')
1338  {
1339    return FALSE;
1340  }
1341  */
1342
1343  return TRUE;
1344}
1345
1346// DOCME needs phpdoc block
1347function external_exec($command, $timeout = NULL)
1348{
1349  global $exec_status;
1350
1351  $command     = trim($command);
1352
1353  // Debug the command *before* we run it!
1354  if (OBS_DEBUG > 0)
1355  {
1356    $debug_command = ($command === '' && isset($GLOBALS['snmp_command'])) ? $GLOBALS['snmp_command'] : $command;
1357    if (OBS_DEBUG < 2 && $GLOBALS['config']['snmp']['hide_auth'] &&
1358        preg_match("/snmp(bulk)?(get|getnext|walk)(\s+-(t|r|Cr)['\d\s]+){0,3}(\s+-Cc)?\s+-v[123]c?\s+/", $debug_command))
1359    {
1360      // Hide snmp auth params from debug cmd out,
1361      // for help users who want send debug output to developers
1362      $pattern = "/\s+(?:(-[uxXaA])\s*(?:'.*?')|(-c)\s*(?:'.*?(@\S+)?'))/"; // do not hide contexts, only community and v3 auth
1363      $debug_command = preg_replace($pattern, ' \1\2 ***\3', $debug_command);
1364    }
1365    print_message(PHP_EOL . 'CMD[%y' . $debug_command . '%n]' . PHP_EOL, 'console');
1366  }
1367
1368  $exec_status = array('command'   => $command,
1369                       'exitdelay' => 0);
1370  if ($command === '')
1371  {
1372    // Hardcode exec_status if empty command passed
1373    if (isset($GLOBALS['snmp_command']))
1374    {
1375      $exec_status['command'] = $GLOBALS['snmp_command'];
1376      unset($GLOBALS['snmp_command']); // Now clean this global var
1377    }
1378    $exec_status['exitcode'] = -1;
1379    $exec_status['endtime']  = microtime(TRUE); // store end unixtime with microseconds
1380    $exec_status['runtime']  = 0;
1381    $exec_status['stderr']   = 'Empty command passed';
1382    $exec_status['stdout']   = '';
1383
1384    if (OBS_DEBUG > 0)
1385    {
1386      print_message('CMD EXITCODE['.($exec_status['exitcode'] !== 0 ? '%r' : '%g').$exec_status['exitcode'].'%n]'.PHP_EOL.
1387                    'CMD RUNTIME['.($exec_status['runtime'] > 7 ? '%r' : '%g').round($exec_status['runtime'], 4).'s%n]', 'console');
1388      print_message("STDOUT[\n\n]", 'console', FALSE);
1389      if ($exec_status['exitcode'] && $exec_status['stderr'])
1390      {
1391        // Show stderr if exitcode not 0
1392        print_message("STDERR[\n".$exec_status['stderr']."\n]", 'console', FALSE);
1393      }
1394    }
1395    return '';
1396  }
1397
1398  if (is_numeric($timeout) && $timeout > 0)
1399  {
1400    $timeout_usec = $timeout * 1000000;
1401    $timeout = 0;
1402  } else {
1403    // set timeout to null (not to 0!), see stream_select() description
1404    $timeout_usec = NULL;
1405    $timeout = NULL;
1406  }
1407
1408  $descriptorspec = array(
1409    //0 => array('pipe', 'r'), // stdin
1410    1 => array('pipe', 'w'), // stdout
1411    2 => array('pipe', 'w')  // stderr
1412  );
1413
1414  //$process = proc_open($command, $descriptorspec, $pipes);
1415  $process = proc_open('exec ' . $command, $descriptorspec, $pipes); // exec prevent to use shell
1416  //stream_set_blocking($pipes[0], 0); // Make stdin/stdout/stderr non-blocking
1417  stream_set_blocking($pipes[1], 0);
1418  stream_set_blocking($pipes[2], 0);
1419
1420  $stdout = $stderr = '';
1421  $runtime = 0;
1422  if (is_resource($process))
1423  {
1424    $start = microtime(TRUE);
1425    //while ($status['running'] !== FALSE)
1426    //while (feof($pipes[1]) === FALSE || feof($pipes[2]) === FALSE)
1427    while (TRUE)
1428    {
1429      $read = array();
1430      if (!feof($pipes[1])) { $read[] = $pipes[1]; }
1431      if (!feof($pipes[2])) { $read[] = $pipes[2]; }
1432      if (empty($read)) { break; }
1433      $write  = NULL;
1434      $except = NULL;
1435      stream_select($read, $write, $except, $timeout, $timeout_usec);
1436
1437      // Read the contents from the buffers
1438      foreach ($read as $pipe)
1439      {
1440        if ($pipe === $pipes[1])
1441        {
1442          $stdout .= fread($pipe, 8192);
1443        }
1444        else if ($pipe === $pipes[2])
1445        {
1446          $stderr .= fread($pipe, 8192);
1447        }
1448      }
1449      $runtime = microtime(TRUE) - $start;
1450
1451      // Get the status of the process
1452      $status = proc_get_status($process);
1453
1454      // Break from this loop if the process exited before timeout
1455      if (!$status['running'])
1456      {
1457        if (feof($pipes[1]) === FALSE)
1458        {
1459          // Very rare situation, seems as next proc_get_status() bug
1460          if (!isset($status_fix)) { $status_fix = $status; }
1461          if (OBS_DEBUG > 1) { print_debug("Wrong process status! Issue in proc_get_status(), see: https://bugs.php.net/bug.php?id=69014"); }
1462        } else {
1463          //var_dump($status);
1464          break;
1465        }
1466      }
1467      // Break from this loop if the process exited by timeout
1468      if ($timeout !== NULL)
1469      {
1470        $timeout_usec -= $runtime * 1000000;
1471        if ($timeout_usec < 0)
1472        {
1473          $status['running']  = FALSE;
1474          $status['exitcode'] = -1;
1475          break;
1476        }
1477      }
1478    }
1479    if ($status['running'])
1480    {
1481      // Fix sometimes wrong status, wait for 10 milliseconds
1482      $delay      = 0;
1483      $delay_step = 10000;  // 10ms
1484      $delay_max  = 300000; // 300ms
1485      while ($status['running'] && $delay < $delay_max)
1486      {
1487        usleep($delay_step);
1488        $status = proc_get_status($process);
1489        $delay += $delay_step;
1490      }
1491      $exec_status['exitdelay'] = intval($delay / 1000); // Convert to ms
1492    }
1493    else if (isset($status_fix))
1494    {
1495      // See fixed proc_get_status() above
1496      $status = $status_fix;
1497    }
1498    $exec_status['exitcode'] = (int)$status['exitcode'];
1499    $exec_status['stderr']   = rtrim($stderr);
1500    $stdout = preg_replace('/(?:\n|\r\n|\r)$/D', '', $stdout); // remove last (only) eol
1501  } else {
1502    $stdout = FALSE;
1503    $exec_status['stderr']   = '';
1504    $exec_status['exitcode'] = -1;
1505  }
1506  proc_terminate($process, 9);
1507  //fclose($pipes[0]);
1508  fclose($pipes[1]);
1509  fclose($pipes[2]);
1510
1511  $exec_status['endtime'] = $start + $runtime; // store end unixtime with microseconds
1512  $exec_status['runtime'] = $runtime;
1513  $exec_status['stdout']  = $stdout;
1514
1515  if (OBS_DEBUG > 0)
1516  {
1517    print_message('CMD EXITCODE['.($exec_status['exitcode'] !== 0 ? '%r' : '%g').$exec_status['exitcode'].'%n]'.PHP_EOL.
1518                  'CMD RUNTIME['.($runtime > 7 ? '%r' : '%g').round($runtime, 4).'s%n]', 'console');
1519    if ($exec_status['exitdelay'] > 0)
1520    {
1521      print_message("CMD EXITDELAY[%r".$exec_status['exitdelay']."ms%n]", 'console', FALSE);
1522    }
1523    print_message("STDOUT[\n".$stdout."\n]", 'console', FALSE);
1524    if ($exec_status['exitcode'] && $exec_status['stderr'])
1525    {
1526      // Show stderr if exitcode not 0
1527      print_message("STDERR[\n".$exec_status['stderr']."\n]", 'console', FALSE);
1528    }
1529  }
1530
1531  return $stdout;
1532}
1533
1534/**
1535 * Get information about process by it identifier (pid)
1536 *
1537 * @param int     $pid    The process identifier.
1538 * @param boolean $stats  If true, additionally show cpu/memory stats
1539 * @return array          Array with information about process, If process not found, return FALSE
1540 */
1541function get_pid_info($pid, $stats = FALSE)
1542{
1543  $pid = intval($pid);
1544  if ($pid < 1)
1545  {
1546    print_debug("Incorrect PID passed");
1547    //trigger_error("PID ".$pid." doesn't exists", E_USER_WARNING);
1548    return FALSE;
1549  }
1550
1551  if (!$stats && stripos(PHP_OS, 'Linux') === 0)
1552  {
1553    // Do not use call to ps on Linux and extended stat not required
1554    // FIXME. Need something same on BSD and other Unix platforms
1555
1556    if ($pid_stat = lstat("/proc/$pid"))
1557    {
1558      $pid_info = array('PID' => "$pid");
1559      $ps_stat = explode(" ", file_get_contents("/proc/$pid/stat"));
1560      //echo PHP_EOL; print_vars($ps_stat); echo PHP_EOL;
1561      //echo PHP_EOL; print_vars($pid_stat); echo PHP_EOL;
1562      $pid_info['PPID']         = $ps_stat[3];
1563      $pid_info['UID']          = $pid_stat['uid'].'';
1564      $pid_info['GID']          = $pid_stat['gid'].'';
1565      $pid_info['STAT']         = $ps_stat[2];
1566      $pid_info['COMMAND']      = trim(str_replace("\0", " ", file_get_contents("/proc/$pid/cmdline")));
1567      $pid_info['STARTED']      = date("r", $pid_stat['mtime']);
1568      $pid_info['STARTED_UNIX'] = $pid_stat['mtime'];
1569    } else {
1570      $pid_info = FALSE;
1571    }
1572
1573  } else {
1574    // Use ps call, have troubles on high load systems!
1575
1576    if ($stats)
1577    {
1578      // Add CPU/Mem stats
1579      $options = 'pid,ppid,uid,gid,pcpu,pmem,vsz,rss,tty,stat,time,lstart,args';
1580    } else {
1581      $options = 'pid,ppid,uid,gid,tty,stat,time,lstart,args';
1582    }
1583
1584    $timezone = get_timezone(); // Get system timezone info, for correct started time conversion
1585
1586    $ps = external_exec('/bin/ps -ww -o '.$options.' -p '.$pid, 1); // Set timeout 1sec for exec
1587    $ps = explode("\n", rtrim($ps));
1588
1589    if ($GLOBALS['exec_status']['exitcode'] === 127)
1590    {
1591      print_debug("/bin/ps command not found, not possible to get process info.");
1592      return NULL;
1593    }
1594    else if ($GLOBALS['exec_status']['exitcode'] !== 0 || count($ps) < 2)
1595    {
1596      print_debug("PID ".$pid." doesn't exists");
1597      //trigger_error("PID ".$pid." doesn't exists", E_USER_WARNING);
1598      return FALSE;
1599    }
1600
1601    // "  PID  PPID   UID   GID %CPU %MEM    VSZ   RSS TT       STAT     TIME                  STARTED COMMAND"
1602    // "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"
1603    //
1604    // "  PID  PPID   UID   GID TT       STAT     TIME                  STARTED COMMAND"
1605    // "14675 10250  1000  1000 pts/4    S+   00:00:00 Mon Mar 21 14:48:08 2016 php ./test_pid.php"
1606    //print_vars($ps);
1607
1608    // Parse output
1609    $keys = preg_split("/\s+/", $ps[0], -1, PREG_SPLIT_NO_EMPTY);
1610    $entries = preg_split("/\s+/", $ps[1], count($keys) - 1, PREG_SPLIT_NO_EMPTY);
1611    $started = preg_split("/\s+/", array_pop($entries), 6, PREG_SPLIT_NO_EMPTY);
1612    $command = array_pop($started);
1613
1614    $started[]    = str_replace(':', '', $timezone['system']); // Add system TZ to started time
1615    $started_rfc  = array_shift($started) . ','; // Sun
1616    // Reimplode and convert to RFC2822 started date 'Sun, 20 Mar 2016 18:01:53 +0300'
1617    $started_rfc .= ' ' . $started[1]; // 20
1618    $started_rfc .= ' ' . $started[0]; // Mar
1619    $started_rfc .= ' ' . $started[3]; // 2016
1620    $started_rfc .= ' ' . $started[2]; // 18:01:53
1621    $started_rfc .= ' ' . $started[4]; // +0300
1622    //$started_rfc .= implode(' ', $started);
1623    $entries[] = $started_rfc;
1624
1625    $entries[] = $command; // Re-add command
1626    //print_vars($entries);
1627    //print_vars($started);
1628
1629    $pid_info = array();
1630    foreach ($keys as $i => $key)
1631    {
1632      $pid_info[$key] = $entries[$i];
1633    }
1634    $pid_info['STARTED_UNIX'] = strtotime($pid_info['STARTED']);
1635    //print_vars($pid_info);
1636
1637  }
1638
1639  return $pid_info;
1640}
1641
1642/**
1643 * Add information about process into DB
1644 *
1645 * @param string $process_name Process identicator
1646 * @param array  $device       Device array
1647 * @param int    $pid          PID for process. If empty used current PHP process ID
1648 * @return int                 DB id for inserted row
1649 */
1650function add_process_info($device, $pid = NULL)
1651{
1652  global $argv;
1653
1654  // Check if device_id passed instead array
1655  if (is_numeric($device))
1656  {
1657    $device = array('device_id' => $device);
1658  }
1659  if (!is_numeric($pid))
1660  {
1661    $pid = getmypid();
1662  }
1663  $pid_info = get_pid_info($pid);
1664
1665  if (is_array($pid_info))
1666  {
1667    $process_name = basename($argv[0]);
1668    if ($process_name == 'poller.php' || $process_name == 'alerter.php')
1669    {
1670      // Try detect parent poller wrapper
1671      $parent_info = $pid_info;
1672      do
1673      {
1674        $found = FALSE;
1675        $parent_info = get_pid_info($parent_info['PPID']);
1676        if (strpos($parent_info['COMMAND'], $process_name) !== FALSE)
1677        {
1678          $found = TRUE;
1679        }
1680        else if (strpos($parent_info['COMMAND'], 'poller-wrapper.py') !== FALSE)
1681        {
1682          $pid_info['PPID'] = $parent_info['PID'];
1683        }
1684      } while ($found);
1685    }
1686    $update_array = array(
1687      'process_pid'     => $pid,
1688      'process_name'    => $process_name,
1689      'process_ppid'    => $pid_info['PPID'],
1690      'process_uid'     => $pid_info['UID'],
1691      'process_command' => $pid_info['COMMAND'],
1692      'process_start'   => $pid_info['STARTED_UNIX'],
1693      'device_id'       => $device['device_id']
1694    );
1695    return dbInsert($update_array, 'observium_processes');
1696  }
1697  print_debug("Process info not added for PID: $pid");
1698}
1699
1700function del_process_info($device, $pid = NULL)
1701{
1702  global $argv;
1703
1704  // Check if device_id passed instead array
1705  if (is_numeric($device))
1706  {
1707    $device = array('device_id' => $device);
1708  }
1709  if (!is_numeric($pid))
1710  {
1711    $pid = getmypid();
1712  }
1713
1714  return dbDelete('observium_processes', '`process_pid` = ? AND `process_name` = ? AND `device_id` = ?', array($pid, basename($argv[0]), $device['device_id']));
1715}
1716
1717function check_process_run($device, $pid = NULL)
1718{
1719  global $argv;
1720
1721  $process_name = basename($argv[0]);
1722  $check = FALSE;
1723
1724  // Check if device_id passed instead array
1725  if (is_numeric($device))
1726  {
1727    $device = array('device_id' => $device);
1728  }
1729
1730  $query  = 'SELECT * FROM `observium_processes` WHERE `process_name` = ? AND `device_id` = ?';
1731  $params = array($process_name, $device['device_id']);
1732  if (is_numeric($pid))
1733  {
1734    $query .= ' AND `process_pid` = ?';
1735    $params[] = intval($pid);
1736  }
1737
1738  foreach (dbFetchRows($query, $params) as $process)
1739  {
1740    // We found processes in DB, check if it exist on system
1741    $pid_info = get_pid_info($process['process_pid']);
1742    if (is_array($pid_info) && strpos($pid_info['COMMAND'], $process_name) !== FALSE)
1743    {
1744      // Process still running
1745      $check = array_merge($pid_info, $process);
1746    } else {
1747      // Remove stalled DB entries
1748      dbDelete('observium_processes', '`process_id` = ?', array($process['process_id']));
1749    }
1750  }
1751
1752  return $check;
1753}
1754
1755/**
1756 * Determine array is associative or sequential?
1757 *
1758 * @param array
1759 * @return boolean
1760 */
1761function is_array_assoc($array)
1762{
1763  return (is_array($array) && $array !== array_values($array));
1764}
1765
1766function array_get_nested($array, $string, $delimiter = '->')
1767{
1768  foreach (explode($delimiter, $string) as $key)
1769  {
1770    if (!array_key_exists($key, $array))
1771    {
1772      return NULL;
1773    }
1774    $array = $array[$key];
1775  }
1776
1777  return $array;
1778}
1779
1780/**
1781 * Fast string compare function, checks if string contain $needle
1782 *
1783 * @param string $string              The string to search in
1784 * @param mixed  $needle              If needle is not a string, it is converted to an string
1785 * @param mixed  $encoding            For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
1786 * @param bool   $case_insensitivity  If case_insensitivity is TRUE, comparison is case insensitive
1787 * @return bool                       Returns TRUE if $string starts with $needle or FALSE otherwise
1788 */
1789function str_contains($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
1790{
1791  // If needle is array, use recursive compare
1792  if (is_array($needle))
1793  {
1794    foreach ($needle as $findme)
1795    {
1796      if (str_contains($string, (string)$findme, $encoding, $case_insensitivity))
1797      {
1798        return TRUE;
1799      }
1800    }
1801    return FALSE;
1802  }
1803
1804  $needle  = (string)$needle;
1805  $compare = $string === $needle;
1806  if ($case_insensitivity)
1807  {
1808    // Case-INsensitive
1809
1810    // NOTE, multibyte compare required mb_* functions and slower than general functions
1811    if ($encoding && check_extension_exists('mbstring') && mb_strlen($string, $encoding) != strlen($string))
1812    {
1813      //$encoding = 'UTF-8';
1814      //return mb_strripos($string, $needle, -mb_strlen($string, $encoding), $encoding) !== FALSE;
1815      return $compare || mb_stripos($string, $needle) !== FALSE;
1816    }
1817
1818    return $compare || stripos($string, $needle) !== FALSE;
1819  } else {
1820    // Case-sensitive
1821    return $compare || strpos($string, $needle) !== FALSE;
1822  }
1823}
1824
1825function str_icontains($string, $needle, $encoding = FALSE)
1826{
1827  return str_contains($string, $needle, $encoding, TRUE);
1828}
1829
1830/**
1831 * Fast string compare function, checks if string begin with $needle
1832 *
1833 * @param string $string              The string to search in
1834 * @param mixed  $needle              If needle is not a string, it is converted to an string
1835 * @param mixed  $encoding            For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
1836 * @param binary $case_insensitivity  If case_insensitivity is TRUE, comparison is case insensitive
1837 * @return binary                     Returns TRUE if $string starts with $needle or FALSE otherwise
1838 */
1839function str_starts($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
1840{
1841  // If needle is array, use recursive compare
1842  if (is_array($needle))
1843  {
1844    foreach ($needle as $findme)
1845    {
1846      if (str_starts($string, (string)$findme, $encoding, $case_insensitivity))
1847      {
1848        return TRUE;
1849      }
1850    }
1851    return FALSE;
1852  }
1853
1854  $needle = (string)$needle;
1855  if ($case_insensitivity)
1856  {
1857    // Case-INsensitive
1858
1859    // NOTE, multibyte compare required mb_* functions and slower than general functions
1860    if ($encoding && check_extension_exists('mbstring') && mb_strlen($string, $encoding) != strlen($string))
1861    {
1862      //$encoding = 'UTF-8';
1863      return mb_strripos($string, $needle, -mb_strlen($string, $encoding), $encoding) !== FALSE;
1864    }
1865
1866    return $needle !== ''
1867           ? strncasecmp($string, $needle, strlen($needle)) === 0
1868           : $string === '';
1869  } else {
1870    // Case-sensitive
1871    return $string[0] === $needle[0]
1872           ? strncmp($string, $needle, strlen($needle)) === 0
1873           : FALSE;
1874  }
1875}
1876
1877function str_istarts($string, $needle, $encoding = FALSE)
1878{
1879  return str_starts($string, $needle, $encoding, TRUE);
1880}
1881
1882/**
1883 * Fast string compare function, checks if string end with $needle
1884 *
1885 * @param string $string              The string to search in
1886 * @param mixed  $needle              If needle is not a string, it is converted to an string
1887 * @param mixed  $encoding            For use "slow" multibyte compare, pass required encoding here (ie: UTF-8)
1888 * @param binary $case_insensitivity  If case_insensitivity is TRUE, comparison is case insensitive
1889 * @return binary                     Returns TRUE if $string ends with $needle or FALSE otherwise
1890 */
1891function str_ends($string, $needle, $encoding = FALSE, $case_insensitivity = FALSE)
1892{
1893  // If needle is array, use recursive compare
1894  if (is_array($needle))
1895  {
1896    foreach ($needle as $findme)
1897    {
1898      if (str_ends($string, (string)$findme, $encoding, $case_insensitivity))
1899      {
1900        return TRUE;
1901      }
1902    }
1903    return FALSE;
1904  }
1905
1906  $needle  = (string)$needle;
1907  $nlen    = strlen($needle);
1908  $compare = $needle !== '';
1909
1910  // NOTE, multibyte compare required mb_* functions and slower than general functions
1911  if ($encoding && $compare && check_extension_exists('mbstring') && mb_strlen($string, $encoding) != strlen($string))
1912  {
1913    //$encoding = 'UTF-8';
1914    $diff = mb_strlen($string, $encoding) - mb_strlen($needle, $encoding);
1915    if ($case_insensitivity)
1916    {
1917      return $diff >= 0 && mb_stripos($string, $needle, $diff, $encoding) !== FALSE;
1918    } else {
1919      return $diff >= 0 && mb_strpos($string, $needle, $diff, $encoding) !== FALSE;
1920    }
1921  }
1922
1923  return $compare
1924         ? substr_compare($string, $needle, -$nlen, $nlen, $case_insensitivity) === 0
1925         : $string === '';
1926}
1927
1928function str_iends($string, $needle, $encoding = FALSE)
1929{
1930  return str_ends($string, $needle, $encoding, TRUE);
1931}
1932
1933// DOCME needs phpdoc block
1934// TESTME needs unit testing
1935function is_cli()
1936{
1937  if (defined('__PHPUNIT_PHAR__') && isset($GLOBALS['cache']['is_cli']))
1938  {
1939    // Allow override is_cli() in PHPUNIT
1940    return $GLOBALS['cache']['is_cli'];
1941  }
1942  else if (!defined('OBS_CLI'))
1943  {
1944    define('OBS_CLI', php_sapi_name() == 'cli' && empty($_SERVER['REMOTE_ADDR']));
1945  }
1946
1947  return OBS_CLI;
1948}
1949
1950function cli_is_piped()
1951{
1952  if (!defined('OBS_CLI_PIPED'))
1953  {
1954    define('OBS_CLI_PIPED', check_extension_exists('posix') && !posix_isatty(STDOUT));
1955  }
1956
1957  return OBS_CLI_PIPED;
1958}
1959
1960// Detect if script runned from crontab
1961// DOCME needs phpdoc block
1962// TESTME needs unit testing
1963function is_cron()
1964{
1965  if (!defined('OBS_CRON'))
1966  {
1967    $cron = is_cli() && !isset($_SERVER['TERM']);
1968    // For more accurate check if STDOUT exist (but this requires posix extension)
1969    if ($cron)
1970    {
1971      $cron = $cron && cli_is_piped();
1972    }
1973    define('OBS_CRON', $cron);
1974  }
1975
1976  return OBS_CRON;
1977}
1978
1979// DOCME needs phpdoc block
1980// TESTME needs unit testing
1981function print_prompt($text, $default_yes = FALSE)
1982{
1983  if (is_cli())
1984  {
1985    if (cli_is_piped())
1986    {
1987      // If now not have interactive TTY skip any prompts, return default
1988      $return = TRUE && $default_yes;
1989    }
1990
1991    $question = ($default_yes ? 'Y/n' : 'y/N');
1992    echo trim($text), " [$question]: ";
1993    $handle = fopen ('php://stdin', 'r');
1994    $line  = strtolower(trim(fgets($handle, 3)));
1995    fclose($handle);
1996    if ($default_yes)
1997    {
1998      $return = ($line === 'no' || $line === 'n');
1999    } else {
2000      $return = ($line === 'yes' || $line === 'y');
2001    }
2002  } else {
2003    // Here placeholder for web prompt
2004    $return = TRUE && $default_yes;
2005  }
2006
2007  return $return;
2008}
2009
2010/**
2011 * This function echoes text with style 'debug', see print_message().
2012 * Here checked constant OBS_DEBUG, if OBS_DEBUG not set output - empty.
2013 *
2014 * @param string $text
2015 * @param boolean $strip Stripe special characters (for web) or html tags (for cli)
2016 */
2017function print_debug($text, $strip = FALSE)
2018{
2019  if (defined('OBS_DEBUG') && OBS_DEBUG > 0)
2020  {
2021    print_message($text, 'debug', $strip);
2022  }
2023}
2024
2025/**
2026 * This function echoes text with style 'error', see print_message().
2027 *
2028 * @param string $text
2029 * @param boolean $strip Stripe special characters (for web) or html tags (for cli)
2030 */
2031function print_error($text, $strip = TRUE)
2032{
2033  print_message($text, 'error', $strip);
2034}
2035
2036/**
2037 * This function echoes text with style 'warning', see print_message().
2038 *
2039 * @param string $text
2040 * @param boolean $strip Stripe special characters (for web) or html tags (for cli)
2041 */
2042function print_warning($text, $strip = TRUE)
2043{
2044  print_message($text, 'warning', $strip);
2045}
2046
2047/**
2048 * This function echoes text with style 'success', see print_message().
2049 *
2050 * @param string $text
2051 * @param boolean $strip Stripe special characters (for web) or html tags (for cli)
2052 */
2053function print_success($text, $strip = TRUE)
2054{
2055  print_message($text, 'success', $strip);
2056}
2057
2058/**
2059 * This function echoes text with specific styles (different for cli and web output).
2060 *
2061 * @param string $text
2062 * @param string $type Supported types: default, success, warning, error, debug
2063 * @param boolean $strip Stripe special characters (for web) or html tags (for cli)
2064 */
2065function print_message($text, $type='', $strip = TRUE)
2066{
2067  global $config;
2068
2069  // Do nothing if input text not any string (like NULL, array or other). (Empty string '' still printed).
2070  if (!is_string($text) && !is_numeric($text)) { return NULL; }
2071
2072  $type = trim(strtolower($type));
2073  switch ($type)
2074  {
2075    case 'success':
2076      $color = array('cli'       => '%g',                   // green
2077                     'cli_color' => FALSE,                  // by default cli coloring disabled
2078                     'class'     => 'alert alert-success'); // green
2079      $icon  = 'oicon-tick-circle';
2080      break;
2081    case 'warning':
2082      $color = array('cli'       => '%b',                   // blue
2083                     'cli_color' => FALSE,                  // by default cli coloring disabled
2084                     'class'     => 'alert alert-warning');               // yellow
2085      $icon  = 'oicon-bell';
2086      break;
2087    case 'error':
2088      $color = array('cli'       => '%r',                   // red
2089                     'cli_color' => FALSE,                  // by default cli coloring disabled
2090                     'class'     => 'alert alert-danger');   // red
2091      $icon  = 'oicon-exclamation-red';
2092      break;
2093    case 'debug':
2094      $color = array('cli'       => '%r',                   // red
2095                     'cli_color' => FALSE,                  // by default cli coloring disabled
2096                     'class'     => 'alert alert-danger');  // red
2097      $icon  = 'oicon-exclamation-red';
2098      break;
2099    case 'color':
2100      $color = array('cli'       => '',                     // none
2101                     'cli_color' => TRUE,                   // allow using coloring
2102                     'class'     => 'alert alert-info');    // blue
2103      $icon  = 'oicon-information';
2104      break;
2105    case 'console':
2106      // This is special type used nl2br conversion for display console messages on WUI with correct line breaks
2107      $color = array('cli'       => '',                     // none
2108                     'cli_color' => TRUE,                   // allow using coloring
2109                     'class'     => 'alert alert-suppressed'); // purple
2110      $icon  = 'oicon-information';
2111      break;
2112    default:
2113      $color = array('cli'       => '%W',                   // bold
2114                     'cli_color' => FALSE,                  // by default cli coloring disabled
2115                     'class'     => 'alert alert-info');    // blue
2116      $icon  = 'oicon-information';
2117      break;
2118  }
2119
2120  if (is_cli())
2121  {
2122    if ($strip)
2123    {
2124      $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8'); // Convert special HTML entities back to characters
2125      $text = str_ireplace(array('<br />', '<br>', '<br/>'), PHP_EOL, $text); // Convert html <br> to into newline
2126      $text = strip_tags($text);
2127    }
2128    if ($type == 'debug' && !$color['cli_color'])
2129    {
2130      // For debug just echo message.
2131      echo($text . PHP_EOL);
2132    } else {
2133
2134      print_cli($color['cli'].$text.'%n'.PHP_EOL, $color['cli_color']);
2135
2136    }
2137  } else {
2138    if ($text === '') { return NULL; } // Do not web output if the string is empty
2139    if ($strip)
2140    {
2141      if ($text == strip_tags($text))
2142      {
2143        // Convert special characters to HTML entities only if text not have html tags
2144        $text = escape_html($text);
2145      }
2146      if ($color['cli_color'])
2147      {
2148        // Replace some Pear::Console_Color2 color codes with html styles
2149        $replace = array('%',                                  // '%%'
2150                         '</span>',                            // '%n'
2151                         '<span class="label label-warning">', // '%y'
2152                         '<span class="label label-success">', // '%g'
2153                         '<span class="label label-danger">',  // '%r'
2154                         '<span class="label label-primary">', // '%b'
2155                         '<span class="label label-info">',    // '%c'
2156                         '<span class="label label-default">', // '%W'
2157                         '<span class="label label-default" style="color:black;">', // '%k'
2158                         '<span style="font-weight: bold;">',  // '%_'
2159                         '<span style="text-decoration: underline;">', // '%U'
2160                         );
2161      } else {
2162        $replace = array('%', '');
2163      }
2164      $text = str_replace(array('%%', '%n', '%y', '%g', '%r', '%b', '%c', '%W', '%k', '%_', '%U'), $replace, $text);
2165    }
2166
2167    $msg = PHP_EOL.'    <div class="'.$color['class'].'">';
2168    if ($type != 'warning' && $type != 'error')
2169    {
2170      $msg .= '<button type="button" class="close" data-dismiss="alert">&times;</button>';
2171    }
2172    if ($type == 'console')
2173    {
2174      $text = nl2br(trim($text)); // Convert newline to <br /> for console messages with line breaks
2175    }
2176
2177    $msg .= '
2178      <div>'.$text.'</div>
2179    </div>'.PHP_EOL;
2180
2181    echo($msg);
2182  }
2183}
2184
2185function print_cli($text, $colour = TRUE)
2186{
2187  //include_once("Console/Color2.php");
2188
2189  $msg = new Console_Color2();
2190
2191  print $msg->convert($text, $colour);
2192}
2193
2194// TESTME needs unit testing
2195/**
2196 * Print an discovery/poller module stats
2197 *
2198 * @global array $GLOBALS['module_stats']
2199 * @param array $device Device array
2200 * @param string $module Module name
2201 */
2202function print_module_stats($device, $module)
2203{
2204  $log_event = FALSE;
2205  $stats_msg = array();
2206  foreach (array('added', 'updated', 'deleted', 'unchanged') as $key)
2207  {
2208    if ($GLOBALS['module_stats'][$module][$key])
2209    {
2210      $stats_msg[] = (int)$GLOBALS['module_stats'][$module][$key].' '.$key;
2211      if ($key != 'unchanged') { $log_event = TRUE; }
2212    }
2213  }
2214  if (count($GLOBALS['module_stats'][$module])) { echo(PHP_EOL); }
2215  if (count($stats_msg)) { print_cli_data("Changes", implode(', ', $stats_msg)); }
2216  if ($GLOBALS['module_stats'][$module]['time'])
2217  {
2218    print_cli_data("Duration", $GLOBALS['module_stats'][$module]['time']."s");
2219  }
2220  if ($log_event) { log_event(nicecase($module).': '.implode(', ', $stats_msg).'.', $device, 'device', $device['device_id']); }
2221}
2222
2223// DOCME needs phpdoc block
2224// TESTME needs unit testing
2225function print_obsolete_config($filter = '')
2226{
2227  global $config;
2228
2229  $list = array();
2230  foreach ($config['obsolete_config'] as $entry)
2231  {
2232    if ($filter && strpos($entry['old'], $filter) === FALSE) { continue; }
2233    $old = explode('->', $entry['old']);
2234    switch (count($old))
2235    {
2236      case 1:
2237        $entry['isset'] = isset($config[$old[0]]);
2238        break;
2239      case 2:
2240        $entry['isset'] = isset($config[$old[0]][$old[1]]);
2241        break;
2242      case 3:
2243        $entry['isset'] = isset($config[$old[0]][$old[1]][$old[2]]);
2244        break;
2245      case 4:
2246        $entry['isset'] = isset($config[$old[0]][$old[1]][$old[2]][$old[3]]);
2247        break;
2248    }
2249    if ($entry['isset'])
2250    {
2251      $new  = explode('->', $entry['new']);
2252      $info = (isset($entry['info']) ? ' ('.$entry['info'].')' : '');
2253      $list[] = "  %r\$config['".implode("']['", $old)."']%n --> %g\$config['".implode("']['", $new)."']%n".$info;
2254    }
2255  }
2256
2257  if ($list)
2258  {
2259    $msg = "%WWARNING.%n Found obsolete configurations in config.php, please rename respectively:\n".implode(PHP_EOL, $list);
2260    print_message($msg, 'color');
2261    return TRUE;
2262  } else {
2263    return FALSE;
2264  }
2265}
2266
2267// Check if php extension exist, than warn or fail
2268// DOCME needs phpdoc block
2269// TESTME needs unit testing
2270function check_extension_exists($extension, $text = FALSE, $fatal = FALSE)
2271{
2272  $exist = FALSE;
2273  $extension = strtolower($extension);
2274  $extension_functions = array(
2275    'ldap'     => 'ldap_connect',
2276    'mysql'    => 'mysql_connect',
2277    'mysqli'   => 'mysqli_connect',
2278    'mbstring' => 'mb_detect_encoding',
2279    'mcrypt'   => 'mcrypt_encrypt', // CLEANME, mcrypt not used anymore (deprecated since php 7.1, removed since php 7.2)
2280    'posix'    => 'posix_isatty',
2281    'session'  => 'session_name',
2282    'svn'      => 'svn_log'
2283  );
2284
2285  if (isset($extension_functions[$extension]))
2286  {
2287    $exist = @function_exists($extension_functions[$extension]);
2288  } else {
2289    $exist = @extension_loaded($extension);
2290  }
2291
2292  if (!$exist)
2293  {
2294    // Print error (only if $text not equals to FALSE)
2295    if ($text === '' || $text === TRUE)
2296    {
2297      // Generic message
2298      print_error("The extension '$extension' is missing. Please check your PHP configuration.");
2299    }
2300    elseif ($text !== FALSE)
2301    {
2302      // Custom message
2303      print_error("The extension '$extension' is missing. $text");
2304    } else {
2305      // Debug message
2306      print_debug("The extension '$extension' is missing. Please check your PHP configuration.");
2307    }
2308
2309    // Exit if $fatal set to TRUE
2310    if ($fatal) { exit(2); }
2311  }
2312
2313  return $exist;
2314}
2315
2316// TESTME needs unit testing
2317/**
2318 * Sign function
2319 *
2320 * This function extracts the sign of the number.
2321 * Returns -1 (negative), 0 (zero), 1 (positive)
2322 *
2323 * @param integer $int
2324 * @return integer
2325 */
2326function sgn($int)
2327{
2328  if ($int < 0)
2329  {
2330    return -1;
2331  } elseif ($int == 0) {
2332    return 0;
2333  } else {
2334    return 1;
2335  }
2336}
2337
2338// Get port array by ID (using cache)
2339// DOCME needs phpdoc block
2340// TESTME needs unit testing
2341// MOVEME to includes/functions.inc.php
2342function get_port_by_id_cache($port_id)
2343{
2344  return get_entity_by_id_cache('port', $port_id);
2345}
2346
2347// Get port array by ID (with port state)
2348// NOTE get_port_by_id(ID) != get_port_by_id_cache(ID)
2349// DOCME needs phpdoc block
2350// TESTME needs unit testing
2351// MOVEME to includes/functions.inc.php
2352function get_port_by_id($port_id)
2353{
2354  if (is_numeric($port_id))
2355  {
2356    //$port = dbFetchRow("SELECT * FROM `ports` LEFT JOIN `ports-state` ON `ports`.`port_id` = `ports-state`.`port_id`  WHERE `ports`.`port_id` = ?", array($port_id));
2357    $port = dbFetchRow("SELECT * FROM `ports`  WHERE `ports`.`port_id` = ?", array($port_id));
2358  }
2359
2360  if (is_array($port))
2361  {
2362    //$port['port_id'] = $port_id; // It corrects the situation, when `ports-state` is empty
2363    humanize_port($port);
2364    return $port;
2365  }
2366
2367  return FALSE;
2368}
2369
2370// DOCME needs phpdoc block
2371// TESTME needs unit testing
2372// MOVEME to includes/functions.inc.php
2373function get_bill_by_id($bill_id)
2374{
2375  $bill = dbFetchRow("SELECT * FROM `bills` WHERE `bill_id` = ?", array($bill_id));
2376
2377  if (is_array($bill))
2378  {
2379    return $bill;
2380  } else {
2381    return FALSE;
2382  }
2383
2384}
2385
2386// DOCME needs phpdoc block
2387// TESTME needs unit testing
2388// MOVEME to includes/functions.inc.php
2389function get_all_devices()
2390{
2391  global $cache;
2392
2393  // FIXME needs access control checks!
2394  // FIXME respect $type (server, network, etc) -- needs an array fill in topnav.
2395
2396  $devices = array();
2397  if (isset($cache['devices']['hostname']))
2398  {
2399    foreach ($cache['devices']['hostname'] as $hostname => $device_id)
2400    {
2401      $devices[$device_id] = $hostname;
2402    }
2403    //$devices = array_keys($cache['devices']['hostname']);
2404  }
2405  else
2406  {
2407    foreach (dbFetchRows("SELECT `device_id`, `hostname` FROM `devices` ORDER BY `hostname`") as $data)
2408    {
2409      $devices[$data['device_id']] = $data['hostname'];
2410    }
2411  }
2412  //r($devices);
2413  //asort($devices);
2414  //r($devices);
2415
2416  return $devices;
2417}
2418
2419// DOCME needs phpdoc block
2420// TESTME needs unit testing
2421// MOVEME to includes/functions.inc.php
2422function get_application_by_id($application_id)
2423{
2424  if (is_numeric($application_id))
2425  {
2426    $application = dbFetchRow("SELECT * FROM `applications` WHERE `app_id` = ?", array($application_id));
2427  }
2428  if (is_array($application))
2429  {
2430    return $application;
2431  } else {
2432    return FALSE;
2433  }
2434}
2435
2436// DOCME needs phpdoc block
2437// TESTME needs unit testing
2438// MOVEME to includes/functions.inc.php
2439function get_device_id_by_port_id($port_id)
2440{
2441  if (is_numeric($port_id))
2442  {
2443    $device_id = dbFetchCell("SELECT `device_id` FROM `ports` WHERE `port_id` = ?", array($port_id));
2444  }
2445  if (is_numeric($device_id))
2446  {
2447    return $device_id;
2448  } else {
2449    return FALSE;
2450  }
2451}
2452
2453// DOCME needs phpdoc block
2454// TESTME needs unit testing
2455// MOVEME to includes/functions.inc.php
2456function get_device_id_by_app_id($app_id)
2457{
2458  if (is_numeric($app_id))
2459  {
2460    $device_id = dbFetchCell("SELECT `device_id` FROM `applications` WHERE `app_id` = ?", array($app_id));
2461  }
2462  if (is_numeric($device_id))
2463  {
2464    return $device_id;
2465  } else {
2466    return FALSE;
2467  }
2468}
2469
2470// DOCME needs phpdoc block
2471// TESTME needs unit testing
2472// MOVEME html/includes/functions.inc.php BUT this used in includes/rewrites.inc.php
2473function port_html_class($ifOperStatus, $ifAdminStatus, $encrypted = FALSE)
2474{
2475  $ifclass = "interface-upup";
2476  if      ($ifAdminStatus == "down")            { $ifclass = "gray"; }
2477  else if ($ifAdminStatus == "up")
2478  {
2479    if      ($ifOperStatus == "down")           { $ifclass = "red"; }
2480    else if ($ifOperStatus == "lowerLayerDown") { $ifclass = "orange"; }
2481    else if ($ifOperStatus == "monitoring")     { $ifclass = "green"; }
2482    //else if ($encrypted === '1')                { $ifclass = "olive"; }
2483    else if ($encrypted)                        { $ifclass = "olive"; }
2484    else if ($ifOperStatus == "up")             { $ifclass = ""; }
2485    else                                        { $ifclass = "purple"; }
2486  }
2487
2488  return $ifclass;
2489}
2490
2491// DOCME needs phpdoc block
2492// TESTME needs unit testing
2493// MOVEME to includes/functions.inc.php
2494function device_by_name($name, $refresh = 0)
2495{
2496  // FIXME - cache name > id too.
2497  return device_by_id_cache(get_device_id_by_hostname($name), $refresh);
2498}
2499
2500// DOCME needs phpdoc block
2501// TESTME needs unit testing
2502// MOVEME to includes/functions.inc.php
2503function accesspoint_by_id($ap_id, $refresh = '0')
2504{
2505  $ap = dbFetchRow("SELECT * FROM `accesspoints` WHERE `accesspoint_id` = ?", array($ap_id));
2506
2507  return $ap;
2508}
2509
2510// DOCME needs phpdoc block
2511// TESTME needs unit testing
2512// MOVEME to includes/functions.inc.php
2513function device_by_id_cache($device_id, $refresh = '0')
2514{
2515  global $cache;
2516
2517  if (!$refresh && isset($cache['devices']['id'][$device_id]) && is_array($cache['devices']['id'][$device_id]))
2518  {
2519    $device = $cache['devices']['id'][$device_id];
2520  } else {
2521    $device = dbFetchRow("SELECT * FROM `devices` WHERE `device_id` = ?", array($device_id));
2522  }
2523
2524  if (!empty($device))
2525  {
2526    humanize_device($device);
2527    if ($refresh || !isset($device['graphs']))
2528    {
2529      // Fetch device graphs
2530      $device['graphs'] = dbFetchRows("SELECT * FROM `device_graphs` WHERE `device_id` = ?", array($device_id));
2531    }
2532    $cache['devices']['id'][$device_id] = $device;
2533
2534    return $device;
2535  } else {
2536    return FALSE;
2537  }
2538}
2539
2540// DOCME needs phpdoc block
2541// TESTME needs unit testing
2542function truncate($substring, $max = 50, $rep = '...')
2543{
2544  if (strlen($substring) < 1) { $string = $rep; } else { $string = $substring; }
2545  $leave = $max - strlen ($rep);
2546  if (strlen($string) > $max) { return substr_replace($string, $rep, $leave); } else { return $string; }
2547}
2548
2549/**
2550 * Wrapper to htmlspecialchars()
2551 *
2552 * @param string $string
2553 */
2554// TESTME needs unit testing
2555function escape_html($string, $flags = ENT_QUOTES)
2556{
2557
2558  $string = htmlspecialchars($string, $flags, 'UTF-8');
2559
2560  // Un-escape allowed tags
2561  foreach($GLOBALS['config']['escape_html']['tags'] as $tag)
2562  {
2563    $string = str_ireplace('&lt;' .$tag.'&gt;', '<' .$tag.'>', $string);
2564    $string = str_ireplace('&lt;/'.$tag.'&gt;', '</'.$tag.'>', $string);
2565  }
2566  // Un-escape allowed entities
2567  foreach($GLOBALS['config']['escape_html']['entities'] as $tag)
2568  {
2569    $string = str_ireplace('&amp;'.$tag.';', '&'.$tag.';', $string);
2570  }
2571
2572  return $string;
2573}
2574
2575// DOCME needs phpdoc block
2576// TESTME needs unit testing
2577// MOVEME to includes/functions.inc.php
2578function get_device_by_device_id($id)
2579{
2580  global $cache;
2581
2582  if (isset($cache['devices']['id'][$id]['hostname']))
2583  {
2584    $hostname = $cache['devices']['id'][$id]['hostname'];
2585  }
2586  else
2587  {
2588    $hostname = dbFetchCell("SELECT `hostname` FROM `devices` WHERE `device_id` = ?", array($id));
2589  }
2590
2591  return $hostname;
2592}
2593
2594// Return random string with optional character list
2595// DOCME needs phpdoc block
2596// TESTME needs unit testing
2597function generate_random_string($max = 16, $characters = NULL)
2598{
2599  if (!$characters || !is_string($characters))
2600  {
2601    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
2602  }
2603
2604  $randstring = '';
2605  $length = strlen($characters) - 1;
2606
2607  for ($i = 0; $i < $max; $i++)
2608  {
2609    $randstring .= $characters[random_int(0, $length)]; // Require PHP 7.x or random_compat
2610  }
2611
2612  return $randstring;
2613}
2614
2615// Backward compatible random string generator
2616// DOCME needs phpdoc block
2617// TESTME needs unit testing
2618function strgen($length = 16)
2619{
2620  return generate_random_string($length);
2621}
2622
2623// DOCME needs phpdoc block
2624// TESTME needs unit testing
2625// MOVEME to includes/functions.inc.php
2626function getpeerhost($id)
2627{
2628  return dbFetchCell("SELECT `device_id` from `bgpPeers` WHERE `bgpPeer_id` = ?", array($id));
2629}
2630
2631// DOCME needs phpdoc block
2632// TESTME needs unit testing
2633// MOVEME to includes/functions.inc.php
2634function get_device_id_by_hostname($hostname)
2635{
2636  global $cache;
2637
2638  if (isset($cache['devices']['hostname'][$hostname]))
2639  {
2640    $id = $cache['devices']['hostname'][$hostname];
2641  }
2642  else
2643  {
2644    $id = dbFetchCell("SELECT `device_id` FROM `devices` WHERE `hostname` = ?", array($hostname));
2645  }
2646
2647  if (is_numeric($id))
2648  {
2649    return $id;
2650  } else {
2651    return FALSE;
2652  }
2653}
2654
2655// DOCME needs phpdoc block
2656// TESTME needs unit testing
2657// MOVEME to includes/functions.inc.php
2658function gethostosbyid($id)
2659{
2660  global $cache;
2661
2662  if (isset($cache['devices']['id'][$id]['os']))
2663  {
2664    $os = $cache['devices']['id'][$id]['os'];
2665  }
2666  else
2667  {
2668    $os = dbFetchCell("SELECT `os` FROM `devices` WHERE `device_id` = ?", array($id));
2669  }
2670
2671  return $os;
2672}
2673
2674// DOCME needs phpdoc block
2675// TESTME needs unit testing
2676function safename($filename)
2677{
2678  return preg_replace('/[^a-zA-Z0-9._\-]/', '_', $filename);
2679}
2680
2681
2682// DOCME needs phpdoc block
2683// TESTME needs unit testing
2684function zeropad($num, $length = 2)
2685{
2686  return str_pad($num, $length, '0', STR_PAD_LEFT);
2687}
2688
2689// DOCME needs phpdoc block
2690// TESTME needs unit testing
2691// RENAME to get_device_entphysical_state
2692// MOVEME to includes/functions.inc.php
2693function get_dev_entity_state($device)
2694{
2695  $state = array();
2696  foreach (dbFetchRows("SELECT * FROM `entPhysical-state` WHERE `device_id` = ?", array($device)) as $entity)
2697  {
2698    $state['group'][$entity['group']][$entity['entPhysicalIndex']][$entity['subindex']][$entity['key']] = $entity['value'];
2699    $state['index'][$entity['entPhysicalIndex']][$entity['subindex']][$entity['group']][$entity['key']] = $entity['value'];
2700  }
2701
2702  return $state;
2703}
2704
2705// OBSOLETE, remove when all function calls will be deleted
2706function get_dev_attrib($device, $attrib_type)
2707{
2708  // Call to new function
2709  return get_entity_attrib('device', $device, $attrib_type);
2710}
2711
2712// OBSOLETE, remove when all function calls will be deleted
2713function get_dev_attribs($device_id)
2714{
2715  // Call to new function
2716  return get_entity_attribs('device', $device_id);
2717}
2718
2719// OBSOLETE, remove when all function calls will be deleted
2720function set_dev_attrib($device, $attrib_type, $attrib_value)
2721{
2722  // Call to new function
2723  return set_entity_attrib('device', $device, $attrib_type, $attrib_value);
2724}
2725
2726// OBSOLETE, remove when all function calls will be deleted
2727function del_dev_attrib($device, $attrib_type)
2728{
2729  // Call to new function
2730  return del_entity_attrib('device', $device, $attrib_type);
2731}
2732
2733/**
2734 * Return model array from definitions, based on device sysObjectID
2735 *
2736 * @param	array	 $device          Device array required keys -> os, sysObjectID
2737 * @param	string $sysObjectID_new If passed, than use "new" sysObjectID instead from device array
2738 * @return array|FALSE            Model array or FALSE if no model specific definitions
2739 */
2740function get_model_array($device, $sysObjectID_new = NULL)
2741{
2742  global $config, $cache;
2743
2744  if (isset($config['os'][$device['os']]['model']))
2745  {
2746    $model  = $config['os'][$device['os']]['model'];
2747    $models = $config['model'][$model];
2748    $set_cache = FALSE;
2749    if ($sysObjectID_new && preg_match('/^\.\d[\d\.]+$/', $sysObjectID_new))
2750    {
2751      // Use passed as param sysObjectID
2752      $sysObjectID = $sysObjectID_new;
2753    }
2754    elseif (isset($cache['devices']['model'][$device['device_id']]))
2755    {
2756      // Return already cached array if no passed param sysObjectID
2757      return $cache['devices']['model'][$device['device_id']];
2758    }
2759    elseif (preg_match('/^\.\d[\d\.]+$/', $device['sysObjectID']))
2760    {
2761      // Use sysObjectID from device array
2762      $sysObjectID = $device['sysObjectID'];
2763      $set_cache = TRUE;
2764    } else {
2765      // Just random non empty string
2766      $sysObjectID = 'empty_sysObjectID_3948ffakc';
2767      $set_cache = TRUE;
2768    }
2769    if ($set_cache && (!is_numeric($device['device_id']) || defined('__PHPUNIT_PHAR__')))
2770    {
2771      // Do not set cache for unknown device_id (not added device) or phpunit
2772      $set_cache = FALSE;
2773    }
2774    if (isset($models[$sysObjectID]))
2775    {
2776      // Exactly match
2777      if ($set_cache)
2778      {
2779        $cache['devices']['model'][$device['device_id']] = $models[$sysObjectID];
2780      }
2781      return $models[$sysObjectID];
2782    }
2783    // Resort sysObjectID array by oids with from high to low order!
2784    //krsort($config['model'][$model]);
2785    uksort($config['model'][$model], 'compare_numeric_oids_reverse');
2786    foreach ($config['model'][$model] as $key => $entry)
2787    {
2788      if (strpos($sysObjectID, $key) === 0)
2789      {
2790        if ($set_cache)
2791        {
2792          $cache['devices']['model'][$device['device_id']] = $entry;
2793        }
2794        return $entry;
2795        break;
2796      }
2797    }
2798    // If model array not found, set cache entry to FALSE,
2799    // for do not search again
2800    if ($set_cache)
2801    {
2802      $cache['devices']['model'][$device['device_id']] = FALSE;
2803    }
2804  }
2805  return FALSE;
2806}
2807
2808// DOCME needs phpdoc block
2809// TESTME needs unit testing
2810function formatRates($value, $round = 2, $sf = 3)
2811{
2812   $value = format_si($value, $round, $sf) . "bps";
2813   return $value;
2814}
2815
2816// DOCME needs phpdoc block
2817// TESTME needs unit testing
2818function formatStorage($value, $round = 2, $sf = 3)
2819{
2820   $value = format_bi($value, $round, $sf) . 'B';
2821   return $value;
2822}
2823
2824// DOCME needs phpdoc block
2825// TESTME needs unit testing
2826function format_si($value, $round = 2, $sf = 3)
2827{
2828  if ($value < "0")
2829  {
2830    $neg = 1;
2831    $value = $value * -1;
2832  }
2833
2834  if ($value >= "0.1")
2835  {
2836    $sizes = Array('', 'k', 'M', 'G', 'T', 'P', 'E');
2837    $ext = $sizes[0];
2838    for ($i = 1; (($i < count($sizes)) && ($value >= 1000)); $i++) { $value = $value / 1000; $ext = $sizes[$i]; }
2839  }
2840  else
2841  {
2842    $sizes = Array('', 'm', 'u', 'n');
2843    $ext = $sizes[0];
2844    for ($i = 1; (($i < count($sizes)) && ($value != 0) && ($value <= 0.1)); $i++) { $value = $value * 1000; $ext = $sizes[$i]; }
2845  }
2846
2847  if ($neg) { $value = $value * -1; }
2848
2849  return format_number_short(round($value, $round), $sf).$ext;
2850}
2851
2852// DOCME needs phpdoc block
2853// TESTME needs unit testing
2854function format_bi($value, $round = 2, $sf = 3)
2855{
2856  if ($value < "0")
2857  {
2858    $neg = 1;
2859    $value = $value * -1;
2860  }
2861  $sizes = Array('', 'k', 'M', 'G', 'T', 'P', 'E');
2862  $ext = $sizes[0];
2863  for ($i = 1; (($i < count($sizes)) && ($value >= 1024)); $i++) { $value = $value / 1024; $ext = $sizes[$i]; }
2864
2865  if ($neg) { $value = $value * -1; }
2866
2867  return format_number_short(round($value, $round), $sf).$ext;
2868}
2869
2870// DOCME needs phpdoc block
2871// TESTME needs unit testing
2872function format_number($value, $base = '1000', $round = 2, $sf = 3)
2873{
2874  if ($base == '1000')
2875  {
2876    return format_si($value, $round, $sf);
2877  } else {
2878    return format_bi($value, $round, $sf);
2879  }
2880}
2881
2882// DOCME needs phpdoc block
2883// TESTME needs unit testing
2884function format_value($value, $format = '', $round = 2, $sf = 3)
2885{
2886
2887  switch (strtolower($format))
2888  {
2889    case 'si':
2890    case '1000':
2891      $value = format_si($value, $round, $sf);
2892      break;
2893    case 'bi':
2894    case '1024':
2895      $value = format_bi($value, $round, $sf);
2896      break;
2897
2898    case 'shorttime':
2899      $value = format_uptime($value, 'short');
2900      break;
2901
2902    case 'uptime':
2903    case 'time':
2904      $value = format_uptime($value);
2905      break;
2906
2907    default:
2908      if (is_numeric($value))
2909      {
2910        $value = sprintf("%01.{$round}f", $value);
2911        $value = preg_replace(array('/\.0+$/', '/(\.\d)0+$/'), '\1', $value);
2912      }
2913  }
2914
2915  return $value;
2916}
2917
2918/**
2919 * Is Valid Hostname
2920 *
2921 * See: http://stackoverflow.com/a/4694816
2922 *      http://stackoverflow.com/a/2183140
2923 *
2924 * The Internet standards (Request for Comments) for protocols mandate that
2925 * component hostname labels may contain only the ASCII letters 'a' through 'z'
2926 * (in a case-insensitive manner), the digits '0' through '9', and the hyphen
2927 * ('-'). The original specification of hostnames in RFC 952, mandated that
2928 * labels could not start with a digit or with a hyphen, and must not end with
2929 * a hyphen. However, a subsequent specification (RFC 1123) permitted hostname
2930 * labels to start with digits. No other symbols, punctuation characters, or
2931 * white space are permitted. While a hostname may not contain other characters,
2932 * such as the underscore character (_), other DNS names may contain the underscore
2933 *
2934 * @param string $hostname
2935 * @return bool
2936 */
2937function is_valid_hostname($hostname)
2938{
2939  return (preg_match("/^(_?[a-z\d](-*[_a-z\d])*)(\.(_?[a-z\d](-*[_a-z\d])*))*$/i", $hostname) // valid chars check
2940          && preg_match("/^.{1,253}$/", $hostname)                                      // overall length check
2941          && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $hostname));                 // length of each label
2942  /* check for invalid starting characters
2943  if (preg_match('/^[_.-]/', $hostname))
2944  {
2945    return FALSE;
2946  } else {
2947    return ctype_alnum(str_replace('_','',str_replace('-','',str_replace('.','',$hostname))));
2948  }
2949  */
2950}
2951
2952// get $host record from /etc/hosts
2953// FIXME Maybe replace the below thing with exec'ing getent? this makes hosts from LDAP and other NSS sources work as well.
2954//
2955//   tom@magic:~$ getent ahostsv4 magic.powersource.cx
2956//   195.160.166.161 STREAM magic.powersource.cx
2957//   tom@magic:~$ getent hosts magic.powersource.cx
2958//   2001:67c:5c:100::c3a0:a6a1 magic.powersource.cx
2959//
2960// Possibly, as above, not ideal for v4/v6 things though... but I'm not sure what the below code does for a v4 or v6 host (or both)
2961//
2962// DOCME needs phpdoc block
2963// TESTME needs unit testing
2964function ipFromEtcHosts($host)
2965{
2966  $host = strtolower($host);
2967  try {
2968    foreach (new SplFileObject('/etc/hosts') as $line)
2969    {
2970      $d = preg_split('/\s/', $line, -1, PREG_SPLIT_NO_EMPTY);
2971      if (empty($d) || substr(reset($d), 0, 1) == '#') { continue; }
2972      //print_vars($d);
2973      $ip = array_shift($d);
2974      $hosts = array_map('strtolower', $d);
2975      if (in_array($host, $hosts))
2976      {
2977        print_debug("Host '$host' found in hosts");
2978        return $ip;
2979      }
2980    }
2981  }
2982  catch (Exception $e)
2983  {
2984    print_warning("Could not open the file /etc/hosts! This file should be world readable, also check that SELinux is not in enforcing mode.");
2985  }
2986
2987  return FALSE;
2988}
2989
2990// Same as gethostbyname(), but work with both IPv4 and IPv6
2991// Get the IPv4 or IPv6 address corresponding to a given Internet hostname
2992// By default return IPv4 address (A record) if exist,
2993// else IPv6 address (AAAA record) if exist.
2994// For get only IPv6 record use gethostbyname6($hostname, OBS_DNS_AAAA)
2995// DOCME needs phpdoc block
2996// TESTME needs unit testing
2997function gethostbyname6($host, $flags = OBS_DNS_ALL)
2998{
2999  // get AAAA record for $host
3000  // if flag OBS_DNS_A is set, if AAAA fails, it tries for A
3001  // the first match found is returned
3002  // otherwise returns FALSE
3003
3004  $dns = gethostbynamel6($host, $flags);
3005  if ($dns == FALSE)
3006  {
3007    return FALSE;
3008  } else {
3009    return $dns[0];
3010  }
3011}
3012
3013// Same as gethostbynamel(), but work with both IPv4 and IPv6
3014// By default returns both IPv4/6 addresses (A and AAAA records),
3015// for get only IPv6 addresses use gethostbynamel6($hostname, OBS_DNS_AAAA)
3016// DOCME needs phpdoc block
3017// TESTME needs unit testing
3018function gethostbynamel6($host, $flags = OBS_DNS_ALL)
3019{
3020  // get AAAA records for $host,
3021  // if $try_a is true, if AAAA fails, it tries for A
3022  // results are returned in an array of ips found matching type
3023  // otherwise returns FALSE
3024
3025  $ip6 = array();
3026  $ip4 = array();
3027
3028  // First try /etc/hosts
3029  $etc = ipFromEtcHosts($host);
3030
3031  $try_a = is_flag_set(OBS_DNS_A, $flags);
3032  if ($try_a === TRUE)
3033  {
3034    if ($etc)
3035    {
3036      if      (str_contains($etc, '.')) { $ip4[] = $etc; }
3037      else if (str_contains($etc, ':')) { $ip6[] = $etc; }
3038    }
3039    // Separate A and AAAA queries, see: https://www.mail-archive.com/observium@observium.org/msg09239.html
3040    $dns = dns_get_record($host, DNS_A);
3041    if (!is_array($dns)) { $dns = array(); }
3042    $dns6 = dns_get_record($host, DNS_AAAA);
3043    if (is_array($dns6))
3044    {
3045      $dns = array_merge($dns, $dns6);
3046    }
3047  } else {
3048    if ($etc && str_contains($etc, ':')) { $ip6[] = $etc; }
3049    $dns = dns_get_record($host, DNS_AAAA);
3050  }
3051
3052  foreach ($dns as $record)
3053  {
3054    switch ($record['type'])
3055    {
3056      case 'A':
3057        $ip4[] = $record['ip'];
3058        break;
3059      case 'AAAA':
3060        $ip6[] = $record['ipv6'];
3061        break;
3062    }
3063  }
3064
3065  if ($try_a && count($ip4))
3066  {
3067    // Merge ipv4 & ipv6
3068    $ip6 = array_merge($ip4, $ip6);
3069  }
3070
3071  if (count($ip6))
3072  {
3073    return $ip6;
3074  }
3075
3076  return FALSE;
3077}
3078
3079// Get hostname by IP (both IPv4/IPv6)
3080// Return PTR or FALSE
3081// DOCME needs phpdoc block
3082// TESTME needs unit testing
3083function gethostbyaddr6($ip)
3084{
3085  //include_once('Net/DNS2.php');
3086  //include_once('Net/DNS2/RR/PTR.php');
3087
3088  $ptr = FALSE;
3089  $resolver = new Net_DNS2_Resolver();
3090  try
3091  {
3092    $response = $resolver->query($ip, 'PTR');
3093    if ($response)
3094    {
3095      $ptr = $response->answer[0]->ptrdname;
3096    }
3097  } catch (Net_DNS2_Exception $e) {}
3098
3099  return $ptr;
3100}
3101
3102// DOCME needs phpdoc block
3103// TESTME needs unit testing
3104// CLEANME DEPRECATED
3105function add_service($device, $service, $descr)
3106{
3107  $insert = array('device_id' => $device['device_id'], 'service_ip' => $device['hostname'], 'service_type' => $service,
3108                  'service_changed' => array('UNIX_TIMESTAMP(NOW())'), 'service_desc' => $descr, 'service_param' => "", 'service_ignore' => "0");
3109
3110  echo dbInsert($insert, 'services');
3111}
3112
3113/**
3114 * Request an http(s) url.
3115 * Note. If first runtime request exit with timeout,
3116 *       than will be set constant OBS_HTTP_REQUEST as FALSE
3117 *       and all other requests will skipped with FALSE response!
3118 *
3119 * @param string   $request Requested URL
3120 * @param array    $context Set additional HTTP context options, see http://php.net/manual/en/context.http.php
3121 * @param int|boolean $rate_limit Rate limit per day for specified domain (in url). If FALSE no limits
3122 * @global array   $config
3123 * @global array   $GLOBALS['response_headers'] Response headers with keys:
3124 *                                              code (HTTP code status), status (HTTP status description) and all other
3125 * @global boolean $GLOBALS['request_status'] TRUE if response code is 2xx or 3xx
3126 *
3127 * @return string|boolean Return response content or FALSE
3128 */
3129function get_http_request($request, $context = array(), $rate_limit = FALSE)
3130{
3131  global $config;
3132
3133  $ok = TRUE;
3134  if (defined('OBS_HTTP_REQUEST') && OBS_HTTP_REQUEST === FALSE)
3135  {
3136    print_debug("HTTP requests skipped since previous request exit with timeout");
3137    $ok = FALSE;
3138    $GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
3139  }
3140  else if (!ini_get('allow_url_fopen'))
3141  {
3142    print_debug('HTTP requests disabled, since PHP config option "allow_url_fopen" set to off. Please enable this option in your PHP config.');
3143    $ok = FALSE;
3144    $GLOBALS['response_headers'] = array('code' => 400, 'descr' => 'HTTP Method Disabled');
3145  }
3146  else if (preg_match('/^https/i', $request) && !check_extension_exists('openssl'))
3147  {
3148    // Check if Secure requests allowed, but ssl extensin not exist
3149    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.');
3150    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.');
3151    $ok = FALSE;
3152    $GLOBALS['response_headers'] = array('code' => 400, 'descr' => 'HTTPS Method Disabled');
3153  }
3154
3155  if ($ok && $rate_limit && is_numeric($rate_limit) && $rate_limit >= 0)
3156  {
3157    // Check limit rates to this domain (per/day)
3158    if (preg_match('/^https?:\/\/([\w\.]+[\w\-\.]*(:\d+)?)/i', $request, $matches))
3159    {
3160      $date    = format_unixtime($config['time']['now'], 'Y-m-d');
3161      $domain  = $matches[0]; // base domain (with http(s)): https://test-me.com/ -> https://test-me.com
3162      $rate_db = json_decode(get_obs_attrib('http_rate_' . $domain), TRUE);
3163      //print_vars($date); print_vars($rate_db);
3164      if (is_array($rate_db) && isset($rate_db[$date]))
3165      {
3166        $rate_count = $rate_db[$date];
3167      } else {
3168        $rate_count = 0;
3169      }
3170      $rate_count++;
3171      set_obs_attrib('http_rate_' . $domain, json_encode(array($date => $rate_count)));
3172      if ($rate_count > $rate_limit)
3173      {
3174        print_debug("HTTP requests skipped because the rate limit $rate_limit/day for domain '$domain' is exceeded (count: $rate_count)");
3175        $GLOBALS['response_headers'] = array('code' => 429, 'descr' => 'Too Many Requests');
3176        $ok = FALSE;
3177      }
3178      else if (OBS_DEBUG > 1)
3179      {
3180        print_debug("HTTP rate count for domain '$domain': $rate_count ($rate_limit/day)");
3181      }
3182    } else {
3183      $rate_limit = FALSE;
3184    }
3185  }
3186
3187  if (OBS_DEBUG > 0)
3188  {
3189    $debug_request = $request;
3190    if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) { $debug_request = preg_replace('/&stats=.+/', '&stats=***', $debug_request); }
3191    $debug_msg = PHP_EOL . 'REQUEST[%y' . $debug_request . '%n]';
3192  }
3193
3194  if (!$ok)
3195  {
3196    if (OBS_DEBUG > 0)
3197    {
3198      print_message($debug_msg . PHP_EOL .
3199                    'REQUEST STATUS[%rFALSE%n]' . PHP_EOL .
3200                    'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
3201    }
3202
3203    // Set GLOBAL var $request_status for use as validate status of last responce
3204    $GLOBALS['request_status'] = FALSE;
3205    return FALSE;
3206  }
3207
3208  $response = '';
3209
3210  // Add common http context
3211  $opts = array('http' => generate_http_context_defaults($context));
3212
3213  // Process http request and calculate runtime
3214  $start = utime();
3215  $context = stream_context_create($opts);
3216  $response = file_get_contents($request, FALSE, $context);
3217  $runtime = utime() - $start;
3218
3219  // Parse response headers
3220  // Note: $http_response_header - see: http://php.net/manual/en/reserved.variables.httpresponseheader.php
3221  $head = array();
3222  foreach ($http_response_header as $k => $v)
3223  {
3224    $t = explode(':', $v, 2);
3225    if (isset($t[1]))
3226    {
3227      // Date: Sat, 12 Apr 2008 17:30:38 GMT
3228      $head[trim($t[0])] = trim($t[1]);
3229    } else {
3230      // HTTP/1.1 200 OK
3231      if (preg_match("!HTTP/([\d\.]+)\s+(\d+)(.*)!", $v, $matches))
3232      {
3233        $head['http']   = $matches[1];
3234        $head['code']   = intval($matches[2]);
3235        $head['descr']  = trim($matches[3]);
3236      } else {
3237        $head[] = $v;
3238      }
3239    }
3240  }
3241  $GLOBALS['response_headers'] = $head;
3242
3243  // Set GLOBAL var $request_status for use as validate status of last responce
3244  if (isset($head['code']) && ($head['code'] < 200 || $head['code'] >= 400))
3245  {
3246    $GLOBALS['request_status'] = FALSE;
3247  }
3248  elseif ($response === FALSE)
3249  {
3250    // An error in get response
3251    $GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
3252    $GLOBALS['request_status'] = FALSE;
3253  } else {
3254    // Valid statuses: 2xx Success, 3xx Redirection or head code not set (ie response not correctly parsed)
3255    $GLOBALS['request_status'] = TRUE;
3256  }
3257
3258  // Set OBS_HTTP_REQUEST for skip all other requests (FALSE for skip all other requests)
3259  if (!defined('OBS_HTTP_REQUEST'))
3260  {
3261    if ($response === FALSE && empty($http_response_header))
3262    {
3263      $GLOBALS['response_headers'] = array('code' => 408, 'descr' => 'Request Timeout');
3264      $GLOBALS['request_status'] = FALSE;
3265
3266      // Validate host from request and check if it timeout request
3267      if (gethostbyname6(parse_url($request, PHP_URL_HOST)))
3268      {
3269        // Timeout error, only if not received response headers
3270        define('OBS_HTTP_REQUEST', FALSE);
3271        print_debug(__FUNCTION__.'() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
3272        logfile(__FUNCTION__.'() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
3273      }
3274    } else {
3275      define('OBS_HTTP_REQUEST', TRUE);
3276    }
3277  }
3278  // FIXME. what if first request fine, but second broken?
3279  //else if ($response === FALSE)
3280  //{
3281  //  if (function_exists('runkit_constant_redefine')) { runkit_constant_redefine('OBS_HTTP_REQUEST', FALSE); }
3282  //}
3283
3284  if (OBS_DEBUG > 0)
3285  {
3286    // Hide extended stats in normal debug level = 1
3287    if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) { $request = preg_replace('/&stats=.+/', '&stats=***', $request); }
3288    // Show debug info
3289    print_message($debug_msg . PHP_EOL .
3290                  'REQUEST STATUS[' . ($GLOBALS['request_status'] ? '%gTRUE' : '%rFALSE') . '%n]' . PHP_EOL .
3291                  'REQUEST RUNTIME['.($runtime > 3 ? '%r' : '%g').round($runtime, 4).'s%n]' . PHP_EOL .
3292                  'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
3293    if (OBS_DEBUG > 1)
3294    {
3295      print_message("RESPONSE[\n".$response."\n]", 'console', FALSE);
3296      print_vars($http_response_header);
3297      print_vars($opts);
3298    }
3299  }
3300
3301  return $response;
3302}
3303
3304/**
3305 * Process HTTP request by definition array and process it for valid status.
3306 * Used definition params in response key.
3307 *
3308 * @param string $def       Definition array or alert transport key (see transports definitions)
3309 * @param string $response  Response from get_http_request()
3310 * @return boolean          Return TRUE if request processed with valid HTTP code (2xx, 3xx) and API response return valid param
3311 */
3312function test_http_request($def, $response)
3313{
3314  $response = trim($response);
3315
3316  if (is_string($def))
3317  {
3318    // Get transport definition for responses
3319    $def = $GLOBALS['config']['transports'][$def]['notification'];
3320  }
3321
3322  // Set status by response status
3323  $success = get_http_last_status();
3324
3325  // If response return valid code and content, additional parse for specific defined tests
3326  if ($success)
3327  {
3328    // Decode if request OK
3329    $is_response_array = FALSE;
3330    if (strtolower($def['response_format']) == 'json')
3331    {
3332      $response = json_decode($response, TRUE);
3333      $is_response_array = TRUE;
3334    }
3335    // else additional formats?
3336
3337    // Check if call succeeded
3338    if (isset($def['response_test']))
3339    {
3340      // Convert single test condition to multi-level condition
3341      if (isset($def['response_test']['operator']))
3342      {
3343        $def['response_test'] = array($def['response_test']);
3344      }
3345
3346      // Compare all definition fields with response,
3347      // if response param not equals to expected, set not success
3348      // multilevel keys should written with '->' separator, ie: $a[key][some][0] - key->some->0
3349      foreach ($def['response_test'] as $test)
3350      {
3351        if ($is_response_array)
3352        {
3353          $field = array_get_nested($response, $test['field']);
3354        } else {
3355          // RAW response
3356          $field = $response;
3357        }
3358        if (test_condition($field, $test['operator'], $test['value']) === FALSE)
3359        {
3360          print_debug("Response test not success: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
3361
3362          $success = FALSE;
3363          break;
3364        } else {
3365          print_debug("Response test success: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
3366        }
3367      }
3368    }
3369
3370    print_debug_vars($response);
3371  }
3372
3373  return $success;
3374}
3375
3376/**
3377 * Return HTTP return code for last request by get_http_request()
3378 *
3379 * @return integer HTTP code
3380 */
3381function get_http_last_code()
3382{
3383  return $GLOBALS['response_headers']['code'];
3384}
3385
3386/**
3387 * Return HTTP return code for last request by get_http_request()
3388 *
3389 * @return boolean HTTP status TRUE if response code 2xx or 3xx
3390 */
3391function get_http_last_status()
3392{
3393  return $GLOBALS['request_status'];
3394}
3395
3396/**
3397 * Generate HTTP specific context with some defaults for proxy, timeout, user-agent.
3398 * Used in get_http_request().
3399 *
3400 * @param array $context HTTP specified context, see http://php.net/manual/ru/function.stream-context-create.php
3401 * @return array HTTP context array
3402 */
3403function generate_http_context_defaults($context = array())
3404{
3405  global $config;
3406
3407  if (!is_array($context)) { $context = array(); } // Fix context if not array passed
3408
3409  // Defaults
3410  $context['timeout'] = '15';
3411
3412  // User agent (required for some type of queries, ie geocoding)
3413  if (!isset($context['header']))
3414  {
3415    $context['header'] = ''; // Avoid 'undefined index' when concatting below
3416  }
3417  $context['header'] .= 'User-Agent: ' . OBSERVIUM_PRODUCT . '/' . OBSERVIUM_VERSION . "\r\n";
3418
3419  if (isset($config['http_proxy']) && $config['http_proxy'])
3420  {
3421    $context['proxy'] = 'tcp://' . $config['http_proxy'];
3422    $context['request_fulluri'] = TRUE;
3423  }
3424
3425  // Basic proxy auth
3426  if (isset($config['proxy_user']) && $config['proxy_user'] && isset($config['proxy_password']))
3427  {
3428    $auth = base64_encode($config['proxy_user'].':'.$config['proxy_password']);
3429    $context['header'] .= 'Proxy-Authorization: Basic ' . $auth . "\r\n";
3430  }
3431
3432  print_debug_vars($context);
3433
3434  return $context;
3435}
3436
3437
3438/**
3439 * Generate HTTP context based on passed params, tags and definition.
3440 * This context will used in get_http_request_test() (or get_http_request())
3441 *
3442 * @global array $config
3443 * @param string $def      Definition array or alert transport key (see transports definitions)
3444 * @param array  $tags     (optional) Contact array and other tags
3445 * @param array  $params   (optional) Array of requested params with key => value entries (used with request method POST)
3446 * @return array           HTTP Context which can used in get_http_request_test() or get_http_request()
3447 */
3448function generate_http_context($def, $tags = array(), $params = array())
3449{
3450  global $config;
3451
3452  if (is_string($def))
3453  {
3454    // Get transport definition for requests
3455    $def = $config['transports'][$def]['notification'];
3456  }
3457
3458  $context = array(); // Init
3459
3460  // Request method POST/GET
3461  if ($def['method'])
3462  {
3463    $context['method'] = strtoupper($def['method']);
3464  }
3465
3466  // Content and headers
3467  $header = "Connection: close\r\n";
3468
3469  // Add encode $params for POST request inside http headers
3470  if ($context['method'] == 'POST')
3471  {
3472    // Generate request params
3473    foreach ($def['request_params'] as $param => $entry)
3474    {
3475      // Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
3476      if (is_array($entry))
3477      {
3478        // ie teams and pagerduty
3479        $params[$param] = array_merge((array)$params[$param], array_tag_replace($tags, $entry));
3480      }
3481      elseif (!isset($params[$param]) || $params[$param] === '')
3482      {
3483        $params[$param] = array_tag_replace($tags, $entry);
3484      }
3485      // Clean empty params
3486      if ($params[$param] === '' || $params[$param] === []) { unset($params[$param]); }
3487    }
3488
3489    if (strtolower($def['request_format']) == 'json')
3490    {
3491      // Encode params as json string
3492      $data   = json_encode($params);
3493      $header .= "Content-Type: application/json; charset=utf-8\r\n";
3494    } else {
3495      // Encode params as url encoded string
3496      $data   = http_build_query($params);
3497      // https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
3498      //$header .= "Content-Type: multipart/form-data\r\n";
3499      $header .= "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
3500    }
3501    $header .= "Content-Length: ".strlen($data)."\r\n";
3502
3503    // Encoded content data
3504    $context['content'] = $data;
3505  }
3506
3507  // Additional headers with contact params
3508  foreach ($def['request_header'] as $entry)
3509  {
3510    // Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
3511    $header .= array_tag_replace($tags, $entry) . "\r\n";
3512  }
3513
3514  $context['header'] = $header;
3515
3516  return $context;
3517}
3518
3519/**
3520 * Generate URL based on passed params, tags and definition.
3521 * This context will used in get_http_request_test() (or get_http_request())
3522 *
3523 * @global array $config
3524 * @param string $def       Definition array or alert transport key (see transports definitions)
3525 * @param array  $tags      (optional) Contact array, used only if transport required additional headers (ie hipchat)
3526 * @param array  $params    (optional) Array of requested params with key => value entries (used with request method GET)
3527 * @return string           URL which can used in get_http_request_test() or get_http_request()
3528 */
3529function generate_http_url($def, $tags = array(), $params = array())
3530{
3531  global $config;
3532
3533  if (is_string($def))
3534  {
3535    // Get definition for transport API
3536    $def = $config['transports'][$def]['notification'];
3537  }
3538
3539  $url = ''; // Init
3540
3541  // Append (if set $def['url_param']) or set hardcoded url for transport
3542  if (isset($def['url']))
3543  {
3544    // Try to find all keys in URL like %bot_hash% matched with same key in $endpoint array
3545    $url .= array_tag_replace($tags, $def['url']);
3546  }
3547
3548  // Add GET params to url
3549  if ($def['method'] == 'GET')
3550  {
3551    // Generate request params
3552    foreach ($def['request_params'] as $param => $entry)
3553    {
3554      // Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
3555      if (is_array($entry))
3556      {
3557        // ie teams and pagerduty
3558        $params[$param] = array_merge((array)$params[$param], array_tag_replace($tags, $entry));
3559      }
3560      elseif (!isset($params[$param]) || $params[$param] === '')
3561      {
3562        $params[$param] = array_tag_replace($tags, $entry);
3563      }
3564      // Clean empty params
3565      if ($params[$param] === '' || $params[$param] === []) { unset($params[$param]); }
3566    }
3567
3568    // Append params to url
3569    if (count($params))
3570    {
3571      $data   = http_build_query($params);
3572      if (str_contains($url, '?'))
3573      {
3574        // Append additional params to url string
3575        $url .= '&' . $data;
3576      } else {
3577        // Add get params as first time
3578        $url .= '?' . $data;
3579      }
3580    }
3581  }
3582
3583  return $url;
3584}
3585
3586/**
3587 * Format date string.
3588 *
3589 * This function convert date/time string to format from
3590 * config option $config['timestamp_format'].
3591 * If date/time not detected in string, function return original string.
3592 * Example conversions to format 'd-m-Y H:i':
3593 * '2012-04-18 14:25:01' -> '18-04-2012 14:25'
3594 * 'Star wars' -> 'Star wars'
3595 *
3596 * @param string $str
3597 * @return string
3598 */
3599// TESTME needs unit testing
3600function format_timestamp($str)
3601{
3602  global $config;
3603
3604  if (($timestamp = strtotime($str)) === FALSE)
3605  {
3606    return $str;
3607  } else {
3608    return date($config['timestamp_format'], $timestamp);
3609  }
3610}
3611
3612/**
3613 * Format unixtime.
3614 *
3615 * This function convert unixtime string to format from
3616 * config option $config['timestamp_format'].
3617 * Can take an optional format parameter, which is passed to date();
3618 *
3619 * @param string $time Unixtime in seconds since the Unix Epoch (also allowed microseconds)
3620 * @param string $format Common date format
3621 * @return string
3622 */
3623// TESTME needs unit testing
3624function format_unixtime($time, $format = NULL)
3625{
3626  global $config;
3627
3628  list($sec, $usec) = explode('.', strval($time));
3629  if (strlen($usec)) {
3630    $date = date_create_from_format('U.u', number_format($time, 6, '.', ''));
3631  } else {
3632    $date = date_create_from_format('U', $sec);
3633  }
3634
3635  // If something wrong with create data object, just return empty string (and yes, we never use zero unixtime)
3636  if (!$date || $time == 0) { return ''; }
3637
3638  // Set correct timezone
3639  $tz = get_timezone();
3640  //r($tz);
3641  $date_timezone = new DateTimeZone($tz['php']);
3642  //$date_timezone = new DateTimeZone($tz['php_name']);
3643  $date->setTimeZone($date_timezone);
3644  //r($date);
3645
3646  if (strlen($format))
3647  {
3648    return date_format($date, $format);
3649  } else {
3650    //return date_format($date, $config['timestamp_format'] . ' T');
3651    return date_format($date, $config['timestamp_format']);
3652  }
3653}
3654
3655/**
3656 * Reformat US-based dates to display based on $config['date_format']
3657 *
3658 * Supported input formats:
3659 *   DD/MM/YYYY
3660 *   DD/MM/YY
3661 *
3662 * Handling of YY -> YYYY years is passed on to PHP's strtotime, which
3663 * is currently cut off at 1970/2069.
3664 *
3665 * @param string $date Erroneous date format
3666 * @return string $date
3667 */
3668function reformat_us_date($date)
3669{
3670  global $config;
3671
3672  $date = trim($date);
3673  if (preg_match('!^\d{1,2}/\d{1,2}/(\d{2}|\d{4})$!', $date))
3674  {
3675    // Only date
3676    $format = $config['date_format'];
3677  }
3678  elseif (preg_match('!^\d{1,2}/\d{1,2}/(\d{2}|\d{4})\s+\d{1,2}:\d{1,2}(:\d{1,2})?$!', $date))
3679  {
3680    // Date + time
3681    $format = $config['timestamp_format'];
3682  } else {
3683    return $date;
3684  }
3685
3686  return date($format, strtotime($date));
3687}
3688
3689/**
3690 * Convert age string to seconds.
3691 *
3692 * This function convert age string to seconds.
3693 * If age is numeric than it in seconds.
3694 * The supplied age accepts values such as 31d, 240h, 1.5d etc.
3695 * Accepted age scales are:
3696 * y (years), M (months), w (weeks), d (days), h (hours), m (minutes), s (seconds).
3697 * NOTE, for month use CAPITAL 'M'
3698 * With wrong and negative returns 0
3699 *
3700 * '3y 4M 6w 5d 3h 1m 3s' -> 109191663
3701 * '3y4M6w5d3h1m3s'       -> 109191663
3702 * '1.5w'                 -> 907200
3703 * -886732     -> 0
3704 * 'Star wars' -> 0
3705 *
3706 * @param string $age
3707 * @return int
3708 */
3709// TESTME needs unit testing
3710function age_to_seconds($age)
3711{
3712  $age = trim($age);
3713
3714  if (is_numeric($age))
3715  {
3716    $age = (int)$age;
3717    if ($age > 0)
3718    {
3719      return $age;
3720    } else {
3721      return 0;
3722    }
3723  }
3724
3725  $pattern = '/^';
3726  $pattern .= '(?:(?<years>\d+(?:\.\d)*)\ ?(?:years?|y)[,\ ]*)*';         // y (years)
3727  $pattern .= '(?:(?<months>\d+(?:\.\d)*)\ ?(?:months?|M)[,\ ]*)*';       // M (months)
3728  $pattern .= '(?:(?<weeks>\d+(?:\.\d)*)\ ?(?:weeks?|w)[,\ ]*)*';         // w (weeks)
3729  $pattern .= '(?:(?<days>\d+(?:\.\d)*)\ ?(?:days?|d)[,\ ]*)*';           // d (days)
3730  $pattern .= '(?:(?<hours>\d+(?:\.\d)*)\ ?(?:hours?|h)[,\ ]*)*';         // h (hours)
3731  $pattern .= '(?:(?<minutes>\d+(?:\.\d)*)\ ?(?:minutes?|min|m)[,\ ]*)*'; // m (minutes)
3732  $pattern .= '(?:(?<seconds>\d+(?:\.\d)*)\ ?(?:seconds?|sec|s))*';       // s (seconds)
3733  $pattern .= '$/';
3734
3735  if (!empty($age) && preg_match($pattern, $age, $matches))
3736  {
3737    $seconds  = $matches['seconds'];
3738    $seconds += $matches['years'] * 31536000; // year   = 365 * 24 * 60 * 60
3739    $seconds += $matches['months'] * 2628000; // month  = year / 12
3740    $seconds += $matches['weeks']   * 604800; // week   = 7 days
3741    $seconds += $matches['days']     * 86400; // day    = 24 * 60 * 60
3742    $seconds += $matches['hours']     * 3600; // hour   = 60 * 60
3743    $seconds += $matches['minutes']     * 60; // minute = 60
3744    $age = (int)$seconds;
3745
3746    return $age;
3747  }
3748
3749  return 0;
3750}
3751
3752/**
3753 * Convert age string to unixtime.
3754 *
3755 * This function convert age string to unixtime.
3756 *
3757 * Description and notes same as for age_to_seconds()
3758 *
3759 * Additional check if $age more than minimal age in seconds
3760 *
3761 * '3y 4M 6w 5d 3h 1m 3s' -> time() - 109191663
3762 * '3y4M6w5d3h1m3s'       -> time() - 109191663
3763 * '1.5w'                 -> time() - 907200
3764 * -886732     -> 0
3765 * 'Star wars' -> 0
3766 *
3767 * @param string $age
3768 * @return int
3769 */
3770// TESTME needs unit testing
3771function age_to_unixtime($age, $min_age = 1)
3772{
3773  $age = age_to_seconds($age);
3774  if ($age >= $min_age)
3775  {
3776    return time() - $age;
3777  }
3778  return 0;
3779}
3780
3781/**
3782 * Convert an variable to base64 encoded string
3783 *
3784 * This function converts any array or other variable to encoded string
3785 * which can be used in urls.
3786 * Can use serialize and json(default) methods.
3787 *
3788 * NOTE. In PHP < 5.4 json converts UTF-8 characters to Unicode escape sequences
3789 * also json rounds float numbers (98172397.1234567890 ==> 98172397.123457)
3790 *
3791 * @param mixed $var
3792 * @param string $method
3793 * @return string
3794 */
3795function var_encode($var, $method = 'json')
3796{
3797  switch ($method)
3798  {
3799    case 'serialize':
3800      $string = base64_encode(serialize($var));
3801      break;
3802    default:
3803      //$tmp = json_encode($var, OBS_JSON_ENCODE);
3804      //echo PHP_EOL . 'precision = ' . ini_get('precision') . "\n";
3805      //echo 'serialize_precision = ' . ini_get('serialize_precision');
3806      //echo("\n---\n"); var_dump($var); echo("\n---\n"); var_dump($tmp);
3807      $string = base64_encode(json_encode($var, OBS_JSON_ENCODE));
3808      break;
3809  }
3810  return $string;
3811}
3812
3813/**
3814 * Decode an previously encoded string by var_encode() to original variable
3815 *
3816 * This function converts base64 encoded string to original variable.
3817 * Can use serialize and json(default) methods.
3818 * If json/serialize not detected returns original var
3819 *
3820 * NOTE. In PHP < 5.4 json converts UTF-8 characters to Unicode escape sequences,
3821 * also json rounds float numbers (98172397.1234567890 ==> 98172397.123457)
3822 *
3823 * @param string $string
3824 * @return mixed
3825 */
3826function var_decode($string, $method = 'json')
3827{
3828  if ((strlen($string) % 4) > 0)
3829  {
3830    // BASE64 length must be multiple by 4
3831    return $string;
3832  }
3833  $value = base64_decode($string, TRUE);
3834  if ($value === FALSE)
3835  {
3836    // This is not base64 string, return original var
3837    return $string;
3838  }
3839
3840  switch ($method)
3841  {
3842    case 'serialize':
3843    case 'unserialize':
3844      if ($value === 'b:0;') { return FALSE; };
3845      $decoded = @unserialize($value);
3846      if ($decoded !== FALSE)
3847      {
3848        // Serialized encoded string detected
3849        return $decoded;
3850      }
3851      break;
3852    default:
3853      if ($string === 'bnVsbA==') { return NULL; };
3854      if (OBS_JSON_DECODE > 0)
3855      {
3856        $decoded = @json_decode($value, TRUE, 512, OBS_JSON_DECODE);
3857      } else {
3858        // Prevent to broke on old php (5.3), where supported only 3 params
3859        $decoded = @json_decode($value, TRUE, 512);
3860      }
3861      switch (json_last_error())
3862      {
3863        case JSON_ERROR_STATE_MISMATCH:
3864        case JSON_ERROR_SYNTAX:
3865          // Critical json errors, return original string
3866          break;
3867        case JSON_ERROR_NONE:
3868        default:
3869          if ($decoded !== NULL)
3870          {
3871            // JSON encoded string detected
3872            return $decoded;
3873          }
3874      }
3875      break;
3876  }
3877
3878  // In all other cases return original var
3879  return $string;
3880}
3881
3882/**
3883 * Parse number with units to numeric.
3884 *
3885 * This function converts numbers with units (e.g. 100MB) to their value
3886 * in bytes (e.g. 104857600).
3887 *
3888 * @param string $str
3889 * @param int Use custom rigid unit base (1000 or 1024)
3890 * @return int
3891 */
3892function unit_string_to_numeric($str, $unit_base = NULL)
3893{
3894  // If it's already a number, return original string
3895  if (is_numeric($str)) { return (float)$str; }
3896
3897  preg_match('/(\d+\.?\d*)\ ?(\w+)/', $str, $matches);
3898
3899  // Error, return original string
3900  if (!is_numeric($matches[1])) { return $str; }
3901
3902  if (is_numeric($unit_base) && ($unit_base == 1000 || $unit_base == 1024))
3903  {
3904    // Use rigid unit base, this interprets any units with hard multiplier base
3905    $base = $unit_base;
3906  }
3907
3908  switch ($matches[2])
3909  {
3910    case '':
3911    case 'B':
3912    case 'b':
3913    case 'bit':
3914    case 'bps':
3915    case 'Bps':
3916    case 'byte':
3917    case 'Byte':
3918      $power = 0;
3919      $base = isset($base) ? $base : 1024;
3920      break;
3921    case 'K':
3922    case 'k':
3923    case 'kB':
3924    case 'kByte':
3925    case 'kbyte':
3926      $power = 1;
3927      $base = isset($base) ? $base : 1024;
3928      break;
3929    case 'kb':
3930    case 'kBps':
3931    case 'kbit':
3932    case 'kbps':
3933      $power = 1;
3934      $base = isset($base) ? $base : 1000;
3935      break;
3936    case 'M':
3937    case 'MB':
3938    case 'MByte':
3939    case 'Mbyte':
3940      $power = 2;
3941      $base = isset($base) ? $base : 1024;
3942      break;
3943    case 'Mb':
3944    case 'MBps':
3945    case 'Mbit':
3946    case 'Mbps':
3947      $power = 2;
3948      $base = isset($base) ? $base : 1000;
3949      break;
3950    case 'G':
3951    case 'GB':
3952    case 'GByte':
3953    case 'Gbyte':
3954      $power = 3;
3955      $base = isset($base) ? $base : 1024;
3956      break;
3957    case 'Gb':
3958    case 'GBps':
3959    case 'Gbit':
3960    case 'Gbps':
3961      $power = 3;
3962      $base = isset($base) ? $base : 1000;
3963      break;
3964    case 'T':
3965    case 'TB':
3966    case 'TByte':
3967    case 'Tbyte':
3968      $power = 4;
3969      $base = isset($base) ? $base : 1024;
3970      break;
3971    case 'Tb':
3972    case 'TBps':
3973    case 'Tbit':
3974    case 'Tbps':
3975      $power = 4;
3976      $base = isset($base) ? $base : 1000;
3977      break;
3978    default:
3979      $power = 0;
3980      $base = isset($base) ? $base : 1024;
3981      break;
3982  }
3983  $multiplier = pow($base, $power);
3984
3985  return (float)($matches[1] * $multiplier);
3986}
3987
3988/**
3989 * Generate Unique ID from string, based on crc32b hash. This ID unique for specific string and not changed over next call.
3990 *
3991 * @param string $string String
3992 * @return int Unique ID
3993 */
3994function string_to_id($string)
3995{
3996  return hexdec(hash("crc32b", $string));
3997}
3998
3999/**
4000 * Convert value of sensor from known unit to defined SI unit (used in poller/discovery)
4001 *
4002 * @param float|string $value Value in non standard unit
4003 * @param string       $unit Unit name/symbol
4004 * @param string       $type Type of value (optional, if same unit can used for multiple types)
4005 * @return float|string Value converted to standard (SI) unit
4006 */
4007function value_to_si($value, $unit, $type = NULL)
4008{
4009  if (!is_numeric($value)) { return $value; } // Just return original value if not numeric
4010
4011  $unit_lower = strtolower($unit);
4012  switch ($unit_lower)
4013  {
4014    case 'f':
4015    case 'fahrenheit':
4016    case 'k':
4017    case 'kelvin':
4018      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Temperature($value, $unit);
4019      $si_value = $value_from->toUnit('C');
4020      if ($si_value < -273.15)
4021      {
4022        // Physically incorrect value
4023        $si_value = FALSE;
4024      }
4025
4026      $type  = 'temperature';
4027      $from  = $value    . " $unit";
4028      $to    = $si_value . ' Celsius';
4029      break;
4030
4031    case 'c':
4032    case 'celsius':
4033      // not convert, just keep correct value
4034      $type  = 'temperature';
4035      break;
4036
4037    case 'w':
4038    case 'watts':
4039      if ($type == 'dbm')
4040      {
4041        // Used when Power convert to dBm
4042        // https://en.wikipedia.org/wiki/DBm
4043        // https://www.everythingrf.com/rf-calculators/watt-to-dbm
4044        if ($value > 0)
4045        {
4046          $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, $unit);
4047          $si_value = $value_from->toUnit('dBm');
4048
4049          $from  = $value    . " $unit";
4050          $to    = $si_value . ' dBm';
4051        } else {
4052          $si_value = FALSE;
4053          $from  = $value    . ' W';
4054          $to    = 'FALSE';
4055        }
4056      } else {
4057        // not convert, just keep correct value
4058        $type  = 'power';
4059      }
4060      break;
4061
4062    case 'dbm':
4063      if ($type == 'power')
4064      {
4065        // Used when Power convert to dBm
4066        // https://en.wikipedia.org/wiki/DBm
4067        // https://www.everythingrf.com/rf-calculators/dbm-to-watts
4068        $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, $unit);
4069        $si_value = $value_from->toUnit('W');
4070
4071        $from  = $value    . " $unit";
4072        $to    = $si_value . ' W';
4073
4074      } else {
4075        // not convert, just keep correct value
4076        $type  = 'dbm';
4077      }
4078      break;
4079
4080    case 'psi':
4081    case 'ksi':
4082    case 'Mpsi':
4083      // https://en.wikipedia.org/wiki/Pounds_per_square_inch
4084      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Pressure($value, $unit);
4085      $si_value = $value_from->toUnit('Pa');
4086
4087      $type  = 'pressure';
4088      $from  = $value    . " $unit";
4089      $to    = $si_value . ' Pa';
4090      break;
4091
4092    case 'ft/s':
4093    case 'fps':
4094    case 'ft/min':
4095    case 'fpm':
4096    case 'lfm': // linear feet per minute
4097    case 'mph': // Miles per hour
4098    case 'mps': // Miles per second
4099    case 'm/min': // Meter per minute
4100    case 'km/h':  // Kilometer per hour
4101      // Any velocity units:
4102      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Velocity($value, $unit);
4103      $si_value = $value_from->toUnit('m/s');
4104
4105      $type  = 'velocity';
4106      $from  = $value    . " $unit";
4107      $to    = $si_value . ' m/s';
4108      break;
4109
4110    case 'ft3/s':
4111    case 'cfs':
4112    case 'ft3/min':
4113    case 'cfm':
4114    case 'gpd': // US (gallon per day)
4115    case 'gpm': // US (gallon per min)
4116    case 'l/min':
4117    case 'lpm':
4118    case 'cmh':
4119    case 'm3/h':
4120    case 'cmm':
4121    case 'm3/min':
4122      if ($type == 'waterflow')
4123      {
4124        // Waterflow default unit is L/s
4125        $si_unit = 'L/s';
4126      }
4127      else if ($type == 'airflow')
4128      {
4129        // Use for Airflow imperial unit CFM (Cubic foot per minute) as more common industry standard
4130        $si_unit = 'CFM';
4131      } else {
4132        // For future
4133        $si_unit = 'm^3/s';
4134      }
4135      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\VolumeFlow($value, $unit);
4136      $si_value = $value_from->toUnit($si_unit);
4137
4138      $from  = $value    . " $unit";
4139      $to    = $si_value . " $si_unit";
4140      break;
4141
4142    default:
4143      // Ability to use any custom function to convert value based on unit name
4144      $function_name = 'value_unit_'.$unit_lower; // ie: value_unit_ekinops_dbm1($value) or value_unit_accuenergy($value)
4145      if (function_exists($function_name))
4146      {
4147        $si_value = call_user_func_array($function_name, array($value));
4148
4149        //$type  = $unit;
4150        $from  = $value . " $unit";
4151        $to    = $si_value;
4152      }
4153  }
4154
4155  if (isset($si_value))
4156  {
4157    print_debug('Converted '.strtoupper($type).' value: '.$from.' -> '.$to);
4158    return $si_value;
4159  }
4160
4161  return $value; // Fallback original value
4162}
4163
4164/**
4165 * Convert value of sensor from known unit to defined SI unit (used in poller/discovery)
4166 *
4167 * @param float|string $value Value
4168 * @param string       $unit_from Unit name/symbol for value
4169 * @param string       $class Type of value
4170 * @param string|array $unit_to Unit name/symbol for convert value (by default used sensor class default unit)
4171 * @return array       Array with values converted to unit_from
4172 */
4173function value_to_units($value, $unit_from, $class, $unit_to = [])
4174{
4175  global $config;
4176
4177  // Convert symbols to supported by lib units
4178  $unit_from = str_replace(['<sup>', '</sup>'], ['^', ''], $unit_from); // I.e. mg/m<sup>3</sup> => mg/m^3
4179  $unit_from = html_entity_decode($unit_from);                          // I.e. &deg;C => °C
4180
4181  // Non numeric values
4182  if (!is_numeric($value))
4183  {
4184    return [$unit_from => $value];
4185  }
4186
4187  switch ($class)
4188  {
4189    case 'temperature':
4190      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Temperature($value, $unit_from);
4191      break;
4192
4193    case 'pressure':
4194      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Pressure($value, $unit_from);
4195      break;
4196
4197    case 'power':
4198    case 'dbm':
4199      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Power($value, $unit_from);
4200      break;
4201
4202    case 'waterflow':
4203    case 'airflow':
4204      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\VolumeFlow($value, $unit_from);
4205      break;
4206
4207    case 'velocity':
4208      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Velocity($value, $unit_from);
4209      break;
4210
4211    case 'lifetime':
4212    case 'uptime':
4213    case 'time':
4214      if ($unit_from == '') { $unit_from = 's'; }
4215      $value_from = new PhpUnitsOfMeasure\PhysicalQuantity\Time($value, $unit_from);
4216      break;
4217
4218    default:
4219      // Unknown, return original value
4220      return [$unit_from => $value];
4221  }
4222
4223  // Use our default unit (if not passed)
4224  if (empty($unit_to) && isset($config['sensor_types'][$class]['symbol']))
4225  {
4226    $unit_to = $config['sensor_types'][$class]['symbol'];
4227  }
4228
4229  // Convert to units
4230  $units = [];
4231  foreach ((array)$unit_to as $to)
4232  {
4233    // Convert symbols to supported by lib units
4234    $tou = str_replace(['<sup>', '</sup>'], ['^', ''], $to); // I.e. mg/m<sup>3</sup> => mg/m^3
4235    $tou = html_entity_decode($tou);                         // I.e. &deg;C => °C
4236
4237    $units[$to] = $value_from->toUnit($tou);
4238  }
4239
4240  return $units;
4241}
4242
4243/**
4244 * Replace all newlines in string to space char (except string begin and end)
4245 *
4246 * @param string $string Input string
4247 * @return string Output string without NL characters
4248 */
4249function nl2space($string)
4250{
4251  if (!is_string($string) || $string == '')
4252  {
4253    return $string;
4254  }
4255
4256  $string = trim($string, "\n\r");
4257  return preg_replace('/ ?(\r\n|\r|\n) ?/', ' ', $string);
4258}
4259
4260/**
4261 * This noob function replace windows/mac newline character to unix newline
4262 *
4263 * @param string $string Input string
4264 * @return string Clean output string
4265 */
4266function nl2nl($string)
4267{
4268  if (!is_string($string) || $string == '')
4269  {
4270    return $string;
4271  }
4272
4273  return preg_replace('/\r\n|\r/', PHP_EOL, $string);
4274}
4275
4276/**
4277 * Microtime
4278 *
4279 * This function returns the current Unix timestamp seconds, accurate to the
4280 * nearest microsecond.
4281 *
4282 * @return float
4283 */
4284function utime()
4285{
4286  return microtime(TRUE);
4287}
4288
4289
4290/**
4291 * Bitwise checking if flags set
4292 *
4293 * Examples:
4294 *  if (is_flag_set(FLAG_A, some_var)) // eg: some_var = 0b01100000000010
4295 *  if (is_flag_set(FLAG_A | FLAG_F | FLAG_L, some_var)) // to check if at least one flag is set
4296 *  if (is_flag_set(FLAG_A | FLAG_J | FLAG_M | FLAG_D, some_var, TRUE)) // to check if all flags are set
4297 *
4298 * @param int $flag Checked flags
4299 * @param int $param Parameter for checking
4300 * @param bool $all Check all flags
4301 * @return bool
4302 */
4303function is_flag_set($flag, $param, $all = FALSE)
4304{
4305  $set = $flag & $param;
4306
4307  if                ($set and !$all) { return TRUE; } // at least one of the flags passed is set
4308  else if ($all and ($set == $flag)) { return TRUE; } // to check that all flags are set
4309
4310  return FALSE;
4311}
4312
4313// DOCME needs phpdoc block
4314// TESTME needs unit testing
4315function is_ssl()
4316{
4317  if (isset($_SERVER['HTTPS']))
4318  {
4319    if ('on' == strtolower($_SERVER['HTTPS'])) { return TRUE; }
4320    if ('1' == $_SERVER['HTTPS']) { return TRUE; }
4321  }
4322  else if (isset($_SERVER['SERVER_PORT']) && ('443' == $_SERVER['SERVER_PORT']))
4323  {
4324    return TRUE;
4325  }
4326  else if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
4327  {
4328    return TRUE;
4329  }
4330
4331  return FALSE;
4332}
4333
4334/**
4335 * This function return object with recursive directory iterator.
4336 *
4337 * @param $dir
4338 *
4339 * @return RecursiveIteratorIterator
4340 */
4341function get_recursive_directory_iterator($dir)
4342{
4343  return new RecursiveIteratorIterator(
4344    new RecursiveDirectoryIterator($dir, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS),
4345    RecursiveIteratorIterator::LEAVES_ONLY,
4346    RecursiveIteratorIterator::CATCH_GET_CHILD
4347  );
4348}
4349
4350// Nice PHP (7.3) compat functions
4351
4352if (!function_exists('array_key_first'))
4353{
4354  /**
4355   * Gets the first key of an array
4356   *
4357   * @param array $array
4358   * @return mixed
4359   */
4360  function array_key_first($array)
4361  {
4362    return $array && is_array($array) ? array_keys($array)[0] : NULL;
4363  }
4364}
4365
4366if (!function_exists('array_key_last'))
4367{
4368  /**
4369   * Gets the last key of an array
4370   *
4371   * @param array $array
4372   * @return mixed
4373   */
4374  function array_key_last($array)
4375  {
4376    return $array && is_array($array) ? array_keys($array)[count($array) - 1] : NULL;
4377  }
4378}
4379
4380// EOF
4381