1<?php
2
3/**
4 * Observium
5 *
6 *   This file is part of Observium.
7 *
8 * @package    observium
9 * @subpackage functions
10 * @author     Adam Armstrong <adama@observium.org>
11 * @copyright  (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited
12 *
13 */
14
15// Observium Includes
16
17require_once($config['install_dir'] . "/includes/common.inc.php");
18include_once($config['install_dir'] . "/includes/encrypt.inc.php");
19include_once($config['install_dir'] . "/includes/rrdtool.inc.php");
20include_once($config['install_dir'] . "/includes/influx.inc.php");
21include_once($config['install_dir'] . "/includes/syslog.inc.php");
22include_once($config['install_dir'] . "/includes/rewrites.inc.php");
23include_once($config['install_dir'] . "/includes/templates.inc.php");
24include_once($config['install_dir'] . "/includes/snmp.inc.php");
25include_once($config['install_dir'] . "/includes/services.inc.php");
26include_once($config['install_dir'] . "/includes/entities.inc.php");
27include_once($config['install_dir'] . "/includes/wifi.inc.php");
28include_once($config['install_dir'] . "/includes/geolocation.inc.php");
29
30include_once($config['install_dir'] . "/includes/alerts.inc.php");
31
32//if (OBSERVIUM_EDITION != 'community') // OBSERVIUM_EDITION - not defined here..
33//{
34foreach (array('groups', 'billing', // Not exist in community edition
35               'community',         // community edition specific
36               'custom',            // custom functions, i.e. short_hostname
37              ) as $entry)
38{
39  $file = $config['install_dir'] . '/includes/' . $entry . '.inc.php';
40  if (is_file($file)) { include_once($file); }
41}
42
43
44// DOCME needs phpdoc block
45// Send to AMQP via UDP-based python proxy.
46// TESTME needs unit testing
47// MOVEME to includes/common.inc.php
48function messagebus_send($message)
49{
50  global $config;
51
52  if ($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP))
53  {
54    $message = json_encode($message);
55    print_debug('Sending JSON via AMQP: ' . $message);
56    socket_sendto($socket, $message, strlen($message), 0, $config['amqp']['proxy']['host'], $config['amqp']['proxy']['port']);
57    socket_close($socket);
58    return TRUE;
59  } else {
60    print_error("Failed to create UDP socket towards AMQP proxy.");
61    return FALSE;
62  }
63}
64
65
66
67/**
68 * Transforms a given string using an array of different transformations, in order.
69 *
70 * @param string $string Original string to be transformed
71 * @param array $transformations Transformation array
72 *
73 * Available transformations:
74 *   'action' => 'prepend'    Prepend static 'string'
75 *   'action' => 'append'     Append static 'string'
76 *   'action' => 'trim'       Trim 'characters' from both sides of the string
77 *   'action' => 'ltrim'      Trim 'characters' from the left of the string
78 *   'action' => 'rtrim'      Trim 'characters' from the right of the string
79 *   'action' => 'replace'    Case-sensitively replace 'from' string by 'to'; 'from' can be an array of strings
80 *   'action' => 'ireplace'   Case-insensitively replace 'from' string by 'to'; 'from' can be an array of strings
81 *   'action' => 'timeticks'  Convert standart Timeticks to seconds
82 *   'action' => 'explode'    Explode string by 'delimiter' (default ' ') and fetch array element (first (default), last)
83 *   'action' => 'regex_replace'  Replace 'from' with 'to'.
84 *
85 * @return string Transformed string
86 */
87function string_transform($string, $transformations)
88{
89  if (!is_array($transformations) || empty($transformations))
90  {
91    // Bail out if no transformations are given
92    return $string;
93  }
94
95  // Simplify single action definition with less array nesting
96  if (isset($transformations['action']))
97  {
98    $transformations = array($transformations);
99  }
100
101  foreach ($transformations as $transformation)
102  {
103    $msg = "  String '$string' transformed by action [".$transformation['action']."] to: ";
104    switch ($transformation['action'])
105    {
106      case 'prepend':
107        $string = $transformation['string'] . $string;
108        break;
109
110      case 'append':
111        $string .= $transformation['string'];
112        break;
113
114      case 'upper':
115        $string = strtoupper($string);
116        break;
117
118      case 'lower':
119        $string = strtolower($string);
120        break;
121
122      case 'nicecase':
123        $string = nicecase($string);
124        break;
125
126      case 'trim':
127      case 'ltrim':
128      case 'rtrim':
129        if (isset($transformation['chars']) && !isset($transformation['characters']))
130        {
131          // Just simple for brain memory key
132          $transformation['characters'] = $transformation['chars'];
133        }
134        if (!isset($transformation['characters']))
135        {
136          $transformation['characters'] = " \t\n\r\0\x0B";
137        }
138
139        if ($transformation['action'] == 'rtrim')
140        {
141          $string = rtrim($string, $transformation['characters']);
142        }
143        else if ($transformation['action'] == 'ltrim')
144        {
145          $string = ltrim($string, $transformation['characters']);
146        } else {
147          $string = trim($string, $transformation['characters']);
148        }
149        break;
150
151      case 'replace':
152        $string = str_replace($transformation['from'], $transformation['to'], $string);
153        break;
154
155      case 'ireplace':
156        $string = str_ireplace($transformation['from'], $transformation['to'], $string);
157        break;
158
159      case 'regex_replace':
160      case 'preg_replace':
161        $tmp_string = preg_replace($transformation['from'], $transformation['to'], $string);
162        if (strlen($tmp_string))
163        {
164          $string = $tmp_string;
165        }
166        break;
167
168      case 'regex_match':
169      case 'preg_match':
170        if (preg_match($transformation['from'], $string, $matches))
171        {
172          $string = array_tag_replace($matches, $transformation['to']);
173        }
174        break;
175
176      case 'map':
177        // Map string by key -> value
178        if (is_array($transformation['map']) && isset($transformation['map'][$string]))
179        {
180          $string = $transformation['map'][$string];
181        }
182        break;
183
184      case 'timeticks':
185        // Timeticks: (2542831) 7:03:48.31
186        $string = timeticks_to_sec($string);
187        break;
188
189      case 'asdot':
190        // BGP 32bit ASN from asdot to plain
191        $string = bgp_asdot_to_asplain($string);
192        break;
193
194      case 'units':
195        // 200kbps -> 200000, 50M -> 52428800
196        $string = unit_string_to_numeric($string);
197        break;
198
199      case 'entity_name':
200        $string = rewrite_entity_name($string);
201        break;
202
203      case 'explode':
204        // String delimiter (default is single space " ")
205        if (isset($transformation['delimiter']) && strlen($transformation['delimiter']))
206        {
207          $delimiter = $transformation['delimiter'];
208        } else {
209          $delimiter = ' ';
210        }
211        $array = explode($delimiter, $string);
212        // Get array index (default is first)
213        if (!isset($transformation['index']))
214        {
215          $transformation['index'] = 'first';
216        }
217        switch ($transformation['index'])
218        {
219          case 'first':
220          case 'begin':
221            $string = array_shift($array);
222            break;
223          case 'last':
224          case 'end':
225            $string = array_pop($array);
226            break;
227          default:
228
229            if (strlen($array[$transformation['index']]))
230            {
231              $string = $array[$transformation['index']];
232            }
233        }
234        break;
235
236      default:
237        // FIXME echo HALP, unknown transformation!
238        break;
239    }
240    print_debug($msg . "'$string'");
241  }
242
243  return $string;
244}
245
246// DOCME needs phpdoc block
247// Sorts an $array by a passed field.
248// TESTME needs unit testing
249// MOVEME to includes/common.inc.php
250function array_sort($array, $on, $order='SORT_ASC')
251{
252  $new_array = array();
253  $sortable_array = array();
254
255  if (count($array) > 0)
256  {
257    foreach ($array as $k => $v)
258    {
259      if (is_array($v))
260      {
261        foreach ($v as $k2 => $v2)
262        {
263          if ($k2 == $on)
264          {
265            $sortable_array[$k] = $v2;
266          }
267        }
268      } else {
269        $sortable_array[$k] = $v;
270      }
271    }
272
273    switch ($order)
274    {
275      case 'SORT_ASC':
276        asort($sortable_array);
277        break;
278      case 'SORT_DESC':
279        arsort($sortable_array);
280        break;
281    }
282
283    foreach ($sortable_array as $k => $v)
284    {
285      $new_array[$k] = $array[$k];
286    }
287  }
288
289  return $new_array;
290}
291
292/** hex2float
293* (Convert 8 digit hexadecimal value to float (single-precision 32bits)
294* Accepts 8 digit hexadecimal values in a string
295* @usage:
296* hex2float32n("429241f0"); returns -> "73.128784179688"
297* */
298function hex2float($number) {
299    $binfinal = sprintf("%032b",hexdec($number));
300    $sign = substr($binfinal, 0, 1);
301    $exp = substr($binfinal, 1, 8);
302    $mantissa = "1".substr($binfinal, 9);
303    $mantissa = str_split($mantissa);
304    $exp = bindec($exp)-127;
305    $significand=0;
306    for ($i = 0; $i < 24; $i++) {
307        $significand += (1 / pow(2,$i))*$mantissa[$i];
308    }
309    return $significand * pow(2,$exp) * ($sign*-2+1);
310}
311
312// A function to process numerical values according to a $scale value
313// Functionised to allow us to have "magic" scales which do special things
314// Initially used for dec>hex>float values used by accuview
315function scale_value($value, $scale)
316{
317
318  if ($scale == '161616') // This is used by Accuview Accuvim II
319  {
320    //CLEANME. Not required anymore, use unit name "accuenergy"
321    return hex2float(dechex($value));
322  } else if($scale != 0) {
323    return $value * $scale;
324  } else {
325    return $value;
326  }
327
328}
329
330/**
331 * Custom value unit conversion functions for some vendors,
332 * who do not know how use snmp float conversions,
333 * do not know physics, mathematics and in general badly studied at school
334 */
335
336function value_unit_accuenergy($value)
337{
338  return hex2float(dechex($value));
339}
340
341// See: https://jira.observium.org/browse/OBS-2941
342// Oids "pm10010mpMesrlineNetRxInputPwrPortn" and "pm10010mpMesrlineNetTxLaserOutputPwrPortn" in EKINOPS-Pm10010mp-MIB
343// If AV<32768:  Tx_Pwr(dBm) = AV/100
344// If AV>=32768: Tx_Pwr(dBm) = (AV-65536)/100
345function value_unit_ekinops_dbm1($value)
346{
347  if ($value >= 32768 && $value <= 65536)
348  {
349    return ($value - 65536) / 100;
350  }
351  else if ($value > 65536 || $value < 0)
352  {
353    return FALSE;
354  }
355
356  return $value / 100;
357}
358
359// See: https://jira.observium.org/browse/OBS-2941
360// oids "pm10010mpMesrclientNetTxPwrPortn" and "pm10010mpMesrclientNetRxPwrPortn" in EKINOPS-Pm10010mp-MIB
361// Power = 10*log(AV)-40) (Unit = dBm)
362function value_unit_ekinops_dbm2($value)
363{
364  //$si_value = 10 * log10($value) + 30; // this is how watts converted to dbm
365  $si_value = 10 * log10($value) - 40; // BUT this how convert it EKINOPS.... WHY??????
366
367  return $si_value;
368}
369
370// Another sort array function
371// http://php.net/manual/en/function.array-multisort.php#100534
372// DOCME needs phpdoc block
373// TESTME needs unit testing
374// MOVEME to includes/common.inc.php
375function array_sort_by()
376{
377  $args = func_get_args();
378  $data = array_shift($args);
379  foreach ($args as $n => $field)
380  {
381    if (is_string($field))
382    {
383      $tmp = array();
384      foreach ($data as $key => $row)
385      {
386        $tmp[$key] = $row[$field];
387      }
388      $args[$n] = $tmp;
389    }
390  }
391  $args[] = &$data;
392  call_user_func_array('array_multisort', $args);
393  return array_pop($args);
394}
395
396/**
397 * Given two arrays, the function diff will return an array of the changes.
398 *
399 * @param array $old First array
400 * @param array $new Second array
401 * @return array Array with diffs and same elements
402 */
403function diff($old, $new)
404{
405
406  $matrix = array();
407  $maxlen = 0;
408  foreach ($old as $oindex => $ovalue)
409  {
410    $nkeys = array_keys($new, $ovalue);
411    foreach ($nkeys as $nindex)
412    {
413      $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ? $matrix[$oindex - 1][$nindex - 1] + 1 : 1;
414      if ($matrix[$oindex][$nindex] > $maxlen)
415      {
416        $maxlen = $matrix[$oindex][$nindex];
417        $omax = $oindex + 1 - $maxlen;
418        $nmax = $nindex + 1 - $maxlen;
419      }
420    }
421  }
422  if ($maxlen == 0)
423  {
424    return array(array('d'=>$old, 'i'=>$new));
425  }
426
427  return array_merge(
428    diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
429    array_slice($new, $nmax, $maxlen),
430    diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)));
431}
432
433/**
434 * Return similar part of two strings (or empty)
435 *
436 * @param string $old First string
437 * @param string $new Second string
438 * @return string Similar part of two strings
439 */
440function str_similar($old, $new)
441{
442
443  $ret = array();
444  $diff = diff(preg_split("/[\s]+/u", $old), preg_split("/[\s]+/u", $new));
445  foreach ($diff as $k)
446  {
447    if (!is_array($k))
448    {
449      $ret[] = $k;
450    }
451  }
452  return implode(' ', $ret);
453}
454
455/**
456 * Return sets of all similar strings from passed array.
457 *
458 * @param array $array Array with strings for find similar
459 * @param boolean $return_flip If TRUE return pairs String -> Similar part,
460 *                             instead vice versa by default return set of arrays Similar part -> Strings
461 * @param integer $similarity Percent of similarity compared string, mostly common is 90%
462 * @return array Array with sets of similar strings
463 */
464function find_similar($array, $return_flip = FALSE, $similarity = 89)
465{
466  natsort($array);
467  //var_dump($array);
468  $array2 = $array;
469  $same_array = array();
470  $same_array_flip = array();
471
472  // $i = 0; // DEBUG
473  foreach ($array as $k => $old)
474  {
475    foreach ($array2 as $k2 => $new)
476    {
477      if ($k === $k2) { continue; } // Skip same array elements
478
479      // Detect string similarity
480      similar_text($old, $new, $perc);
481
482      // $i++; echo "$i ($perc %): '$old' <> '$new' (".str_similar($old, $new).")\n"; // DEBUG
483
484      if ($perc > $similarity)
485      {
486        if (isset($same_array_flip[$old]))
487        {
488          // This is found already similar string by previous round(s)
489          $same = $same_array_flip[$old];
490          $same_array_flip[$new] = $same;
491        }
492        else if (isset($same_array_flip[$new]))
493        {
494          // This is found already similar string by previous round(s)
495          $same = $same_array_flip[$new];
496          $same_array_flip[$old] = $same;
497        } else {
498          // New similarity, get similar string part
499          $same = str_similar($old, $new);
500
501          // Return array pairs as:
502          // String -> Similar part
503          $same_array_flip[$old] = $same;
504          $same_array_flip[$new] = $same;
505        }
506
507        // Return array elements as:
508        // Similar part -> Strings
509        if (!isset($same_array[$same]) || !in_array($old, $same_array[$same])) {
510          $same_array[$same][] = $old;
511        }
512        $same_array[$same][] = $new;
513
514        unset($array2[$k]); // Remove array element if similar found
515
516        break;
517      }
518    }
519    if (!isset($same_array_flip[$old]))
520    {
521      // Similarity not found, just add as single string
522      $same_array_flip[$old] = $old;
523      $same_array[$old][] = $old;
524    }
525  }
526
527  if ($return_flip)
528  {
529    // Return array pairs as:
530    // String -> Similar part
531    return $same_array_flip;
532  } else {
533    // Return array elements as:
534    // Similar part -> Strings
535    return $same_array;
536  }
537}
538
539/**
540 * Includes filename with global config variable
541 *
542 * @param string $filename Filename for include
543 *
544 * @return boolean Status of include
545 */
546function include_wrapper($filename)
547{
548  global $config;
549
550  $status = include($filename);
551
552  return (boolean)$status;
553}
554
555// Strip all non-alphanumeric characters from a string.
556// DOCME needs phpdoc block
557// TESTME needs unit testing
558// MOVEME to includes/common.inc.php
559function only_alphanumeric($string)
560{
561  return preg_replace('/[^a-zA-Z0-9]/', '', $string);
562}
563
564/**
565 * Detect the device's OS
566 *
567 * Order for detect:
568 *  if device rechecking (know old os): complex discovery (all), sysObjectID, sysDescr, file check
569 *  if device first checking:           complex discovery (except network), sysObjectID, sysDescr, complex discovery (network), file check
570 *
571 * @param array $device Device array
572 * @return string Detected device os name
573 */
574function get_device_os($device)
575{
576  global $config, $table_rows, $cache_os;
577
578  // If $recheck sets as TRUE, verified that 'os' corresponds to the old value.
579  // recheck only if old device exist in definitions
580  $recheck = isset($config['os'][$device['os']]);
581
582  $sysDescr     = snmp_fix_string(snmp_get_oid($device, 'sysDescr.0', 'SNMPv2-MIB'));
583  $sysDescr_ok  = $GLOBALS['snmp_status'] || $GLOBALS['snmp_error_code'] === 1; // Allow empty response for sysDescr (not timeouts)
584  $sysObjectID  = snmp_cache_sysObjectID($device);
585  /*
586  $sysObjectID  = snmp_get($device, 'sysObjectID.0', '-Ovqn', 'SNMPv2-MIB');
587  if (strpos($sysObjectID, 'Wrong Type') !== FALSE)
588  {
589    // Wrong Type (should be OBJECT IDENTIFIER): "1.3.6.1.4.1.25651.1.2"
590    list(, $sysObjectID) = explode(':', $sysObjectID);
591    $sysObjectID = '.'.trim($sysObjectID, ' ."');
592  }
593  */
594
595  // Cache discovery os definitions
596  cache_discovery_definitions();
597  $discovery_os = $GLOBALS['cache']['discovery_os'];
598  $cache_os = array();
599
600  $table_rows    = array();
601  $table_opts    = array('max-table-width' => TRUE); // Set maximum table width as available columns in terminal
602  $table_headers = array('%WOID%n', '');
603  $table_rows[] = array('sysDescr',    $sysDescr);
604  $table_rows[] = array('sysObjectID', $sysObjectID);
605 	print_cli_table($table_rows, $table_headers, NULL, $table_opts);
606  //print_debug("Detect OS. sysDescr: '$sysDescr', sysObjectID: '$sysObjectID'");
607
608  $table_rows    = array(); // Reinit
609  //$table_opts    = array('max-table-width' => 200);
610  $table_headers = array('%WOID%n', '%WMatched definition%n', '');
611  // By first check all sysObjectID
612  foreach ($discovery_os['sysObjectID'] as $def => $cos)
613  {
614    if (match_sysObjectID($sysObjectID, $def))
615    {
616      // Store matched OS, but by first need check by complex discovery arrays!
617      $sysObjectID_def = $def;
618      $sysObjectID_os  = $cos;
619      break;
620    }
621  }
622
623  if ($recheck)
624  {
625    $table_desc = 'Re-Detect OS matched';
626    $old_os = $device['os'];
627
628    if (!$sysDescr_ok && !empty($old_os))
629    {
630      // If sysDescr empty - return old os, because some snmp error
631      // print_debug("ERROR: sysDescr not received, OS re-check stopped.");
632      // return $old_os;
633    }
634
635    // Recheck by complex discovery array
636    // Yes, before sysObjectID, because complex more accurate and can intersect with it!
637    foreach ($discovery_os['discovery'][$old_os] as $def)
638    {
639      if (match_discovery_os($sysObjectID, $sysDescr, $def, $device))
640      {
641        print_cli_table($table_rows, $table_headers, $table_desc . " ($old_os: ".$config['os'][$old_os]['text'].'):', $table_opts);
642        return $old_os;
643      }
644    }
645    foreach ($discovery_os['discovery_network'][$old_os] as $def)
646    {
647      if (match_discovery_os($sysObjectID, $sysDescr, $def, $device))
648      {
649        print_cli_table($table_rows, $table_headers, $table_desc . " ($old_os: ".$config['os'][$old_os]['text'].'):', $table_opts);
650        return $old_os;
651      }
652    }
653
654    /** DISABLED.
655     * Recheck only by complex, networked and file rules
656
657    // Recheck by sysObjectID
658    if ($sysObjectID_os)
659    {
660      // If OS detected by sysObjectID just return it
661      $table_rows[] = array('sysObjectID', $sysObjectID_def, $sysObjectID);
662      print_cli_table($table_rows, $table_headers, $table_desc . " ($old_os: ".$config['os'][$old_os]['text'].'):', $table_opts);
663      return $sysObjectID_os;
664    }
665
666    // Recheck by sysDescr from definitions
667    foreach ($discovery_os['sysDescr'][$old_os] as $pattern)
668    {
669      if (preg_match($pattern, $sysDescr))
670      {
671        $table_rows[] = array('sysDescr', $pattern, $sysDescr);
672        print_cli_table($table_rows, $table_headers, $table_desc . " ($old_os: ".$config['os'][$old_os]['text'].'):', $table_opts);
673        return $old_os;
674      }
675    }
676    */
677
678    // Recheck by include file (moved to end!)
679
680    // Else full recheck 'os'!
681    unset($os, $file);
682
683  } // End recheck
684
685  $table_desc = 'Detect OS matched';
686
687  // Check by complex discovery arrays (except networked)
688  // Yes, before sysObjectID, because complex more accurate and can intersect with it!
689  foreach ($discovery_os['discovery'] as $cos => $defs)
690  {
691    foreach ($defs as $def)
692    {
693      if (match_discovery_os($sysObjectID, $sysDescr, $def, $device)) { $os = $cos; break 2; }
694    }
695  }
696
697  // Check by sysObjectID
698  if (!$os && $sysObjectID_os)
699  {
700    // If OS detected by sysObjectID just return it
701    $os = $sysObjectID_os;
702    $table_rows[] = array('sysObjectID', $sysObjectID_def, $sysObjectID);
703    print_cli_table($table_rows, $table_headers, $table_desc . " ($os: ".$config['os'][$os]['text'].'):', $table_opts);
704    return $os;
705  }
706
707  if (!$os && $sysDescr)
708  {
709    // Check by sysDescr from definitions
710    foreach ($discovery_os['sysDescr'] as $cos => $patterns)
711    {
712      foreach ($patterns as $pattern)
713      {
714        if (preg_match($pattern, $sysDescr))
715        {
716          $table_rows[] = array('sysDescr', $pattern, $sysDescr);
717          $os = $cos;
718          break 2;
719        }
720      }
721    }
722  }
723
724  // Check by complex discovery arrays, now networked
725  if (!$os)
726  {
727    foreach ($discovery_os['discovery_network'] as $cos => $defs)
728    {
729      foreach ($defs as $def)
730      {
731        if (match_discovery_os($sysObjectID, $sysDescr, $def, $device)) { $os = $cos; break 2; }
732      }
733    }
734  }
735
736  if (!$os)
737  {
738    $path = $config['install_dir'] . '/includes/discovery/os';
739    $sysObjectId = $sysObjectID; // old files use wrong variable name
740
741    // Recheck first
742    $recheck_file = FALSE;
743    if ($recheck && $old_os)
744    {
745      if (is_file($path . "/$old_os.inc.php"))
746      {
747        $recheck_file = $path . "/$old_os.inc.php";
748      }
749      else if (isset($config['os'][$old_os]['discovery_os']) &&
750               is_file($path . '/'.$config['os'][$old_os]['discovery_os'] . '.inc.php'))
751      {
752        $recheck_file = $path . '/'.$config['os'][$old_os]['discovery_os'] . '.inc.php';
753      }
754
755      if ($recheck_file)
756      {
757        print_debug("Including $recheck_file");
758
759        $sysObjectId = $sysObjectID; // old files use wrong variable name
760        include($recheck_file);
761
762        if ($os && $os == $old_os)
763        {
764          $table_rows[] = array('file', $recheck_file, '');
765          print_cli_table($table_rows, $table_headers, $table_desc . " ($old_os: ".$config['os'][$old_os]['text'].'):', $table_opts);
766          return $old_os;
767        }
768      }
769    }
770
771    // Check all other by include file
772    $dir_handle = @opendir($path) or die("Unable to open $path");
773    while ($file = readdir($dir_handle))
774    {
775      if (preg_match('/\.inc\.php$/', $file) && $file !== $recheck_file)
776      {
777        print_debug("Including $file");
778
779        include($path . '/' . $file);
780
781        if ($os)
782        {
783          $table_rows[] = array('file', $file, '');
784          break; // Stop while if os detected
785        }
786      }
787    }
788    closedir($dir_handle);
789  }
790
791  if ($os)
792  {
793    print_cli_table($table_rows, $table_headers, $table_desc . " ($os: ".$config['os'][$os]['text'].'):', $table_opts);
794    return $os;
795  } else {
796    return 'generic';
797  }
798}
799
800/**
801 * Compares sysObjectID with $needle. Return TRUE if match.
802 *
803 * @param string $sysObjectID Walked sysObjectID from device
804 * @param string $needle      Compare with this
805 * @return boolean            TRUE if match, otherwise FALSE
806 */
807function match_sysObjectID($sysObjectID, $needle)
808{
809  if (substr($needle, -1) === '.')
810  {
811    // Use wildcard compare if sysObjectID definition have '.' at end, ie:
812    //   .1.3.6.1.4.1.2011.1.
813    if (str_starts($sysObjectID, $needle)) { return TRUE; }
814  } else {
815    // Use exact match sysObjectID definition or wildcard compare with '.' at end, ie:
816    //   .1.3.6.1.4.1.2011.2.27
817    if ($sysObjectID === $needle || str_starts($sysObjectID, $needle.'.')) { return TRUE; }
818  }
819
820  return FALSE;
821}
822
823/**
824 * Compares complex sysObjectID/sysDescr definition with $needle. Return TRUE if match.
825 *
826 * @param string $sysObjectID Walked sysObjectID from device
827 * @param string $sysDescr    Walked sysDescr from device
828 * @param array  $needle      Compare with this definition array
829 * @param array  $device      Device array, optional if compare used not standard OIDs
830 * @return boolean            TRUE if match, otherwise FALSE
831 */
832function match_discovery_os($sysObjectID, $sysDescr, $needle, $device = array())
833{
834  global $table_rows, $cache_os;
835
836  $needle_oids  = array_keys($needle);
837  $needle_count = count($needle_oids);
838
839  // Match sysObjectID and sysDescr always first!
840  $needle_oids_order = array_merge(array('sysObjectID', 'sysDescr'), $needle_oids);
841  $needle_oids_order = array_unique($needle_oids_order);
842  $needle_oids_order = array_intersect($needle_oids_order, $needle_oids);
843
844  foreach ($needle_oids_order as $oid)
845  {
846    $match = FALSE;
847    switch ($oid)
848    {
849      case 'sysObjectID':
850        foreach ((array)$needle[$oid] as $def)
851        {
852          //var_dump($def);
853          //var_dump($sysObjectID);
854          //var_dump(match_sysObjectID($sysObjectID, $def));
855          if (match_sysObjectID($sysObjectID, $def))
856          {
857            $match_defs[$oid] = array($def, $sysObjectID);
858            $needle_count--;
859            $match = TRUE;
860            break;
861          }
862        }
863        break;
864
865      case 'sysDescr':
866        foreach ((array)$needle[$oid] as $def)
867        {
868          //print_vars($def);
869          //print_vars($sysDescr);
870          //print_vars(preg_match($def, $sysDescr));
871          if (preg_match($def, $sysDescr))
872          {
873            $match_defs[$oid] = array($def, $sysDescr);
874            $needle_count--;
875            $match = TRUE;
876            break;
877          }
878        }
879        break;
880
881      case 'sysName':
882        // other common SNMPv2-MIB fetch first
883        if (!isset($cache_os[$oid]))
884        {
885          $value    = snmp_fix_string(snmp_get_oid($device, $oid . '.0', 'SNMPv2-MIB'));
886          $value_ok = $GLOBALS['snmp_status'] || $GLOBALS['snmp_error_code'] === 1; // Allow empty response
887          $cache_os[$oid] = array('ok' => $value_ok, 'value' => $value);
888        } else {
889          // Use already cached data
890          $value    = $cache_os[$oid]['value'];
891          $value_ok = $cache_os[$oid]['ok'];
892        }
893        foreach ((array)$needle[$oid] as $def)
894        {
895          //print_vars($def);
896          //print_vars($value);
897          //print_vars(preg_match($def, $value));
898          if ($value_ok && preg_match($def, $value))
899          {
900            $match_defs[$oid] = array($def, $value);
901            $needle_count--;
902            $match = TRUE;
903            break;
904          }
905        }
906        break;
907
908      default:
909        // All other oids,
910        // fetch by first, than compare with pattern
911        if (!isset($cache_os[$oid]))
912        {
913          $value    = snmp_fix_string(snmp_get_oid($device, $oid));
914          $value_ok = $GLOBALS['snmp_status'] || $GLOBALS['snmp_error_code'] === 1; // Allow empty response
915          $cache_os[$oid] = array('ok' => $value_ok, 'value' => $value);
916        } else {
917          // Use already cached data
918          $value    = $cache_os[$oid]['value'];
919          $value_ok = $cache_os[$oid]['ok'];
920        }
921        foreach ((array)$needle[$oid] as $def)
922        {
923          // print_vars($def);
924          // print_vars($value);
925          // print_vars(preg_match($def, $value));
926          if ($value_ok && preg_match($def, $value))
927          {
928            $match_defs[$oid] = array($def, $value);
929            $needle_count--;
930            $match = TRUE;
931            break;
932          }
933        }
934        break;
935    }
936
937    // Stop all other checks, last oid not match with any..
938    if (!$match) { return FALSE; }
939  }
940
941  // Match only if all oids found and matched
942  $match = $needle_count === 0;
943
944  // Store detailed info
945  if ($match)
946  {
947    foreach ($match_defs as $oid => $def)
948    {
949      $table_rows[] = array($oid, $def[0], $def[1]);
950    }
951  }
952
953  return $match;
954}
955
956function cache_discovery_definitions()
957{
958  global $config, $cache;
959
960  // Cache/organize discovery definitions
961  if (!isset($cache['discovery_os']))
962  {
963    foreach (array_keys($config['os']) as $cos)
964    {
965      // Generate full array with sysObjectID from definitions
966      foreach ($config['os'][$cos]['sysObjectID'] as $oid)
967      {
968        $oid = trim($oid);
969        if ($oid[0] != '.') { $oid = '.' . $oid; } // Add first point if not already added
970
971        if (isset($cache['discovery_os']['sysObjectID'][$oid]) && strpos($cache['discovery_os']['sysObjectID'][$oid], 'test_') !== 0)
972        {
973          print_error("Duplicate sysObjectID '$oid' in definitions for OSes: ".$cache['discovery_os']['sysObjectID'][$oid]." and $cos!");
974          continue;
975        }
976        // sysObjectID -> os
977        $cache['discovery_os']['sysObjectID'][$oid] = $cos;
978        $cache['discovery_os']['sysObjectID_cos'][$oid][] = $cos; // Collect how many same sysObjectID known by definitions
979        //$sysObjectID_def[$oid] = $cos;
980      }
981
982      // Generate full array with sysDescr from definitions
983      if (isset($config['os'][$cos]['sysDescr']))
984      {
985        // os -> sysDescr (list)
986        $cache['discovery_os']['sysDescr'][$cos] = $config['os'][$cos]['sysDescr'];
987      }
988
989      // Complex match with combinations of sysDescr / sysObjectID and any other
990      foreach ($config['os'][$cos]['discovery'] as $discovery)
991      {
992        $oids = array_keys($discovery);
993        if (!in_array('sysObjectID', $oids))
994        {
995          // Check if definition have additional "networked" OIDs (without sysObjectID checks)
996          $def_name = 'discovery_network';
997        } else {
998          $def_name = 'discovery';
999        }
1000
1001        if (count($oids) === 1)
1002        {
1003          // single oids convert to old array format
1004          switch (array_shift($oids))
1005          {
1006            case 'sysObjectID':
1007              foreach ((array)$discovery['sysObjectID'] as $oid)
1008              {
1009                $oid = trim($oid);
1010                if ($oid[0] != '.') { $oid = '.' . $oid; } // Add first point if not already added
1011
1012                if (isset($cache['discovery_os']['sysObjectID'][$oid]) && strpos($cache['discovery_os']['sysObjectID'][$oid], 'test_') !== 0)
1013                {
1014                  print_error("Duplicate sysObjectID '$oid' in definitions for OSes: ".$cache['discovery_os']['sysObjectID'][$oid]." and $cos!");
1015                  continue;
1016                }
1017                // sysObjectID -> os
1018                $cache['discovery_os']['sysObjectID'][$oid] = $cos;
1019                $cache['discovery_os']['sysObjectID_cos'][$oid][] = $cos; // Collect how many same sysObjectID known by definitions
1020              }
1021              break;
1022            case 'sysDescr':
1023              // os -> sysDescr (list)
1024              if (isset($cache['discovery_os']['sysDescr'][$cos]))
1025              {
1026                $cache['discovery_os']['sysDescr'][$cos] = array_unique(array_merge((array)$cache['discovery_os']['sysDescr'][$cos], (array)$discovery['sysDescr']));
1027              } else {
1028                $cache['discovery_os']['sysDescr'][$cos] = (array)$discovery['sysDescr'];
1029              }
1030              break;
1031            case 'file':
1032              $cache['discovery_os']['file'][$cos] = $discovery['file'];
1033              break;
1034            default:
1035              // All other leave as is
1036              $cache['discovery_os'][$def_name][$cos][] = $discovery;
1037          }
1038        } else {
1039          if ($def_name == 'discovery') // This have sysObjectID
1040          {
1041            $new_oids = array();
1042            foreach ((array)$discovery['sysObjectID'] as $oid)
1043            {
1044              $oid = trim($oid);
1045              if ($oid[0] != '.') { $oid = '.' . $oid; } // Add first point if not already added
1046              $new_oids[] = $oid;
1047              $cache['discovery_os']['sysObjectID_cos'][$oid][] = $cos; // Collect how many same sysObjectID known by definitions
1048            }
1049            $discovery['sysObjectID'] = $new_oids; // Override sysObjectIDs with normalized
1050          }
1051          // Leave complex definitions as is
1052          $cache['discovery_os'][$def_name][$cos][] = $discovery;
1053        }
1054      }
1055    }
1056
1057    // NOTE: Currently too hard for detect if same sysObjectID used in multiple OSes, and how get best OS
1058    // Best os should match by max params
1059    // Remove all single sysObjectIDs count
1060    foreach ($cache['discovery_os']['sysObjectID_cos'] as $oid => $oses)
1061    {
1062      $oses = array_unique($oses);
1063      if (count($oses) < 2)
1064      {
1065        // Single sysObjectID, no additional matches needed
1066        unset($cache['discovery_os']['sysObjectID_cos'][$oid]);
1067      } else {
1068        /*
1069        if (isset($cache['discovery_os']['sysObjectID'][$oid]))
1070        {
1071          // Move simple check to complex
1072          $cos = $cache['discovery_os']['sysObjectID'][$oid];
1073          ///$cache['discovery_os']['discovery'][$cos][] = array('sysObjectID' => $oid);
1074          //unset($cache['discovery_os']['sysObjectID'][$oid]);
1075        }
1076        */
1077        $cache['discovery_os']['sysObjectID_cos'][$oid] = $oses;
1078      }
1079    }
1080
1081    // Resort sysObjectID array by oids with from high to low order!
1082    //krsort($cache['discovery_os']['sysObjectID']);
1083    uksort($cache['discovery_os']['sysObjectID'], 'compare_numeric_oids_reverse');
1084
1085    //print_vars($cache['discovery_os']['sysObjectID_cos']);
1086    //print_vars($cache['discovery_os']);
1087  }
1088}
1089
1090/**
1091 * Compare two numeric oids and return -1, 0, 1
1092 * ie: .1.2.1. vs 1.2.2
1093 */
1094function compare_numeric_oids($oid1, $oid2)
1095{
1096  $oid1_array = explode('.', ltrim($oid1, '.'));
1097  $oid2_array = explode('.', ltrim($oid2, '.'));
1098
1099  $count1 = count($oid1_array);
1100  $count2 = count($oid2_array);
1101
1102  for ($i = 0; $i <= min($count1, $count2) - 1; $i++)
1103  {
1104    $int1 = intval($oid1_array[$i]);
1105    $int2 = intval($oid2_array[$i]);
1106    if      ($int1 > $int2) { return 1; }
1107    else if ($int1 < $int2) { return -1; }
1108  }
1109  if      ($count1 > $count2) { return 1; }
1110  else if ($count1 < $count2) { return -1; }
1111
1112  return 0;
1113}
1114
1115/**
1116 * Compare two numeric oids and return -1, 0, 1
1117 * here reverse order
1118 * ie: .1.2.1. vs 1.2.2
1119 */
1120function compare_numeric_oids_reverse($oid1, $oid2)
1121{
1122  return compare_numeric_oids($oid2, $oid1);
1123}
1124
1125// Rename a device
1126// DOCME needs phpdoc block
1127// TESTME needs unit testing
1128function renamehost($id, $new, $source = 'console', $options = array())
1129{
1130  global $config;
1131
1132  $new = strtolower(trim($new));
1133
1134  // Test if new host exists in database
1135  //if (dbFetchCell('SELECT COUNT(`device_id`) FROM `devices` WHERE `hostname` = ?', array($new)) == 0)
1136  if (!dbExist('devices', '`hostname` = ?', array($new)))
1137  {
1138    $flags = OBS_DNS_ALL;
1139    $transport = strtolower(dbFetchCell("SELECT `snmp_transport` FROM `devices` WHERE `device_id` = ?", array($id)));
1140
1141    // Try detect if hostname is IP
1142    switch (get_ip_version($new))
1143    {
1144      case 6:
1145        $new     = Net_IPv6::compress($new, TRUE); // Always use compressed IPv6 name
1146      case 4:
1147        if ($config['require_hostname'])
1148        {
1149          print_error("Hostname should be a valid resolvable FQDN name. Or set config option \$config['require_hostname'] as FALSE.");
1150 	 	      return FALSE;
1151        }
1152        $ip      = $new;
1153        break;
1154      default:
1155        if ($transport == 'udp6' || $transport == 'tcp6') // Exclude IPv4 if used transport 'udp6' or 'tcp6'
1156        {
1157          $flags = $flags ^ OBS_DNS_A; // exclude A
1158        }
1159        // Test DNS lookup.
1160        $ip      = gethostbyname6($new, $flags);
1161    }
1162
1163    if ($ip)
1164    {
1165      $options['ping_skip'] = (isset($options['ping_skip']) && $options['ping_skip']) || get_entity_attrib('device', $id, 'ping_skip');
1166      if ($options['ping_skip'])
1167      {
1168        // Skip ping checks
1169        $flags = $flags | OBS_PING_SKIP;
1170      }
1171      // Test reachability
1172      if (isPingable($new, $flags))
1173      {
1174        // Test directory mess in /rrd/
1175        if (!file_exists($config['rrd_dir'].'/'.$new))
1176        {
1177          $host = dbFetchCell("SELECT `hostname` FROM `devices` WHERE `device_id` = ?", array($id));
1178          if (!file_exists($config['rrd_dir'].'/'.$host))
1179          {
1180            print_warning("Old RRD directory does not exist, rename skipped.");
1181          }
1182          else if (!rename($config['rrd_dir'].'/'.$host, $config['rrd_dir'].'/'.$new))
1183          {
1184            print_error("NOT renamed. Error while renaming RRD directory.");
1185            return FALSE;
1186          }
1187          $return = dbUpdate(array('hostname' => $new), 'devices', '`device_id` = ?', array($id));
1188          if ($options['ping_skip'])
1189          {
1190            set_entity_attrib('device', $id, 'ping_skip', 1);
1191          }
1192          log_event("Device hostname changed: $host -> $new", $id, 'device', $id, 5); // severity 5, for logging user/console info
1193          return TRUE;
1194        } else {
1195          // directory already exists
1196          print_error("NOT renamed. Directory rrd/$new already exists");
1197        }
1198      } else {
1199        // failed Reachability
1200        print_error("NOT renamed. Could not ping $new");
1201      }
1202    } else {
1203      // Failed DNS lookup
1204      print_error("NOT renamed. Could not resolve $new");
1205    }
1206  } else {
1207    // found in database
1208    print_error("NOT renamed. Already got host $new");
1209  }
1210  return FALSE;
1211}
1212
1213// Deletes device from database and RRD dir.
1214// DOCME needs phpdoc block
1215// TESTME needs unit testing
1216function delete_device($id, $delete_rrd = FALSE)
1217{
1218  global $config;
1219
1220  $ret = PHP_EOL;
1221  $device = device_by_id_cache($id);
1222  $host = $device['hostname'];
1223
1224  if (!is_array($device))
1225  {
1226    return FALSE;
1227  } else {
1228    $ports = dbFetchRows("SELECT * FROM `ports` WHERE `device_id` = ?", array($id));
1229    if (!empty($ports))
1230    {
1231      $ret .= ' * Deleted interfaces: ';
1232      $deleted_ports = [];
1233      foreach ($ports as $int_data)
1234      {
1235        $int_if = $int_data['ifDescr'];
1236        $int_id = $int_data['port_id'];
1237        delete_port($int_id, $delete_rrd);
1238        $deleted_ports[] = "id=$int_id ($int_if)";
1239      }
1240      $ret .= implode(', ', $deleted_ports).PHP_EOL;
1241    }
1242
1243    // Remove entities from common tables
1244    $deleted_entities = array();
1245    foreach (get_device_entities($id) as $entity_type => $entity_ids)
1246    {
1247      foreach ($config['entity_tables'] as $table)
1248      {
1249        $where = '`entity_type` = ?' . generate_query_values($entity_ids, 'entity_id');
1250        $table_status = dbDelete($table, $where, array($entity_type));
1251        if ($table_status) { $deleted_entities[$entity_type] = 1; }
1252      }
1253    }
1254    if (count($deleted_entities))
1255    {
1256      $ret .= ' * Deleted common entity entries linked to device: ';
1257      $ret .= implode(', ', array_keys($deleted_entities)) . PHP_EOL;
1258    }
1259
1260    $deleted_tables = array();
1261    $ret .= ' * Deleted device entries from tables: ';
1262    foreach ($config['device_tables'] as $table)
1263    {
1264      $where = '`device_id` = ?';
1265      $table_status = dbDelete($table, $where, array($id));
1266      if ($table_status) { $deleted_tables[] = $table; }
1267    }
1268    if (count($deleted_tables))
1269    {
1270      $ret .= implode(', ', $deleted_tables).PHP_EOL;
1271
1272      // Request for clear WUI cache
1273      set_cache_clear('wui');
1274    }
1275
1276    if ($delete_rrd)
1277    {
1278      $device_rrd = rtrim(get_rrd_path($device, ''), '/');
1279      if (is_file($device_rrd.'/status.rrd'))
1280      {
1281        external_exec("rm -rf ".escapeshellarg($device_rrd));
1282        $ret .= ' * Deleted device RRDs dir: ' . $device_rrd . PHP_EOL;
1283      }
1284
1285    }
1286
1287    log_event("Deleted device: $host", $id, 'device', $id, 5); // severity 5, for logging user/console info
1288    $ret .= " * Deleted device: $host";
1289  }
1290
1291  return $ret;
1292}
1293
1294function add_device_vars($vars)
1295{
1296
1297    global $config;
1298
1299    $hostname = strip_tags($vars['hostname']);
1300
1301    // Keep original snmp/rrd config, for revert at end
1302    $config_snmp = $config['snmp'];
1303    $config_rrd  = $config['rrd_override'];
1304
1305    // Default snmp port
1306    if ($vars['snmp_port'] && is_numeric($vars['snmp_port']))
1307    {
1308      $snmp_port = (int)$vars['snmp_port'];
1309    } else {
1310      $snmp_port = 161;
1311    }
1312
1313    // Default snmp version
1314    if ($vars['snmp_version'] !== "v2c" &&
1315        $vars['snmp_version'] !== "v3"  &&
1316        $vars['snmp_version'] !== "v1")
1317    {
1318      $vars['snmp_version'] = $config['snmp']['version'];
1319    }
1320
1321    switch ($vars['snmp_version'])
1322    {
1323      case 'v2c':
1324      case 'v1':
1325
1326        if (strlen($vars['snmp_community']))
1327        {
1328          // Hrm, I not sure why strip_tags
1329          $snmp_community = strip_tags($vars['snmp_community']);
1330          $config['snmp']['community'] = array($snmp_community);
1331        }
1332
1333        $snmp_version = $vars['snmp_version'];
1334
1335        print_message("Adding SNMP$snmp_version host $hostname port $snmp_port");
1336        break;
1337
1338      case 'v3':
1339
1340        if (strlen($vars['snmp_authlevel']))
1341        {
1342          $snmp_v3 = array (
1343            'authlevel'  => $vars['snmp_authlevel'],
1344            'authname'   => $vars['snmp_authname'],
1345            'authpass'   => $vars['snmp_authpass'],
1346            'authalgo'   => $vars['snmp_authalgo'],
1347            'cryptopass' => $vars['snmp_cryptopass'],
1348            'cryptoalgo' => $vars['snmp_cryptoalgo'],
1349          );
1350
1351          array_unshift($config['snmp']['v3'], $snmp_v3);
1352        }
1353
1354        $snmp_version = "v3";
1355
1356        print_message("Adding SNMPv3 host $hostname port $snmp_port");
1357        break;
1358
1359      default:
1360        print_error("Unsupported SNMP Version. There was a dropdown menu, how did you reach this error?"); // We have a hacker!
1361        return FALSE;
1362    }
1363
1364    if ($vars['ignorerrd'] == 'confirm' ||
1365        $vars['ignorerrd'] == '1' ||
1366        $vars['ignorerrd'] == 'on')
1367    {
1368      $config['rrd_override'] = TRUE;
1369    }
1370
1371    $snmp_options = array();
1372    if ($vars['ping_skip'] == '1' || $vars['ping_skip'] == 'on')
1373    {
1374      $snmp_options['ping_skip'] = TRUE;
1375    }
1376
1377    // Optional SNMP Context
1378    if (strlen(trim($vars['snmp_context'])))
1379    {
1380      $snmp_options['snmp_context'] = trim($vars['snmp_context']);
1381    }
1382
1383    $result = add_device($hostname, $snmp_version, $snmp_port, strip_tags($vars['snmp_transport']), $snmp_options);
1384
1385    // Revert original snmp/rrd config
1386    $config['snmp'] = $config_snmp;
1387    $config['rrd_override'] = $config_rrd;
1388
1389    return $result;
1390
1391}
1392
1393
1394
1395/**
1396 * Adds the new device to the database.
1397 *
1398 * Before adding the device, checks duplicates in the database and the availability of device over a network.
1399 *
1400 * @param string $hostname Device hostname
1401 * @param string|array $snmp_version SNMP version(s) (default: $config['snmp']['version'])
1402 * @param string $snmp_port SNMP port (default: 161)
1403 * @param string $snmp_transport SNMP transport (default: udp)
1404 * @param array $options Additional options can be passed ('ping_skip' - for skip ping test and add device attrib for skip pings later
1405 *                                                         'break' - for break recursion,
1406 *                                                         'test'  - for skip adding, only test device availability)
1407 *
1408 * @return mixed Returns $device_id number if added, 0 (zero) if device not accessible with current auth and FALSE if device complete not accessible by network. When testing, returns -1 if the device is available.
1409 */
1410// TESTME needs unit testing
1411function add_device($hostname, $snmp_version = array(), $snmp_port = 161, $snmp_transport = 'udp', $options = array(), $flags = OBS_DNS_ALL)
1412{
1413  global $config;
1414
1415  // If $options['break'] set as TRUE, break recursive function execute
1416  if (isset($options['break']) && $options['break']) { return FALSE; }
1417  $return = FALSE; // By default return FALSE
1418
1419  // Reset snmp timeout and retries options for speedup device adding
1420  unset($config['snmp']['timeout'], $config['snmp']['retries']);
1421
1422  $snmp_transport = strtolower($snmp_transport);
1423
1424  $hostname = strtolower(trim($hostname));
1425
1426  // Try detect if hostname is IP
1427  switch (get_ip_version($hostname))
1428  {
1429    case 6:
1430      $hostname = Net_IPv6::compress($hostname, TRUE); // Always use compressed IPv6 name
1431    case 4:
1432      if ($config['require_hostname'])
1433      {
1434        print_error("Hostname should be a valid resolvable FQDN name. Or set config option \$config['require_hostname'] as FALSE.");
1435        return $return;
1436      }
1437      $ip       = $hostname;
1438      break;
1439    default:
1440      if ($snmp_transport == 'udp6' || $snmp_transport == 'tcp6') // IPv6 used only if transport 'udp6' or 'tcp6'
1441      {
1442        $flags = $flags ^ OBS_DNS_A; // exclude A
1443      }
1444      // Test DNS lookup.
1445      $ip       = gethostbyname6($hostname, $flags);
1446  }
1447
1448  // Test if host exists in database
1449  //if (dbFetchCell("SELECT COUNT(*) FROM `devices` WHERE `hostname` = ?", array($hostname)) == '0')
1450  if (!dbExist('devices', '`hostname` = ?', array($hostname)))
1451  {
1452    if ($ip)
1453    {
1454      $ip_version = get_ip_version($ip);
1455
1456      // Test reachability
1457      $options['ping_skip'] = isset($options['ping_skip']) && $options['ping_skip'];
1458      if ($options['ping_skip'])
1459      {
1460        $flags = $flags | OBS_PING_SKIP;
1461      }
1462      if (isPingable($hostname, $flags))
1463      {
1464        // Test directory exists in /rrd/
1465        if (!$config['rrd_override'] && file_exists($config['rrd_dir'].'/'.$hostname))
1466        {
1467          print_error("Directory <observium>/rrd/$hostname already exists.");
1468          return FALSE;
1469        }
1470
1471        // Detect snmp transport
1472        if (stripos($snmp_transport, 'tcp') !== FALSE)
1473        {
1474          $snmp_transport = ($ip_version == 4 ? 'tcp' : 'tcp6');
1475        } else {
1476          $snmp_transport = ($ip_version == 4 ? 'udp' : 'udp6');
1477        }
1478        // Detect snmp port
1479        if (!is_numeric($snmp_port) || $snmp_port < 1 || $snmp_port > 65535)
1480        {
1481          $snmp_port = 161;
1482        } else {
1483          $snmp_port = (int)$snmp_port;
1484        }
1485        // Detect snmp version
1486        if (empty($snmp_version))
1487        {
1488          // Here set default snmp version order
1489          $i = 1;
1490          $snmp_version_order = array();
1491          foreach (array('v2c', 'v3', 'v1') as $tmp_version)
1492          {
1493            if ($config['snmp']['version'] == $tmp_version)
1494            {
1495              $snmp_version_order[0]  = $tmp_version;
1496            } else {
1497              $snmp_version_order[$i] = $tmp_version;
1498            }
1499            $i++;
1500          }
1501          ksort($snmp_version_order);
1502
1503          foreach ($snmp_version_order as $tmp_version)
1504          {
1505            $ret = add_device($hostname, $tmp_version, $snmp_port, $snmp_transport, $options);
1506            if ($ret === FALSE)
1507            {
1508              // Set $options['break'] for break recursive
1509              $options['break'] = TRUE;
1510            }
1511            else if (is_numeric($ret) && $ret != 0)
1512            {
1513              return $ret;
1514            }
1515          }
1516        }
1517        else if ($snmp_version === "v3")
1518        {
1519          // Try each set of parameters from config
1520          foreach ($config['snmp']['v3'] as $auth_iter => $snmp_extra)
1521          {
1522            // Append SNMP context if passed
1523            if (isset($options['snmp_context']) && strlen($options['snmp_context']))
1524            {
1525              $snmp_extra['snmp_context'] = $options['snmp_context'];
1526            }
1527
1528            $device = build_initial_device_array($hostname, NULL, $snmp_version, $snmp_port, $snmp_transport, $snmp_extra);
1529
1530            if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1531            {
1532              // Hide snmp auth
1533              print_message("Trying v3 parameters *** / ### [$auth_iter] ... ");
1534            } else {
1535              print_message("Trying v3 parameters " . $device['snmp_authname'] . "/" .  $device['snmp_authlevel'] . " ... ");
1536            }
1537
1538            if (isSNMPable($device))
1539            {
1540              if (!check_device_duplicated($device))
1541              {
1542                if (isset($options['test']) && $options['test'])
1543                {
1544                  print_message('%WDevice "'.$hostname.'" has successfully been tested and available by '.strtoupper($snmp_transport).' transport with SNMP '.$snmp_version.' credentials.%n', 'color');
1545                  $device_id = -1;
1546                } else {
1547                  $device_id = createHost($hostname, NULL, $snmp_version, $snmp_port, $snmp_transport, $snmp_extra);
1548                  if ($options['ping_skip'])
1549                  {
1550                    set_entity_attrib('device', $device_id, 'ping_skip', 1);
1551                    // Force pingable check
1552                    if (isPingable($hostname, $flags ^ OBS_PING_SKIP))
1553                    {
1554                      print_warning("You passed the option the skip device ICMP echo pingable checks, but device responds to ICMP echo. Please check device preferences.");
1555                    }
1556                  }
1557                }
1558                return $device_id;
1559              } else {
1560                // When detected duplicate device, this mean it already SNMPable and not need check next auth!
1561                return FALSE;
1562              }
1563            } else {
1564              print_warning("No reply on credentials " . $device['snmp_authname'] . "/" .  $device['snmp_authlevel'] . " using $snmp_version.");
1565            }
1566          }
1567        }
1568        else if ($snmp_version === "v2c" || $snmp_version === "v1")
1569        {
1570          // Try each community from config
1571          foreach ($config['snmp']['community'] as $auth_iter => $snmp_community)
1572          {
1573            // Append SNMP context if passed
1574            $snmp_extra = array();
1575            if (isset($options['snmp_context']) && strlen($options['snmp_context']))
1576            {
1577              $snmp_extra['snmp_context'] = $options['snmp_context'];
1578            }
1579            $device = build_initial_device_array($hostname, $snmp_community, $snmp_version, $snmp_port, $snmp_transport, $snmp_extra);
1580
1581            if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1582            {
1583              // Hide snmp auth
1584              print_message("Trying $snmp_version community *** [$auth_iter] ...");
1585            } else {
1586              print_message("Trying $snmp_version community $snmp_community ...");
1587            }
1588
1589            if (isSNMPable($device))
1590            {
1591              if (!check_device_duplicated($device))
1592              {
1593                if (isset($options['test']) && $options['test'])
1594                {
1595                  print_message('%WDevice "'.$hostname.'" has successfully been tested and available by '.strtoupper($snmp_transport).' transport with SNMP '.$snmp_version.' credentials.%n', 'color');
1596                  $device_id = -1;
1597                } else {
1598                  $device_id = createHost($hostname, $snmp_community, $snmp_version, $snmp_port, $snmp_transport, $snmp_extra);
1599                  if ($options['ping_skip'])
1600                  {
1601                    set_entity_attrib('device', $device_id, 'ping_skip', 1);
1602                    // Force pingable check
1603                    if (isPingable($hostname, $flags ^ OBS_PING_SKIP))
1604                    {
1605                      print_warning("You passed the option the skip device ICMP echo pingable checks, but device responds to ICMP echo. Please check device preferences.");
1606                    }
1607                  }
1608                }
1609                return $device_id;
1610              } else {
1611                // When detected duplicate device, this mean it already SNMPable and not need check next auth!
1612                return FALSE;
1613              }
1614            } else {
1615              if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1616              {
1617                print_warning("No reply on given community *** using $snmp_version.");
1618              } else {
1619                print_warning("No reply on community $snmp_community using $snmp_version.");
1620              }
1621              $return = 0; // Return zero for continue trying next auth
1622            }
1623          }
1624        } else {
1625          print_error("Unsupported SNMP Version \"$snmp_version\".");
1626          $return = 0; // Return zero for continue trying next auth
1627        }
1628
1629        if (!$device_id)
1630        {
1631          // Failed SNMP
1632          print_error("Could not reach $hostname with given SNMP parameters using $snmp_version.");
1633          $return = 0; // Return zero for continue trying next auth
1634        }
1635      } else {
1636        // failed Reachability
1637        print_error("Could not ping $hostname.");
1638      }
1639    } else {
1640      // Failed DNS lookup
1641      print_error("Could not resolve $hostname.");
1642    }
1643  } else {
1644    // found in database
1645    print_error("Already got device $hostname.");
1646  }
1647
1648  return $return;
1649}
1650
1651/**
1652 * Check duplicated devices in DB by sysName, snmpEngineID and entPhysicalSerialNum (if possible)
1653 *
1654 * If found duplicate devices return TRUE, in other cases return FALSE
1655 *
1656 * @param array $device Device array which should be checked for duplicates
1657 * @return bool TRUE if duplicates found
1658 */
1659// TESTME needs unit testing
1660function check_device_duplicated($device)
1661{
1662  // Hostname should be uniq
1663  if ($device['hostname'] &&
1664      //dbFetchCell("SELECT COUNT(*) FROM `devices` WHERE `hostname` = ?", array($device['hostname'])) != '0')
1665      dbExist('devices', '`hostname` = ?', array($device['hostname'])))
1666  {
1667    // Retun TRUE if have device with same hostname in DB
1668    print_error("Already got device with hostname (".$device['hostname'].").");
1669    return TRUE;
1670  }
1671
1672  $snmpEngineID = snmp_cache_snmpEngineID($device);
1673  $sysName      = snmp_get_oid($device, 'sysName.0', 'SNMPv2-MIB');
1674  if (empty($sysName) || strpos($sysName, '.') === FALSE)
1675  {
1676    $sysName = FALSE;
1677  } else{
1678    // sysName stored in db as lowercase, always compare as lowercase too!
1679    $sysName = strtolower($sysName);
1680  }
1681
1682  if (!empty($snmpEngineID))
1683  {
1684    $test_devices = dbFetchRows('SELECT * FROM `devices` WHERE `disabled` = 0 AND `snmpEngineID` = ?', array($snmpEngineID));
1685    foreach ($test_devices as $test)
1686    {
1687      $compare = strtolower($test['sysName']) === $sysName;
1688      if ($compare)
1689      {
1690        // Last check (if possible) serial, for cluster devices sysName and snmpEngineID same
1691        $test_entPhysical = dbFetchRow('SELECT * FROM `entPhysical` WHERE `device_id` = ? AND `entPhysicalSerialNum` != ? ORDER BY `entPhysicalClass` LIMIT 1', array($test['device_id'], ''));
1692        if (isset($test_entPhysical['entPhysicalSerialNum']))
1693        {
1694          $serial = snmp_get_oid($device, 'entPhysicalSerialNum.'.$test_entPhysical['entPhysicalIndex'], 'ENTITY-MIB');
1695          $compare = strtolower($serial) == strtolower($test_entPhysical['entPhysicalSerialNum']);
1696          if ($compare)
1697          {
1698            // This devices really same, with same sysName, snmpEngineID and entPhysicalSerialNum
1699            print_error("Already got device with SNMP-read sysName ($sysName), 'snmpEngineID' = $snmpEngineID and 'entPhysicalSerialNum' = $serial (".$test['hostname'].").");
1700            return TRUE;
1701          }
1702        } else {
1703          // Return TRUE if have same snmpEngineID && sysName in DB
1704          print_error("Already got device with SNMP-read sysName ($sysName) and 'snmpEngineID' = $snmpEngineID (".$test['hostname'].").");
1705          return TRUE;
1706        }
1707      }
1708    }
1709  } else {
1710    // If snmpEngineID empty, check only by sysName
1711    $test_devices = dbFetchRows('SELECT * FROM `devices` WHERE `disabled` = 0 AND `sysName` = ?', array($sysName));
1712    if ($sysName !== FALSE && is_array($test_devices) && count($test_devices) > 0)
1713    {
1714      $has_serial = FALSE;
1715      foreach ($test_devices as $test)
1716      {
1717        // Last check (if possible) serial, for cluster devices sysName and snmpEngineID same
1718        $test_entPhysical = dbFetchRow('SELECT * FROM `entPhysical` WHERE `device_id` = ? AND `entPhysicalSerialNum` != ? ORDER BY `entPhysicalClass` LIMIT 1', array($test['device_id'], ''));
1719        if (isset($test_entPhysical['entPhysicalSerialNum']))
1720        {
1721          $serial = snmp_get_oid($device, "entPhysicalSerialNum.".$test_entPhysical['entPhysicalIndex'], "ENTITY-MIB");
1722          $compare = strtolower($serial) == strtolower($test_entPhysical['entPhysicalSerialNum']);
1723          if ($compare)
1724          {
1725            // This devices really same, with same sysName, snmpEngineID and entPhysicalSerialNum
1726            print_error("Already got device with SNMP-read sysName ($sysName) and 'entPhysicalSerialNum' = $serial (".$test['hostname'].").");
1727            return TRUE;
1728          }
1729          $has_serial = TRUE;
1730        }
1731      }
1732      if (!$has_entPhysical)
1733      {
1734        // Return TRUE if have same sysName in DB
1735        print_error("Already got device with SNMP-read sysName ($sysName).");
1736        return TRUE;
1737      }
1738    }
1739  }
1740
1741  // In all other cases return FALSE
1742  return FALSE;
1743}
1744
1745// DOCME needs phpdoc block
1746// TESTME needs unit testing
1747// MOVEME to includes/common.inc.php
1748function scanUDP($host, $port, $timeout)
1749{
1750  $handle = fsockopen($host, $port, $errno, $errstr, 2);
1751  socket_set_timeout($handle, $timeout);
1752  $write = fwrite($handle,"\x00");
1753  if (!$write) { next; }
1754  $startTime = time();
1755  $header = fread($handle, 1);
1756  $endTime = time();
1757  $timeDiff = $endTime - $startTime;
1758
1759  if ($timeDiff >= $timeout)
1760  {
1761    fclose($handle); return 1;
1762  } else { fclose($handle); return 0; }
1763}
1764
1765// DOCME needs phpdoc block
1766// TESTME needs unit testing
1767function build_initial_device_array($hostname, $snmp_community, $snmp_version, $snmp_port = 161, $snmp_transport = 'udp', $snmp_extra = array())
1768{
1769  $device = array();
1770  $device['hostname']       = $hostname;
1771  $device['snmp_port']      = $snmp_port;
1772  $device['snmp_transport'] = $snmp_transport;
1773  $device['snmp_version']   = $snmp_version;
1774
1775  if ($snmp_version === "v2c" || $snmp_version === "v1")
1776  {
1777    $device['snmp_community'] = $snmp_community;
1778  }
1779  else if ($snmp_version == "v3")
1780  {
1781    $device['snmp_authlevel']  = $snmp_extra['authlevel'];
1782    $device['snmp_authname']   = $snmp_extra['authname'];
1783    $device['snmp_authpass']   = $snmp_extra['authpass'];
1784    $device['snmp_authalgo']   = $snmp_extra['authalgo'];
1785    $device['snmp_cryptopass'] = $snmp_extra['cryptopass'];
1786    $device['snmp_cryptoalgo'] = $snmp_extra['cryptoalgo'];
1787  }
1788  // Append SNMP context if passed
1789  if (isset($snmp_extra['snmp_context']) && strlen($snmp_extra['snmp_context']))
1790  {
1791    $device['snmp_context'] = $snmp_extra['snmp_context'];
1792  }
1793
1794  print_debug_vars($device);
1795
1796  return $device;
1797}
1798
1799// DOCME needs phpdoc block
1800// TESTME needs unit testing
1801// MOVEME to includes/common.inc.php
1802function netmask2cidr($netmask)
1803{
1804  $addr = Net_IPv4::parseAddress("1.2.3.4/$netmask");
1805  return $addr->bitmask;
1806}
1807
1808// DOCME needs phpdoc block
1809// TESTME needs unit testing
1810// MOVEME to includes/common.inc.php
1811function cidr2netmask($cidr)
1812{
1813  return (long2ip(ip2long("255.255.255.255") << (32-$cidr)));
1814}
1815
1816// Detect SNMP auth params without adding device by hostname or IP
1817// if SNMP auth detected return array with auth params or FALSE if not detected
1818// DOCME needs phpdoc block
1819// TESTME needs unit testing
1820function detect_device_snmpauth($hostname, $snmp_port = 161, $snmp_transport = 'udp', $detect_ip_version = FALSE)
1821{
1822  global $config;
1823
1824  // Additional checks for IP version
1825  if ($detect_ip_version)
1826  {
1827    $ip_version = get_ip_version($hostname);
1828    if (!$ip_version)
1829    {
1830      $ip = gethostbyname6($hostname);
1831      $ip_version = get_ip_version($ip);
1832    }
1833    // Detect snmp transport
1834    if (stripos($snmp_transport, 'tcp') !== FALSE)
1835    {
1836      $snmp_transport = ($ip_version == 4 ? 'tcp' : 'tcp6');
1837    } else {
1838      $snmp_transport = ($ip_version == 4 ? 'udp' : 'udp6');
1839    }
1840  }
1841  // Detect snmp port
1842  if (!is_numeric($snmp_port) || $snmp_port < 1 || $snmp_port > 65535)
1843  {
1844    $snmp_port = 161;
1845  } else {
1846    $snmp_port = (int)$snmp_port;
1847  }
1848
1849  // Here set default snmp version order
1850  $i = 1;
1851  $snmp_version_order = array();
1852  foreach (array('v2c', 'v3', 'v1') as $tmp_version)
1853  {
1854    if ($config['snmp']['version'] == $tmp_version)
1855    {
1856      $snmp_version_order[0]  = $tmp_version;
1857    } else {
1858      $snmp_version_order[$i] = $tmp_version;
1859    }
1860    $i++;
1861  }
1862  ksort($snmp_version_order);
1863
1864  foreach ($snmp_version_order as $snmp_version)
1865  {
1866    if ($snmp_version === 'v3')
1867    {
1868      // Try each set of parameters from config
1869      foreach ($config['snmp']['v3'] as $auth_iter => $snmp_v3)
1870      {
1871        $device = build_initial_device_array($hostname, NULL, $snmp_version, $snmp_port, $snmp_transport, $snmp_v3);
1872
1873        if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1874        {
1875          // Hide snmp auth
1876          print_message("Trying v3 parameters *** / ### [$auth_iter] ... ");
1877        } else {
1878          print_message("Trying v3 parameters " . $device['snmp_authname'] . "/" .  $device['snmp_authlevel'] . " ... ");
1879        }
1880
1881        if (isSNMPable($device))
1882        {
1883          return $device;
1884        } else {
1885          if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1886          {
1887            print_warning("No reply on credentials *** / ### using $snmp_version.");
1888          } else {
1889            print_warning("No reply on credentials " . $device['snmp_authname'] . "/" .  $device['snmp_authlevel'] . " using $snmp_version.");
1890          }
1891        }
1892      }
1893    } else { // if ($snmp_version === "v2c" || $snmp_version === "v1")
1894      // Try each community from config
1895      foreach ($config['snmp']['community'] as $auth_iter => $snmp_community)
1896      {
1897        $device = build_initial_device_array($hostname, $snmp_community, $snmp_version, $snmp_port, $snmp_transport);
1898
1899        if ($config['snmp']['hide_auth'] && OBS_DEBUG < 2)
1900        {
1901          // Hide snmp auth
1902          print_message("Trying $snmp_version community *** [$auth_iter] ...");
1903        } else {
1904          print_message("Trying $snmp_version community $snmp_community ...");
1905        }
1906
1907        if (isSNMPable($device))
1908        {
1909          return $device;
1910        } else {
1911          print_warning("No reply on community $snmp_community using $snmp_version.");
1912        }
1913      }
1914    }
1915  }
1916
1917  return FALSE;
1918}
1919
1920/**
1921 * Checks device availability by snmp query common oids
1922 *
1923 * @param array $device Device array
1924 * @return float SNMP query runtime in milliseconds
1925 */
1926// TESTME needs unit testing
1927function isSNMPable($device)
1928{
1929  if (isset($device['os'][0]) && isset($GLOBALS['config']['os'][$device['os']]['snmpable']) && $device['os'] != 'generic')
1930  {
1931    // Known device os, and defined custom snmpable OIDs
1932    $pos   = snmp_get_multi_oid($device, $GLOBALS['config']['os'][$device['os']]['snmpable'], array(), 'SNMPv2-MIB', NULL, OBS_SNMP_ALL_NUMERIC);
1933    $count = count($pos);
1934  } else {
1935    // Normal checks by sysObjectID and sysUpTime
1936    $pos   = snmp_get_multi_oid($device, 'sysObjectID.0 sysUpTime.0', array(), 'SNMPv2-MIB');
1937    $count = count($pos[0]);
1938
1939    if ($count === 0 && (empty($device['os']) || !isset($GLOBALS['config']['os'][$device['os']])))
1940    {
1941      // New device (or os changed) try to all snmpable OIDs
1942      foreach (array_chunk($GLOBALS['config']['os']['generic']['snmpable'], 3) as $snmpable)
1943      {
1944        $pos   = snmp_get_multi_oid($device, $snmpable, array(), 'SNMPv2-MIB', NULL, OBS_SNMP_ALL_NUMERIC);
1945        if ($count = count($pos)) { break; } // stop foreach on first oids set
1946      }
1947    }
1948  }
1949
1950  if ($GLOBALS['snmp_status'] && $count > 0)
1951  {
1952    // SNMP response time in milliseconds.
1953    $time_snmp = $GLOBALS['exec_status']['runtime'] * 1000;
1954    $time_snmp = number_format($time_snmp, 2, '.', '');
1955    return $time_snmp;
1956  }
1957
1958  return 0;
1959}
1960
1961/**
1962 * Checks device availability by icmp echo response
1963 * If flag OBS_PING_SKIP passed, pings skipped and returns 0.001 (1ms)
1964 *
1965 * @param string $hostname Device hostname or IP address
1966 * @param int Flags. Supported OBS_DNS_A, OBS_DNS_AAAA and OBS_PING_SKIP
1967 * @return float Average response time for used retries count (default retries is 3)
1968 */
1969function isPingable($hostname, $flags = OBS_DNS_ALL)
1970{
1971  global $config;
1972
1973  $ping_debug = isset($config['ping']['debug']) && $config['ping']['debug'];
1974  $try_a      = is_flag_set(OBS_DNS_A, $flags);
1975
1976  set_status_var('ping_dns', 'ok'); // Set initially dns status as ok
1977  if (is_flag_set(OBS_PING_SKIP, $flags))
1978  {
1979    return 0.001; // Ping is skipped, just return 1ms
1980  }
1981
1982  // Timeout, default is 500ms (as in fping)
1983  $timeout = (isset($config['ping']['timeout']) ? (int)$config['ping']['timeout'] : 500);
1984  if ($timeout < 50) { $timeout = 50; }
1985  else if ($timeout > 2000) { $timeout = 2000; }
1986
1987  // Retries, default is 3
1988  $retries = (isset($config['ping']['retries']) ? (int)$config['ping']['retries'] : 3);
1989  if      ($retries < 1)  { $retries = 3; }
1990  else if ($retries > 10) { $retries = 10; }
1991
1992  if ($ip_version = get_ip_version($hostname))
1993  {
1994    // Ping by IP
1995    if ($ip_version === 6)
1996    {
1997      $cmd = $config['fping6'] . " -t $timeout -c 1 -q $hostname 2>&1";
1998    } else {
1999      if (!$try_a)
2000      {
2001        if ($ping_debug) { logfile('debug.log', __FUNCTION__ . "() | DEVICE: $hostname | Passed IPv4 address but device use IPv6 transport"); }
2002        print_debug('Into function ' . __FUNCTION__ . '() passed IPv4 address ('.$hostname.'but device use IPv6 transport');
2003        set_status_var('ping_dns', 'incorrect'); // Incorrect
2004        return 0;
2005      }
2006      // Forced check for actual IPv4 address
2007      $cmd = $config['fping'] . " -t $timeout -c 1 -q $hostname 2>&1";
2008    }
2009  } else {
2010    // First try IPv4
2011    $ip = ($try_a ? gethostbyname($hostname) : FALSE); // Do not check IPv4 if transport IPv6
2012    if ($ip && $ip != $hostname)
2013    {
2014      $cmd = $config['fping'] . " -t $timeout -c 1 -q $ip 2>&1";
2015    } else {
2016      $ip = gethostbyname6($hostname, OBS_DNS_AAAA);
2017      // Second try IPv6
2018      if ($ip)
2019      {
2020        $cmd = $config['fping6'] . " -t $timeout -c 1 -q $ip 2>&1";
2021      } else {
2022        // No DNS records
2023        if ($ping_debug) { logfile('debug.log', __FUNCTION__ . "() | DEVICE: $hostname | NO DNS record found"); }
2024        set_status_var('ping_dns', 'alert');
2025        return 0;
2026      }
2027    }
2028  }
2029
2030  // Sleep interval between retries, max 1 sec, min 333ms (1s/3),
2031  // next retry will increase interval by 1.5 Backoff factor (see fping -B option)
2032  // We not use fping native retries, because fping waiting for all responses, but we wait only first OK
2033  $sleep = floor(1000000 / $retries);
2034  if ($sleep < 333000) { $sleep = 333000; }
2035
2036  $ping = 0; // Init false
2037  for ($i=1; $i <= $retries; $i++)
2038  {
2039    $output = external_exec($cmd);
2040    if ($GLOBALS['exec_status']['exitcode'] === 0)
2041    {
2042      // normal $output = '8.8.8.8 : xmt/rcv/%loss = 1/1/0%, min/avg/max = 1.21/1.21/1.21'
2043      $tmp = explode('/', $output);
2044      $ping = $tmp[7]; // Avg
2045      if (!$ping) { $ping = 0.001; } // Protection from zero (exclude false status)
2046    }
2047    if ($ping) { break; }
2048
2049    if ($ping_debug)
2050    {
2051      $ping_times = format_unixtime($GLOBALS['exec_status']['endtime'] - $GLOBALS['exec_status']['runtime'], 'H:i:s.v') . ', ' . round($GLOBALS['exec_status']['runtime'], 3) . 's';
2052      logfile('debug.log', __FUNCTION__ . "() | DEVICE: $hostname | $ping_times | FPING OUT ($i): " . $output);
2053      //log_event(__FUNCTION__ . "() | DEVICE: $hostname | FPING OUT ($i): " . $output, $device_id, 'device', $device_id, 7); // severity 7, debug
2054      // WARNING, this is very long operation, increase polling time up to 10s
2055      if ($i == $retries && is_file($config['mtr']))
2056      {
2057        $mtr = $config['mtr'] . " -r -n -c 3 $ip";
2058        logfile('debug.log', __FUNCTION__ . "() | DEVICE: $hostname | MTR OUT:\n" . external_exec($mtr));
2059      }
2060    }
2061
2062    if ($i < $retries)
2063    {
2064      // Sleep and increase next sleep interval
2065      usleep($sleep);
2066      $sleep *= 1.5; // Backoff factor 1.5
2067    }
2068  }
2069
2070  return $ping;
2071}
2072
2073// DOCME needs phpdoc block
2074// TESTME needs unit testing
2075// MOVEME to includes/common.inc.php
2076function is_odd($number)
2077{
2078  return $number & 1; // 0 = even, 1 = odd
2079}
2080
2081// TESTME needs unit testing
2082/**
2083 * Add device into database
2084 *
2085 * @param string $hostname Device hostname
2086 * @param string $snmp_community SNMP v1/v2c community
2087 * @param string $snmp_version   SNMP version (v1, v2c, v3)
2088 * @param int    $snmp_port      SNMP port (default: 161)
2089 * @param string $snmp_transport SNMP transport (udp, udp6, tcp, tcp6)
2090 * @param array  $snmp_extra     SNMP v3 auth params and/or SNMP context
2091 *
2092 * @return bool|string
2093 */
2094function createHost($hostname, $snmp_community = NULL, $snmp_version, $snmp_port = 161, $snmp_transport = 'udp', $snmp_extra = array())
2095{
2096  $hostname = trim(strtolower($hostname));
2097
2098  $device = array('hostname'       => $hostname,
2099                  'sysName'        => $hostname,
2100                  'status'         => '1',
2101                  'snmp_community' => $snmp_community,
2102                  'snmp_port'      => $snmp_port,
2103                  'snmp_transport' => $snmp_transport,
2104                  'snmp_version'   => $snmp_version
2105            );
2106
2107  // Add snmp v3 auth params & snmp context
2108  foreach (array('authlevel', 'authname', 'authpass', 'authalgo', 'cryptopass', 'cryptoalgo', 'context') as $v3_key)
2109  {
2110    if (isset($snmp_extra['snmp_'.$v3_key]))
2111    {
2112      // Or $snmp_v3['snmp_authlevel']
2113      $device['snmp_'.$v3_key] = $snmp_extra['snmp_'.$v3_key];
2114    }
2115    else if (isset($snmp_extra[$v3_key]))
2116    {
2117      // Or $snmp_v3['authlevel']
2118      $device['snmp_'.$v3_key] = $snmp_extra[$v3_key];
2119    }
2120  }
2121
2122  $device['os']           = get_device_os($device);
2123  $device['snmpEngineID'] = snmp_cache_snmpEngineID($device);
2124  $device['sysName']      = snmp_get_oid($device, 'sysName.0',     'SNMPv2-MIB');
2125  $device['location']     = snmp_get_oid($device, 'sysLocation.0', 'SNMPv2-MIB');
2126  $device['sysContact']   = snmp_get_oid($device, 'sysContact.0',  'SNMPv2-MIB');
2127
2128  if(isset($GLOBALS['config']['poller_name']))
2129  {
2130    $poller_id = dbFetchCell("SELECT `poller_id` FROM `pollers` WHERE `poller_name` = ?", array($GLOBALS['config']['poller_name']));
2131
2132    if(is_numeric($poller_id))
2133    {
2134      $device['poller_id'] = $poller_id;
2135    }
2136  }
2137
2138  if ($device['os'])
2139  {
2140    $device_id = dbInsert($device, 'devices');
2141    if ($device_id)
2142    {
2143      log_event("Device added: $hostname", $device_id, 'device', $device_id, 5); // severity 5, for logging user/console info
2144      if (is_cli())
2145      {
2146        print_success("Now discovering ".$device['hostname']." (id = ".$device_id.")");
2147        $device['device_id'] = $device_id;
2148        // Discover things we need when linking this to other hosts.
2149        discover_device($device, $options = array('m' => 'ports'));
2150        discover_device($device, $options = array('m' => 'ipv4-addresses'));
2151        discover_device($device, $options = array('m' => 'ipv6-addresses'));
2152        log_event("snmpEngineID -> ".$device['snmpEngineID'], $device, 'device', $device['device_id']);
2153        // Reset `last_discovered` for full rediscover device by cron
2154        dbUpdate(array('last_discovered' => 'NULL'), 'devices', '`device_id` = ?', array($device_id));
2155        array_push($GLOBALS['devices'], $device_id); // FIXME, seems as $devices var not used anymore
2156      }
2157
2158      // Request for clear WUI cache
2159      set_cache_clear('wui');
2160
2161      return($device_id);
2162    } else {
2163      return FALSE;
2164    }
2165  } else {
2166    return FALSE;
2167  }
2168}
2169
2170// Allows overwriting elements of an array of OIDs with replacement values from a private MIB.
2171// Currently used by ports to replace OIDs with private ports tables.
2172
2173function merge_private_mib(&$device, $entity_type, $mib, &$entity_stats, $limit_oids = NULL)
2174{
2175
2176  global $config;
2177
2178  foreach ($config['mibs'][$mib][$entity_type] as $table => $def)
2179  {
2180
2181    if (isset($def['oids']) && is_array($def['oids']))
2182    {
2183
2184      print_cli_data_field($mib);
2185      echo $table . ' ';
2186
2187      $walked = array();
2188
2189      // Walk to $entity_tmp, link to $entity_stats if we're not rewriting
2190      if (isset($def['map']))
2191      {
2192        $entity_tmp = array();
2193        if (isset($def['map']['oid']))
2194        {
2195          echo $def['map']['oid'] . ' ';
2196          $entity_tmp = snmpwalk_cache_oid($device, $def['map']['oid'], $entity_tmp, $mib);
2197
2198          // Skip next Oids walk if no response
2199          if (!snmp_status()) { continue; }
2200
2201          $walked[] = $def['map']['oid'];
2202        }
2203        foreach ((array)$def['map']['oid_extra'] as $oid)
2204        {
2205          echo $oid . ' ';
2206          $entity_tmp = snmpwalk_cache_oid($device, $oid, $entity_tmp, $mib);
2207          $walked[] = $oid;
2208        }
2209      } else {
2210        $entity_tmp = &$entity_stats;
2211      }
2212
2213      // Populated $entity_tmp
2214      foreach ($def['oids'] as $oid => $entry)
2215      {
2216        // Skip if there's an OID list and we're not on it.
2217        if (isset($limit_oids) && !in_array($oid, $limit_oids)) { continue; }
2218
2219        // If this OID is being used twice, don't walk it again.
2220        if (!isset($entry['oid']) || in_array($entry['oid'], $walked)) { continue; }
2221
2222        echo $entry['oid'] . ' ';
2223        $flags = isset($entry['snmp_flags']) ? $entry['snmp_flags'] : OBS_SNMP_ALL;
2224        $entity_tmp = snmpwalk_cache_oid($device, $entry['oid'], $entity_tmp, $mib, NULL, $flags);
2225
2226        // Skip next Oids walk if no response
2227        if (!snmp_status() && $oid == array_key_first($def['oids'])) { continue 2; }
2228
2229        $walked[] = $entry['oid'];
2230      }
2231
2232      // Rewrite indexes using map from $entity_tmp to $entity_stats
2233      if (isset($def['map']))
2234      {
2235        $entity_new = array();
2236        $map_tmp = array();
2237        // Generate mapping list
2238        if (isset($def['map']['index']))
2239        {
2240          // Index by tags
2241          foreach ($entity_tmp as $index => $entry)
2242          {
2243            $entry['index'] = $index;
2244            $map_index = array_tag_replace($entry, $def['map']['index']);
2245            $map_tmp[$index] = $map_index;
2246          }
2247        } else {
2248          // Mapping by Oid
2249          foreach ($entity_stats as $index => $entry)
2250          {
2251            if (isset($entry[$def['map']['map']]))
2252            {
2253              $map_tmp[$entry[$def['map']['map']]] = $index;
2254            }
2255          }
2256        }
2257
2258        foreach ($entity_tmp as $index => $entry)
2259        {
2260          if (isset($map_tmp[$index]))
2261          {
2262            foreach ($entry as $oid => $value)
2263            {
2264              $entity_new[$map_tmp[$index]][$oid] = $value;
2265            }
2266          }
2267        }
2268      } else {
2269        $entity_new = $entity_tmp;
2270      }
2271
2272      echo '['; // start change list
2273
2274      foreach ($entity_new as $index => $port)
2275      {
2276        foreach ($def['oids'] as $oid => $entry)
2277        {
2278          // Skip if there's an OID list and we're not on it.
2279          if (isset($limit_oids) && !in_array($oid, $limit_oids))
2280          {
2281            continue;
2282          }
2283
2284          $mib_oid = $entry['oid'];
2285          if (isset($entry['oid']) && isset($port[$mib_oid]))
2286          {
2287            $entity_stats[$index][$oid] = $port[$mib_oid];
2288
2289            if (isset($entry['transformations']))
2290            {
2291              // Translate to standard IF-MIB values
2292              $entity_stats[$index][$oid] = string_transform($entity_stats[$index][$oid], $entry['transformations']);
2293              echo 'T';
2294            }
2295
2296            echo '.';
2297          }
2298          else if (isset($entry['value']))
2299          {
2300            // Set fixed value
2301            $entity_stats[$index][$oid] = $entry['value'];
2302          }
2303          else
2304          {
2305            echo '!';
2306          }
2307        }
2308      }
2309      echo ']'; // end change list
2310      echo PHP_EOL; // end CLI DATA FIELD
2311    }
2312  }
2313  return TRUE;
2314}
2315
2316// BOOLEAN safe function to check if hostname resolves as IPv4 or IPv6 address
2317// DOCME needs phpdoc block
2318// TESTME needs unit testing
2319// MOVEME to includes/common.inc.php
2320function isDomainResolves($hostname, $flags = OBS_DNS_ALL)
2321{
2322  return (TRUE && gethostbyname6($hostname, $flags));
2323}
2324
2325/**
2326 * Returns IP version for string or FALSE if string not an IP
2327 *
2328 * Examples:
2329 *  get_ip_version('127.0.0.1')   === 4
2330 *  get_ip_version('::1')         === 6
2331 *  get_ip_version('my_hostname') === FALSE
2332 *
2333 * @param sting $address IP address string
2334 * @return mixed IP version or FALSE if passed incorrect address
2335 */
2336function get_ip_version($address)
2337{
2338  $address_version = FALSE;
2339  if      (strpos($address, '/') !== FALSE)
2340  {
2341    // Dump condition,
2342    // IPs with CIDR not correct for us here
2343  }
2344  //else if (strpos($address, '.') !== FALSE && Net_IPv4::validateIP($address))
2345  else if (preg_match('%^'.OBS_PATTERN_IPV4.'$%', $address))
2346  {
2347    $address_version = 4;
2348  }
2349  //else if (strpos($address, ':') !== FALSE && Net_IPv6::checkIPv6($address))
2350  else if (preg_match('%^'.OBS_PATTERN_IPV6.'$%i', $address))
2351  {
2352    $address_version = 6;
2353  }
2354  return $address_version;
2355}
2356
2357/**
2358 * Check if a given IPv4 address (and prefix length) is valid.
2359 *
2360 * @param string $ipv4_address    IPv4 Address
2361 * @param string $ipv4_prefixlen  IPv4 Prefix length (optional, either 24 or 255.255.255.0)
2362 *
2363 * @return bool Returns TRUE if address is valid, FALSE if not valid.
2364 */
2365// TESTME needs unit testing
2366function is_ipv4_valid($ipv4_address, $ipv4_prefixlen = NULL)
2367{
2368
2369  if (str_contains($ipv4_address, '/'))
2370  {
2371    list($ipv4_address, $ipv4_prefixlen) = explode('/', $ipv4_address);
2372  }
2373  $ip_full = $ipv4_address . '/' . $ipv4_prefixlen;
2374
2375  // False if invalid IPv4 syntax
2376  if (strlen($ipv4_prefixlen) &&
2377      !preg_match('%^'.OBS_PATTERN_IPV4_NET.'$%', $ip_full))
2378  {
2379    // Address with prefix
2380    return FALSE;
2381  }
2382  else if (!preg_match('%^'.OBS_PATTERN_IPV4.'$%i', $ipv4_address))
2383  {
2384    // Address withot prefix
2385    return FALSE;
2386  }
2387
2388  $ipv4_type = get_ip_type($ip_full);
2389
2390  // False if link-local, unspecified or any used defined
2391  if (in_array($ipv4_type, $GLOBALS['config']['ip-address']['ignore_type']))
2392  {
2393    return FALSE;
2394  }
2395
2396  return TRUE;
2397}
2398
2399/**
2400 * Check if a given IPv6 address (and prefix length) is valid.
2401 * Link-local addresses are considered invalid.
2402 *
2403 * @param string $ipv6_address    IPv6 Address
2404 * @param string $ipv6_prefixlen  IPv6 Prefix length (optional)
2405 *
2406 * @return bool Returns TRUE if address is valid, FALSE if not valid.
2407 */
2408// TESTME needs unit testing
2409function is_ipv6_valid($ipv6_address, $ipv6_prefixlen = NULL)
2410{
2411  if (str_contains($ipv6_address, '/'))
2412  {
2413    list($ipv6_address, $ipv6_prefixlen) = explode('/', $ipv6_address);
2414  }
2415  $ip_full = $ipv6_address . '/' . $ipv6_prefixlen;
2416
2417  // False if invalid IPv6 syntax
2418  if (strlen($ipv6_prefixlen) &&
2419      !preg_match('%^'.OBS_PATTERN_IPV6_NET.'$%i', $ip_full))
2420  {
2421    // Address with prefix
2422    return FALSE;
2423  }
2424  else if (!preg_match('%^'.OBS_PATTERN_IPV6.'$%i', $ipv6_address))
2425  {
2426    // Address withot prefix
2427    return FALSE;
2428  }
2429
2430  $ipv6_type = get_ip_type($ip_full);
2431
2432  // False if link-local, unspecified or any used defined
2433  if (in_array($ipv6_type, $GLOBALS['config']['ip-address']['ignore_type']))
2434  {
2435    return FALSE;
2436  }
2437
2438  return TRUE;
2439}
2440
2441/**
2442 * Detect IP type.
2443 * Based on https://www.ripe.net/manage-ips-and-asns/ipv6/ipv6-address-types/ipv6-address-types
2444 *
2445 *  Known types:
2446 *   - unspecified    : ::/128, 0.0.0.0
2447 *   - loopback       : ::1/128, 127.0.0.1
2448 *   - ipv4mapped     : only for IPv6 ::ffff/96 (::ffff:192.0.2.47)
2449 *   - private        : fc00::/7 (fdf8:f53b:82e4::53),
2450 *                      10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
2451 *   - link-local     : fe80::/10 (fe80::200:5aee:feaa:20a2),
2452 *                      169.254.0.0/16
2453 *   - teredo         : only for IPv6 2001:0000::/32 (2001:0000:4136:e378:8000:63bf:3fff:fdd2)
2454 *   - benchmark      : 2001:0002::/48 (2001:0002:6c::430),
2455 *                      198.18.0.0/15
2456 *   - orchid         : only for IPv6 2001:0010::/28 (2001:10:240:ab::a)
2457 *   - 6to4           : 2002::/16 (2002:cb0a:3cdd:1::1),
2458 *                      192.88.99.0/24
2459 *   - documentation  : 2001:db8::/32 (2001:db8:8:4::2),
2460 *                      192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24
2461 *   - global-unicast : only for IPv6 2000::/3
2462 *   - multicast      : ff00::/8 (ff01:0:0:0:0:0:0:2), 224.0.0.0/4
2463 *   - unicast        : all other
2464 *
2465 * @param string $address IPv4 or IPv6 address string
2466 * @return string IP type
2467 */
2468function get_ip_type($address)
2469{
2470  global $config;
2471
2472  list($ip, $bits) = explode('/', trim($address)); // Remove subnet/mask if exist
2473
2474  $ip_version = get_ip_version($ip);
2475  switch ($ip_version)
2476  {
2477    case 4:
2478
2479      // Detect IPv4 broadcast
2480      if (strlen($bits))
2481      {
2482        $ip_parse = Net_IPv4::parseAddress($address);
2483        if ($ip == $ip_parse->broadcast && $ip_parse->bitmask < 31) // Do not set /31 and /32 as broadcast!
2484        {
2485          $ip_type = 'broadcast';
2486          break;
2487        }
2488      }
2489
2490      // no break here!
2491    case 6:
2492
2493      $ip_type = ($ip_version == 4) ? 'unicast': 'reserved'; // Default for any valid address
2494      foreach ($config['ip_types'] as $type => $entry)
2495      {
2496        if (isset($entry['networks']) && match_network($ip, $entry['networks'], TRUE))
2497        {
2498          // Stop loop if IP founded in networks
2499          $ip_type = $type;
2500          break;
2501        }
2502
2503      }
2504      break;
2505
2506    default:
2507      // Not valid IP address
2508      return FALSE;
2509  }
2510
2511  return $ip_type;
2512}
2513
2514/**
2515 * Parse string for valid IP/Network queries.
2516 *
2517 * Valid queries example:
2518 *    - 10.0.0.0/8  - exactly IPv4 network, matches to 10.255.0.2, 10.0.0.0, 10.255.255.255
2519 *    - 10.12.0.3/8 - same as previous, NOTE network detected by prefix: 10.0.0.0/8
2520 *    - 10.12.0.3   - single IPv4 address
2521 *    - *.12.0.3, 10.*.3   - * matching by any string in address
2522 *    - ?.12.0.3, 10.?.?.3 - ? matching by single char
2523 *    - 10.12              - match by part of address, matches to 10.12.2.3, 10.122.3.3, 1.2.110.120
2524 *    - 1762::b03:1:ae00/119        - exactly IPv6 network, matches to 1762::b03:1:ae00, 1762::B03:1:AF18, 1762::b03:1:afff
2525 *    - 1762:0:0:0:0:B03:1:AF18/119 - same as previous, NOTE network detected by prefix: 1762::b03:1:ae00/119
2526 *    - 1762:0:0:0:0:B03:1:AF18     - single IPv6 address
2527 *    - *::B03:1:AF18, 1762::*:AF18 - * matching by any string in address
2528 *    - ?::B03:1:AF18, 1762::?:AF18 - ? matching by single char
2529 *    - 1762:b03                    -  match by part of address, matches to 1:AF18:1762::B03, 1762::b03:1:ae00
2530 *
2531 * Return array contain this params:
2532 *    'query_type' - which query type required.
2533 *                   Possible: single       (single IP address),
2534 *                             network      (addresses inside network),
2535 *                             like, %like% (part of address with masks *, ?)
2536 *    'ip_version' - numeric IP version (4, 6)
2537 *    'ip_type'    - ipv4 or ipv6
2538 *    'address'    - string with passed IP address without prefixes or address part
2539 *    'prefix'     - detected network prefix
2540 *    'network'    - detected network with prefix
2541 *    'network_start' - first address of network
2542 *    'network_end'   - last address of network (broadcast)
2543 *
2544 * @param string $network Network/IP query string
2545 * @return array Array with parsed network params
2546 */
2547function parse_network($network)
2548{
2549  $network = trim($network);
2550
2551  $array = array(
2552    'query_type' => 'network', // Default query type by valid network with prefix
2553  );
2554
2555  if (preg_match('%^'.OBS_PATTERN_IPV4_NET.'$%', $network, $matches))
2556  {
2557    // Match by valid IPv4 network
2558    $array['ip_version'] = 4;
2559    $array['ip_type']    = 'ipv4';
2560    $array['address']    = $matches['ipv4']; // Same as IP
2561    //$array['prefix']     = $matches['ipv4_prefix'];
2562
2563    // Convert Cisco Inverse netmask to normal mask
2564    if (isset($matches['ipv4_inverse_mask']))
2565    {
2566      $matches['ipv4_mask'] = inet_pton($matches['ipv4_inverse_mask']);
2567      $matches['ipv4_mask'] = inet_ntop(~$matches['ipv4_mask']); // Binary inverse and back to IP string
2568      $matches['ipv4_network'] = $matches['ipv4'] . '/' . $matches['ipv4_mask'];
2569    }
2570
2571    if ($matches['ipv4_prefix'] == '32' || $matches['ipv4_mask'] == '255.255.255.255')
2572    {
2573      $array['prefix']        = '32';
2574      $array['network_start'] = $array['address'];
2575      $array['network_end']   = $array['address'];
2576      $array['network']       = $matches['ipv4_network']; // Network with prefix
2577      $array['query_type']    = 'single'; // Single IP query
2578    } else {
2579      $address = Net_IPv4::parseAddress($matches['ipv4_network']);
2580      //print_vars($address);
2581      $array['prefix']        = $address->bitmask . '';
2582      $array['network_start'] = $address->network;
2583      $array['network_end']   = $address->broadcast;
2584      $array['network']       = $array['network_start'] . '/' . $array['prefix']; // Network with prefix
2585    }
2586  }
2587  else if (preg_match('%^'.OBS_PATTERN_IPV6_NET.'$%i', $network, $matches))
2588  {
2589    // Match by valid IPv6 network
2590    $array['ip_version'] = 6;
2591    $array['ip_type']    = 'ipv6';
2592    $array['address']    = $matches['ipv6']; // Same as IP
2593    $array['prefix']     = $matches['ipv6_prefix'];
2594    if ($array['prefix'] == 128)
2595    {
2596      $array['network_start'] = $array['address'];
2597      $array['network_end']   = $array['address'];
2598      $array['network']       = $matches['ipv6_network']; // Network with prefix
2599      $array['query_type'] = 'single'; // Single IP query
2600    } else {
2601      $address = Net_IPv6::parseAddress($array['address'], $array['prefix']);
2602      //print_vars($address);
2603      $array['network_start'] = $address['start'];
2604      $array['network_end']   = $address['end'];
2605      $array['network']       = $array['network_start'] . '/' . $array['prefix']; // Network with prefix
2606    }
2607  }
2608  else if ($ip_version = get_ip_version($network))
2609  {
2610    // Single IPv4/IPv6
2611    $array['ip_version'] = $ip_version;
2612    $array['address']    = $network;
2613    $array['prefix']     = $matches['ipv6_prefix'];
2614    if ($ip_version == 4)
2615    {
2616      $array['ip_type']  = 'ipv4';
2617      $array['prefix']   = '32';
2618    } else {
2619      $array['ip_type']  = 'ipv6';
2620      $array['prefix']   = '128';
2621    }
2622    $array['network_start'] = $array['address'];
2623    $array['network_end']   = $array['address'];
2624    $array['network']       = $network . '/' . $array['prefix']; // Add prefix
2625    $array['query_type']    = 'single'; // Single IP query
2626  }
2627  else if (preg_match('/^[\d\.\?\*]+$/', $network))
2628  {
2629    // Match IPv4 by mask
2630    $array['ip_version'] = 4;
2631    $array['ip_type']    = 'ipv4';
2632    $array['address']    = $network;
2633    if (str_contains($network, array('?', '*')))
2634    {
2635      // If network contains * or !
2636      $array['query_type'] = 'like';
2637    } else {
2638      // All other cases
2639      $array['query_type'] = '%like%';
2640    }
2641  }
2642  else if (preg_match('/^[abcdef\d\:\?\*]+$/i', $network))
2643  {
2644    // Match IPv6 by mask
2645    $array['ip_version'] = 6;
2646    $array['ip_type']    = 'ipv6';
2647    $array['address']    = $network;
2648    if (str_contains($network, array('?', '*')))
2649    {
2650      // If network contains * or !
2651      $array['query_type'] = 'like';
2652    } else {
2653      // All other cases
2654      $array['query_type'] = '%like%';
2655    }
2656  } else {
2657    // Not valid network string passed
2658    return FALSE;
2659  }
2660
2661  // Add binary addresses for single and network queries
2662  switch ($array['query_type'])
2663  {
2664    case 'single':
2665      $array['address_binary']       = inet_pton($array['address']);
2666      break;
2667    case 'network':
2668      $array['network_start_binary'] = inet_pton($array['network_start']);
2669      $array['network_end_binary']   = inet_pton($array['network_end']);
2670      break;
2671  }
2672
2673  return $array;
2674}
2675
2676/**
2677 * Determines whether or not the supplied IP address is within the supplied network (IPv4 or IPv6).
2678 *
2679 * @param string $ip     IP Address
2680 * @param string $nets   IPv4/v6 networks
2681 * @param bool   $first  FIXME
2682 *
2683 * @return bool Returns TRUE if address is found in supplied network, FALSE if it is not.
2684 */
2685// TESTME needs unit testing
2686function match_network($ip, $nets, $first = FALSE)
2687{
2688  $return = FALSE;
2689  $ip_version = get_ip_version($ip);
2690  if ($ip_version)
2691  {
2692    if (!is_array($nets)) { $nets = array($nets); }
2693    foreach ($nets as $net)
2694    {
2695      $ip_in_net = FALSE;
2696
2697      $revert    = (preg_match('/^\!/', $net) ? TRUE : FALSE); // NOT match network
2698      if ($revert)
2699      {
2700        $net     = preg_replace('/^\!/', '', $net);
2701      }
2702
2703      if ($ip_version == 4)
2704      {
2705        if (strpos($net, '.') === FALSE) { continue; }      // NOT IPv4 net, skip
2706        if (strpos($net, '/') === FALSE) { $net .= '/32'; } // NET without mask as single IP
2707        $ip_in_net = Net_IPv4::ipInNetwork($ip, $net);
2708      } else {
2709        //print_vars($ip); echo(' '); print_vars($net); echo(PHP_EOL);
2710        if (strpos($net, ':') === FALSE) { continue; }       // NOT IPv6 net, skip
2711        if (strpos($net, '/') === FALSE) { $net .= '/128'; } // NET without mask as single IP
2712        $ip_in_net = Net_IPv6::isInNetmask($ip, $net);
2713      }
2714
2715      if ($revert && $ip_in_net) { return FALSE; } // Return FALSE if IP found in network where should NOT match
2716      if ($first  && $ip_in_net) { return TRUE; }  // Return TRUE if IP found in first match
2717      $return = $return || $ip_in_net;
2718    }
2719  }
2720
2721  return $return;
2722}
2723
2724/**
2725 * Convert SNMP hex string to binary map, mostly used for VLANs discovery
2726 *
2727 * Examples:
2728 *   "00 40" => "0000000001000000"
2729 *
2730 * @param string $hex HEX encoded string
2731 * @return string Binary string
2732 */
2733function hex2binmap($hex)
2734{
2735  $hex = str_replace(array(' ', "\n"), '', $hex);
2736
2737  $binary = '';
2738  $length = strlen($hex);
2739  for ($i = 0; $i < $length; $i++)
2740  {
2741    $char = $hex[$i];
2742    $binary .= zeropad(base_convert($char, 16, 2), 4);
2743  }
2744
2745  return $binary;
2746}
2747
2748/**
2749 * Convert HEX encoded IP value to pretty IP string
2750 *
2751 * Examples:
2752 *  IPv4 "C1 9C 5A 26" => "193.156.90.38"
2753 *  IPv4 "J}4:"        => "74.125.52.58"
2754 *  IPv6 "20 01 07 F8 00 12 00 01 00 00 00 00 00 05 02 72" => "2001:07f8:0012:0001:0000:0000:0005:0272"
2755 *  IPv6 "20:01:07:F8:00:12:00:01:00:00:00:00:00:05:02:72" => "2001:07f8:0012:0001:0000:0000:0005:0272"
2756 *
2757 * @param string $ip_hex HEX encoded IP address
2758 *
2759 * @return string IP address or original input string if not contains IP address
2760 */
2761function hex2ip($ip_hex)
2762{
2763  $ip  = trim($ip_hex, "\"\t\n\r\0\x0B");
2764
2765  // IPv6z, ie: 2a:02:a0:10:80:03:00:00:00:00:00:00:00:00:00:01%503316482
2766  if (str_contains($ip, '%'))
2767  {
2768    list($ip) = explode('%', $ip);
2769  }
2770
2771  $len = strlen($ip);
2772  if ($len === 5 && $ip[0] === ' ')
2773  {
2774    $ip  = substr($ip, 1);
2775    $len = 4;
2776  }
2777  if ($len === 4)
2778  {
2779    // IPv4 hex string converted to SNMP string
2780    $ip  = str2hex($ip);
2781    $len = strlen($ip);
2782  }
2783
2784  $ip  = str_replace(' ', '', $ip);
2785
2786  if ($len > 8)
2787  {
2788    // For IPv6
2789    $ip = str_replace(':', '', $ip);
2790    $len = strlen($ip);
2791  }
2792
2793  if (!ctype_xdigit($ip))
2794  {
2795    return $ip_hex;
2796  }
2797
2798  switch ($len)
2799  {
2800    case 8:
2801      // IPv4
2802      $ip_array = array();
2803      foreach (str_split($ip, 2) as $entry)
2804      {
2805        $ip_array[] = hexdec($entry);
2806      }
2807      $separator = '.';
2808      break;
2809
2810    case 16:
2811      // Cisco incorrect IPv4 (54 2E 68 02 FF FF FF FF)
2812      $ip_array = array();
2813      foreach (str_split($ip, 2) as $i => $entry)
2814      {
2815        if ($i == 4) { break; }
2816        $ip_array[] = hexdec($entry);
2817      }
2818      $separator = '.';
2819      break;
2820
2821    case 32:
2822      // IPv6
2823      $ip_array = str_split(strtolower($ip), 4);
2824      $separator = ':';
2825      break;
2826
2827    default:
2828      // Try convert hex string to string
2829      $ip = snmp_hexstring($ip_hex);
2830      if (get_ip_version($ip))
2831      {
2832        return $ip;
2833      }
2834      return $ip_hex;
2835  }
2836  $ip = implode($separator, $ip_array);
2837
2838  return $ip;
2839}
2840
2841/**
2842 * Convert IP string to HEX encoded value.
2843 *
2844 * Examples:
2845 *  IPv4 "193.156.90.38" => "C1 9C 5A 26"
2846 *  IPv6 "2001:07f8:0012:0001:0000:0000:0005:0272" => "20 01 07 f8 00 12 00 01 00 00 00 00 00 05 02 72"
2847 *  IPv6 "2001:7f8:12:1::5:0272" => "20 01 07 f8 00 12 00 01 00 00 00 00 00 05 02 72"
2848 *
2849 * @param string $ip IP address string
2850 * @param string $separator Separator for HEX parts
2851 *
2852 * @return string HEX encoded address
2853 */
2854function ip2hex($ip, $separator = ' ')
2855{
2856  $ip_hex     = trim($ip, " \"\t\n\r\0\x0B");
2857  $ip_version = get_ip_version($ip_hex);
2858
2859  if ($ip_version === 4)
2860  {
2861    // IPv4
2862    $ip_array = array();
2863    foreach (explode('.', $ip_hex) as $entry)
2864    {
2865      $ip_array[] = zeropad(dechex($entry));
2866    }
2867  }
2868  else if ($ip_version === 6)
2869  {
2870    // IPv6
2871    $ip_hex   = str_replace(':', '', Net_IPv6::uncompress($ip_hex, TRUE));
2872    $ip_array = str_split($ip_hex, 2);
2873  } else {
2874    return $ip;
2875  }
2876  $ip_hex = implode($separator, $ip_array);
2877
2878  return $ip_hex;
2879}
2880
2881// DOCME needs phpdoc block
2882// TESTME needs unit testing
2883function snmp2ipv6($ipv6_snmp)
2884{
2885  $ipv6 = explode('.',$ipv6_snmp);
2886
2887  // Workaround stupid Microsoft bug in Windows 2008 -- this is fixed length!
2888  // < fenestro> "because whoever implemented this mib for Microsoft was ignorant of RFC 2578 section 7.7 (2)"
2889  if (count($ipv6) == 17 && $ipv6[0] == 16)
2890  {
2891    array_shift($ipv6);
2892  }
2893
2894  for ($i = 0;$i <= 15;$i++) { $ipv6[$i] = zeropad(dechex($ipv6[$i])); }
2895  for ($i = 0;$i <= 15;$i+=2) { $ipv6_2[] = $ipv6[$i] . $ipv6[$i+1]; }
2896
2897  return implode(':',$ipv6_2);
2898}
2899
2900// DOCME needs phpdoc block
2901// TESTME needs unit testing
2902function ipv62snmp($ipv6)
2903{
2904  $ipv6_ex = explode(':',Net_IPv6::uncompress($ipv6));
2905  for ($i = 0;$i < 8;$i++) { $ipv6_ex[$i] = zeropad($ipv6_ex[$i],4); }
2906  $ipv6_ip = implode('',$ipv6_ex);
2907  for ($i = 0;$i < 32;$i+=2) $ipv6_split[] = hexdec(substr($ipv6_ip,$i,2));
2908
2909  return implode('.',$ipv6_split);
2910}
2911
2912// DOCME needs phpdoc block
2913// TESTME needs unit testing
2914// MOVEME to includes/common.inc.php
2915function get_astext($asn)
2916{
2917  global $config, $cache;
2918
2919  // Fetch pre-set AS text from config first
2920  if (isset($config['astext'][$asn]))
2921  {
2922    return $config['astext'][$asn];
2923  } else {
2924    // Not preconfigured, check cache before doing a new DNS request
2925    if (!isset($cache['astext'][$asn]))
2926    {
2927      $result = dns_get_record("AS$asn.asn.cymru.com", DNS_TXT);
2928      if (OBS_DEBUG > 1)
2929      {
2930        print_vars($result);
2931      }
2932      $txt = explode('|', $result[0]['txt']);
2933      $cache['astext'][$asn] = trim(str_replace('"', '', $txt[4]));
2934    }
2935
2936    return $cache['astext'][$asn];
2937  }
2938}
2939
2940/**
2941 * Use this function to write to the eventlog table
2942 *
2943 * @param string        $text      Message text
2944 * @param integer|array $device    Device array or device id
2945 * @param string        $type      Entity type (ie port, device, global)
2946 * @param integer       $reference Reference ID to current entity type
2947 * @param integer       $severity  Event severity (0 - 8)
2948 * @return integer                 Event DB id
2949 */
2950// TESTME needs unit testing
2951function log_event($text, $device = NULL, $type = NULL, $reference = NULL, $severity = 6)
2952{
2953  if (is_null($device) && is_null($type))
2954  {
2955    // Without device and type - is global events
2956    $type = 'global';
2957  } else {
2958    $type = strtolower($type);
2959  }
2960
2961  // Global events not have device_id
2962  if ($type != 'global')
2963  {
2964    if (!is_array($device)) { $device = device_by_id_cache($device); }
2965    if ($device['ignore'] && $type != 'device') { return FALSE; } // Do not log events if device ignored
2966  }
2967
2968  if ($type == 'port')
2969  {
2970    if (is_array($reference))
2971    {
2972      $port      = $reference;
2973      $reference = $port['port_id'];
2974    } else {
2975      $port = get_port_by_id_cache($reference);
2976    }
2977    if ($port['ignore']) { return FALSE; } // Do not log events if interface ignored
2978  }
2979
2980  $severity = priority_string_to_numeric($severity); // Convert named severities to numeric
2981  if (($type == 'device' && $severity == 5) || isset($_SESSION['username'])) // Severity "Notification" additional log info about username or cli
2982  {
2983    $severity = ($severity == 6 ? 5 : $severity); // If severity default, change to notification
2984    if (isset($_SESSION['username']))
2985    {
2986      $text .= ' (by user: '.$_SESSION['username'].')';
2987    }
2988    else if (is_cli())
2989    {
2990      if (is_cron())
2991      {
2992        $text .= ' (by cron)';
2993      } else {
2994        $text .= ' (by console, user '  . $_SERVER['USER'] . ')';
2995      }
2996    }
2997  }
2998
2999  $insert = array('device_id' => ($device['device_id'] ? $device['device_id'] : 0), // Currently db schema not allow NULL value for device_id
3000                  'entity_id' => (is_numeric($reference) ? $reference : array('NULL')),
3001                  'entity_type' => ($type ? $type : array('NULL')),
3002                  'timestamp' => array("NOW()"),
3003                  'severity' => $severity,
3004                  'message' => $text);
3005
3006  $id = dbInsert($insert, 'eventlog');
3007
3008  return $id;
3009}
3010
3011// Parse string with emails. Return array with email (as key) and name (as value)
3012// DOCME needs phpdoc block
3013// MOVEME to includes/common.inc.php
3014function parse_email($emails)
3015{
3016  $result = array();
3017
3018  if (is_string($emails))
3019  {
3020    $emails = preg_split('/[,;]\s{0,}/', $emails);
3021    foreach ($emails as $email)
3022    {
3023      $email = trim($email);
3024      if (preg_match('/^\s*' . OBS_PATTERN_EMAIL_LONG . '\s*$/iu', $email, $matches))
3025      {
3026        $email = trim($matches['email']);
3027        $name  = trim($matches['name'], " \t\n'\"");
3028        $result[$email] = (!empty($name) ? $name : NULL);
3029      }
3030      else if (strpos($email, "@") && !preg_match('/\s/', $email))
3031      {
3032        $result[$email] = NULL;
3033      } else {
3034        return FALSE;
3035      }
3036    }
3037  } else {
3038    // Return FALSE if input not string
3039    return FALSE;
3040  }
3041  return $result;
3042}
3043
3044/**
3045 * Converting string to hex
3046 *
3047 * By Greg Winiarski of ditio.net
3048 * http://ditio.net/2008/11/04/php-string-to-hex-and-hex-to-string-functions/
3049 * We claim no copyright over this function and assume that it is free to use.
3050 *
3051 * @param string $string
3052 *
3053 * @return string
3054 */
3055// MOVEME to includes/common.inc.php
3056function str2hex($string)
3057{
3058  $hex='';
3059  for ($i=0; $i < strlen($string); $i++)
3060  {
3061    $hex .= dechex(ord($string[$i]));
3062  }
3063  return $hex;
3064}
3065
3066/**
3067 * Converting hex to string
3068 *
3069 * By Greg Winiarski of ditio.net
3070 * http://ditio.net/2008/11/04/php-string-to-hex-and-hex-to-string-functions/
3071 * We claim no copyright over this function and assume that it is free to use.
3072 *
3073 * @param string $hex HEX string
3074 * @param string $eol EOL char, default is \n
3075 *
3076 * @return string
3077 */
3078// TESTME needs unit testing
3079// MOVEME to includes/common.inc.php
3080function hex2str($hex, $eol = "\n")
3081{
3082  $string='';
3083
3084  $hex = str_replace(' ', '', $hex);
3085  for ($i = 0; $i < strlen($hex) - 1; $i += 2)
3086  {
3087    $hex_chr = $hex[$i].$hex[$i+1];
3088    if ($hex_chr == '00')
3089    {
3090      // 00 is EOL
3091      $string .= $eol;
3092    } else {
3093      $string .= chr(hexdec($hex_chr));
3094    }
3095  }
3096
3097  return $string;
3098}
3099
3100/**
3101 * Converting hex/dec coded ascii char to UTF-8 char
3102 *
3103 * Used together with snmp_fix_string()
3104 *
3105 * @param string $hex
3106 *
3107 * @return string
3108 */
3109function convert_ord_char($ord)
3110{
3111  if (is_array($ord))
3112  {
3113    $ord = array_shift($ord);
3114  }
3115  if (preg_match('/^(?:<|x)([0-9a-f]+)>?$/i', $ord, $match))
3116  {
3117    $ord = hexdec($match[1]);
3118  }
3119  else if (is_numeric($ord))
3120  {
3121    $ord = intval($ord);
3122  }
3123  else if (preg_match('/^[\p{L}]+$/u', $ord))
3124  {
3125    // Unicode chars
3126    return $ord;
3127  } else {
3128    // Non-printable chars
3129    $ord = ord($ord);
3130  }
3131
3132  $no_bytes = 0;
3133  $byte = array();
3134
3135  if ($ord < 128)
3136  {
3137    return chr($ord);
3138  }
3139  else if ($ord < 2048)
3140  {
3141    $no_bytes = 2;
3142  }
3143  else if ($ord < 65536)
3144  {
3145    $no_bytes = 3;
3146  }
3147  else if ($ord < 1114112)
3148  {
3149    $no_bytes = 4;
3150  } else {
3151    return;
3152  }
3153  switch($no_bytes)
3154  {
3155    case 2:
3156      $prefix = array(31, 192);
3157      break;
3158    case 3:
3159      $prefix = array(15, 224);
3160      break;
3161    case 4:
3162      $prefix = array(7, 240);
3163      break;
3164  }
3165
3166  for ($i = 0; $i < $no_bytes; $i++)
3167  {
3168    $byte[$no_bytes - $i - 1] = (($ord & (63 * pow(2, 6 * $i))) / pow(2, 6 * $i)) & 63 | 128;
3169  }
3170
3171  $byte[0] = ($byte[0] & $prefix[0]) | $prefix[1];
3172
3173  $ret = '';
3174  for ($i = 0; $i < $no_bytes; $i++)
3175  {
3176    $ret .= chr($byte[$i]);
3177  }
3178
3179  return $ret;
3180}
3181
3182// Check if the supplied string is a hex string
3183// FIXME This is test for SNMP hex string, for just hex string use ctype_xdigit()
3184// DOCME needs phpdoc block
3185// TESTME needs unit testing
3186// MOVEME to includes/snmp.inc.php
3187function isHexString($str)
3188{
3189  return (preg_match("/^[a-f0-9][a-f0-9](\ +[a-f0-9][a-f0-9])*$/is", trim($str)) ? TRUE : FALSE);
3190}
3191
3192// Include all .inc.php files in $dir
3193// DOCME needs phpdoc block
3194// TESTME needs unit testing
3195// MOVEME to includes/common.inc.php
3196function include_dir($dir, $regex = "")
3197{
3198  global $device, $config, $valid;
3199
3200  if ($regex == "")
3201  {
3202    $regex = "/\.inc\.php$/";
3203  }
3204
3205  if ($handle = opendir($config['install_dir'] . '/' . $dir))
3206  {
3207    while (false !== ($file = readdir($handle)))
3208    {
3209      if (filetype($config['install_dir'] . '/' . $dir . '/' . $file) == 'file' && preg_match($regex, $file))
3210      {
3211        print_debug("Including: " . $config['install_dir'] . '/' . $dir . '/' . $file);
3212
3213        include($config['install_dir'] . '/' . $dir . '/' . $file);
3214      }
3215    }
3216
3217    closedir($handle);
3218  }
3219}
3220
3221// DOCME needs phpdoc block
3222// TESTME needs unit testing
3223function is_port_valid($port, $device)
3224{
3225  global $config;
3226  $valid = TRUE;
3227
3228  if (isset($port['ifOperStatus']) && strlen($port['ifOperStatus']) && // Currently skiped empty ifOperStatus for exclude false positives
3229      !in_array($port['ifOperStatus'], array('testing', 'dormant', 'down', 'lowerLayerDown', 'unknown', 'up', 'monitoring')))
3230  {
3231    // See http://tools.cisco.com/Support/SNMP/do/BrowseOID.do?objectInput=ifOperStatus
3232    $valid = FALSE;
3233    print_debug("ignored (by ifOperStatus = notPresent or invalid value).");
3234  }
3235
3236  $ports_skip_ifType = isset($config['os'][$device['os']]['ports_skip_ifType']) && $config['os'][$device['os']]['ports_skip_ifType'];
3237  if ($valid && !isset($port['ifType']) && !$ports_skip_ifType)
3238  {
3239    /* Some devices (ie D-Link) report ports without any usefull info, example:
3240    [74] => Array
3241        (
3242            [ifName] => po22
3243            [ifInMulticastPkts] => 0
3244            [ifInBroadcastPkts] => 0
3245            [ifOutMulticastPkts] => 0
3246            [ifOutBroadcastPkts] => 0
3247            [ifLinkUpDownTrapEnable] => enabled
3248            [ifHighSpeed] => 0
3249            [ifPromiscuousMode] => false
3250            [ifConnectorPresent] => false
3251            [ifAlias] => po22
3252            [ifCounterDiscontinuityTime] => 0:0:00:00.00
3253        )
3254    */
3255    $valid = FALSE;
3256    print_debug("ignored (by empty ifType).");
3257  }
3258
3259  if ($port['ifDescr'] === '' && $config['os'][$device['os']]['ifType_ifDescr'] && $port['ifIndex'])
3260  {
3261    // This happen on some liebert UPS devices
3262    $type = rewrite_iftype($port['ifType']);
3263    if ($type)
3264    {
3265      $port['ifDescr'] = $type . ' ' . $port['ifIndex'];
3266    }
3267  }
3268
3269  $if = ($config['os'][$device['os']]['ifname'] ? $port['ifName'] : $port['ifDescr']);
3270
3271  if ($valid && is_array($config['bad_if']))
3272  {
3273    foreach ($config['bad_if'] as $bi)
3274    {
3275      if (stripos($port['ifDescr'], $bi) !== FALSE)
3276      {
3277        $valid = FALSE;
3278        print_debug("ignored (by ifDescr): ".$port['ifDescr']." [ $bi ]");
3279        break;
3280      }
3281      elseif (stripos($port['ifName'], $bi) !== FALSE)
3282      {
3283        $valid = FALSE;
3284        print_debug("ignored (by ifName): ".$port['ifName']." [ $bi ]");
3285        break;
3286      }
3287    }
3288  }
3289
3290  if ($valid && is_array($config['bad_ifalias_regexp']))
3291  {
3292    foreach ($config['bad_ifalias_regexp'] as $bi)
3293    {
3294      if (preg_match($bi . 'i', $port['ifAlias']))
3295      {
3296        $valid = FALSE;
3297        print_debug("ignored (by ifAlias): ".$port['ifAlias']." [ $bi ]");
3298        break;
3299      }
3300    }
3301  }
3302
3303  if ($valid && is_array($config['bad_if_regexp']))
3304  {
3305    foreach ($config['bad_if_regexp'] as $bi)
3306    {
3307      if (preg_match($bi . 'i', $port['ifName']))
3308      {
3309        $valid = FALSE;
3310        print_debug("ignored (by ifName regexp): ".$port['ifName']." [ $bi ]");
3311        break;
3312      }
3313      elseif (preg_match($bi . 'i', $port['ifDescr']))
3314      {
3315        $valid = FALSE;
3316        print_debug("ignored (by ifDescr regexp): ".$port['ifDescr']." [ $bi ]");
3317        break;
3318      }
3319    }
3320  }
3321
3322  if ($valid && is_array($config['bad_iftype']))
3323  {
3324    foreach ($config['bad_iftype'] as $bi)
3325    {
3326      if (strpos($port['ifType'], $bi) !== FALSE)
3327      {
3328        $valid = FALSE;
3329        print_debug("ignored (by ifType): ".$port['ifType']." [ $bi ]");
3330        break;
3331      }
3332    }
3333  }
3334  if ($valid && empty($port['ifDescr']) && empty($port['ifName']))
3335  {
3336    $valid = FALSE;
3337    print_debug("ignored (by empty ifDescr and ifName).");
3338  }
3339  if ($valid && $device['os'] == 'catos' && strstr($if, "vlan")) { $valid = FALSE; }
3340
3341  return $valid;
3342}
3343
3344/**
3345 * Validate BGP peer
3346 *
3347 * @param array $peer BGP peer array from discovery or polling process
3348 * @param array $device Common device array
3349 * @return boolean TRUE if peer array valid
3350 */
3351function is_bgp_peer_valid($peer, $device)
3352{
3353  $valid = TRUE;
3354
3355  if (isset($peer['admin_status']) && empty($peer['admin_status']))
3356  {
3357    $valid = FALSE;
3358    print_debug("Peer ignored (by empty Admin Status).");
3359  }
3360
3361  if ($valid && !(is_numeric($peer['as']) && $peer['as'] != 0))
3362  {
3363    $valid = FALSE;
3364    print_debug("Peer ignored (by invalid AS number '".$peer['as']."').");
3365  }
3366
3367  if ($valid && !get_ip_version($peer['ip']))
3368  {
3369    $valid = FALSE;
3370    print_debug("Peer ignored (by invalid Remote IP '".$peer['ip']."').");
3371  }
3372
3373  return $valid;
3374}
3375
3376/**
3377 * Detect is BGP AS number in private range, see:
3378 * https://tools.ietf.org/html/rfc6996
3379 * https://tools.ietf.org/html/rfc7300
3380 *
3381 * @param string|int $as AS number
3382 * @return boolean TRUE if AS number in private range
3383 */
3384function is_bgp_as_private($as)
3385{
3386  $as = bgp_asdot_to_asplain($as); // Convert ASdot to ASplain
3387
3388  // Note 65535 and 5294967295 not really Private ASNs,
3389  // this is Reserved for use by Well-known Communities
3390  $private = ($as >= 64512      && $as <= 65535) ||    // 16-bit private ASn
3391             ($as >= 4200000000 && $as <= 5294967295); // 32-bit private ASn
3392
3393  return $private;
3394}
3395
3396/**
3397 * Convert AS number from asplain to asdot format (for 32bit ASn).
3398 *
3399 * @param string|int $as AS number in plain or dot format
3400 * @return string AS number in dot format (for 32bit ASn)
3401 */
3402function bgp_asplain_to_asdot($as)
3403{
3404  if (strpos($as, '.') || // Already asdot format
3405      ($as < 65536))      // 16bit ASn no need to formatting
3406  {
3407    return $as;
3408  }
3409
3410  $as2 = $as % 65536;
3411  $as1 = ($as - $as2) / 65536;
3412
3413  return intval($as1) . '.' . intval($as2);
3414}
3415
3416/**
3417 * Convert AS number from asdot to asplain format (for 32bit ASn).
3418 *
3419 * @param string|int $as AS number in plain or dot format
3420 * @return string AS number in plain format (for 32bit ASn)
3421 */
3422function bgp_asdot_to_asplain($as)
3423{
3424  if (strpos($as, '.') === FALSE)   // Already asplain format
3425  {
3426    return $as;
3427  }
3428
3429  list($as1, $as2) = explode('.', $as, 2);
3430  $as = $as1 * 65536 + $as2;
3431
3432  return "$as";
3433}
3434
3435/**
3436 * Convert BGP peer index to vendor MIB specific entries
3437 *
3438 * @param array $peer Array with walked peer oids
3439 * @param string $index Peer index
3440 * @param string $mib MIB name
3441 */
3442function parse_bgp_peer_index(&$peer, $index, $mib = 'BGP4V2-MIB')
3443{
3444  $address_types = $GLOBALS['config']['mibs']['INET-ADDRESS-MIB']['rewrite']['InetAddressType'];
3445  $index_parts   = explode('.', $index);
3446  switch ($mib)
3447  {
3448    case 'BGP4-MIB':
3449      // bgpPeerRemoteAddr
3450      if (get_ip_version($index))
3451      {
3452        $peer['bgpPeerRemoteAddr'] = $index;
3453      }
3454      break;
3455
3456    case 'ARISTA-BGP4V2-MIB':
3457      // 1. aristaBgp4V2PeerInstance
3458      $peer['aristaBgp4V2PeerInstance'] = array_shift($index_parts);
3459      // 2. aristaBgp4V2PeerRemoteAddrType
3460      $peer_addr_type = array_shift($index_parts);
3461      if (strlen($peer['aristaBgp4V2PeerRemoteAddrType']) == 0)
3462      {
3463        $peer['aristaBgp4V2PeerRemoteAddrType'] = $peer_addr_type;
3464      }
3465      if (isset($address_types[$peer['aristaBgp4V2PeerRemoteAddrType']]))
3466      {
3467        $peer['aristaBgp4V2PeerRemoteAddrType'] = $address_types[$peer['aristaBgp4V2PeerRemoteAddrType']];
3468      }
3469      // 3. length of the IP address
3470      $ip_len = array_shift($index_parts);
3471      // 4. IP address
3472      $ip_parts = array_slice($index_parts, 0, $ip_len);
3473
3474      // 5. aristaBgp4V2PeerRemoteAddr
3475      $peer_ip = implode('.', $ip_parts);
3476      if ($ip_len == 16)
3477      {
3478        $peer_ip = snmp2ipv6($peer_ip);
3479      }
3480      if ($peer_addr_type = get_ip_version($peer_ip))
3481      {
3482        $peer['aristaBgp4V2PeerRemoteAddr']     = $peer_ip;
3483        $peer['aristaBgp4V2PeerRemoteAddrType'] = 'ipv' . $peer_addr_type; // FIXME. not sure, but seems as Arista use only ipv4/ipv6 for afi
3484      }
3485      break;
3486
3487    case 'BGP4V2-MIB':
3488    case 'FOUNDRY-BGP4V2-MIB': // BGP4V2-MIB draft
3489      // 1. bgp4V2PeerInstance
3490      $peer['bgp4V2PeerInstance'] = array_shift($index_parts);
3491      // 2. bgp4V2PeerLocalAddrType
3492      $local_addr_type = array_shift($index_parts);
3493      if (strlen($peer['bgp4V2PeerLocalAddrType']) == 0)
3494      {
3495        $peer['bgp4V2PeerLocalAddrType'] = $local_addr_type;
3496      }
3497      if (isset($address_types[$peer['bgp4V2PeerLocalAddrType']]))
3498      {
3499        $peer['bgp4V2PeerLocalAddrType'] = $address_types[$peer['bgp4V2PeerLocalAddrType']];
3500      }
3501      // 3. length of the local IP address
3502      $ip_len = array_shift($index_parts);
3503      // 4. IP address
3504      $ip_parts = array_slice($index_parts, 0, $ip_len);
3505
3506      // 5. bgp4V2PeerLocalAddr
3507      $local_ip = implode('.', $ip_parts);
3508      if ($ip_len == 16)
3509      {
3510        $local_ip = snmp2ipv6($local_ip);
3511      }
3512      if (get_ip_version($local_ip))
3513      {
3514        $peer['bgp4V2PeerLocalAddr'] = $local_ip;
3515      }
3516
3517      // Get second part of index
3518      $index_parts = array_slice($index_parts, $ip_len);
3519      $peer_addr_type = array_shift($index_parts);
3520      if (strlen($peer['bgp4V2PeerRemoteAddrType']) == 0)
3521      {
3522        $peer['bgp4V2PeerRemoteAddrType'] = $peer_addr_type;
3523      }
3524      if (isset($address_types[$peer['bgp4V2PeerRemoteAddrType']]))
3525      {
3526        $peer['bgp4V2PeerRemoteAddrType'] = $address_types[$peer['bgp4V2PeerRemoteAddrType']];
3527      }
3528      // 6. length of the IP address
3529      $ip_len = array_shift($index_parts);
3530      // 7. IP address
3531      $ip_parts = array_slice($index_parts, 0, $ip_len);
3532
3533      // 8. bgp4V2PeerRemoteAddr
3534      $peer_ip = implode('.', $ip_parts);
3535      if ($ip_len == 16)
3536      {
3537        $peer_ip = snmp2ipv6($peer_ip);
3538      }
3539      if ($peer_addr_type = get_ip_version($peer_ip))
3540      {
3541        $peer['bgp4V2PeerRemoteAddr']     = $peer_ip;
3542        $peer['bgp4V2PeerRemoteAddrType'] = 'ipv' . $peer_addr_type;
3543      }
3544      break;
3545
3546    case 'BGP4-V2-MIB-JUNIPER':
3547      // 1. jnxBgpM2PeerRoutingInstance
3548      $peer['jnxBgpM2PeerRoutingInstance'] = array_shift($index_parts);
3549      // 2. jnxBgpM2PeerLocalAddrType
3550      $local_addr_type = array_shift($index_parts);
3551      if (strlen($peer['jnxBgpM2PeerLocalAddrType']) == 0)
3552      {
3553        $peer['jnxBgpM2PeerLocalAddrType'] = $local_addr_type;
3554      }
3555      if (isset($address_types[$peer['jnxBgpM2PeerLocalAddrType']]))
3556      {
3557        $peer['jnxBgpM2PeerLocalAddrType'] = $address_types[$peer['jnxBgpM2PeerLocalAddrType']];
3558      }
3559      // 3. length of the local IP address
3560      $ip_len = (strstr($peer['jnxBgpM2PeerLocalAddrType'], 'ipv6') ? 16 : 4);
3561      // 4. IP address
3562      $ip_parts = array_slice($index_parts, 0, $ip_len);
3563
3564      // 5. jnxBgpM2PeerLocalAddr
3565      $local_ip = implode('.', $ip_parts);
3566      if ($ip_len == 16)
3567      {
3568        $local_ip = snmp2ipv6($local_ip);
3569      }
3570      if (get_ip_version($local_ip))
3571      {
3572        $peer['jnxBgpM2PeerLocalAddr'] = $local_ip;
3573      }
3574
3575      // Get second part of index
3576      $index_parts = array_slice($index_parts, $ip_len);
3577      // 6. jnxBgpM2PeerRemoteAddrType
3578      $peer_addr_type = array_shift($index_parts);
3579      if (strlen($peer['jnxBgpM2PeerRemoteAddrType']) == 0)
3580      {
3581        $peer['jnxBgpM2PeerRemoteAddrType'] = $peer_addr_type;
3582      }
3583      if (isset($address_types[$peer['jnxBgpM2PeerRemoteAddrType']]))
3584      {
3585        $peer['jnxBgpM2PeerRemoteAddrType'] = $address_types[$peer['jnxBgpM2PeerRemoteAddrType']];
3586      }
3587      // 7. length of the remote IP address
3588      $ip_len = (strstr($peer['jnxBgpM2PeerRemoteAddrType'], 'ipv6') ? 16 : 4);
3589      // 8. IP address
3590      $ip_parts = array_slice($index_parts, 0, $ip_len);
3591
3592      // 9. jnxBgpM2PeerRemoteAddr
3593      $peer_ip = implode('.', $ip_parts);
3594      if ($ip_len == 16)
3595      {
3596        $peer_ip = snmp2ipv6($peer_ip);
3597      }
3598      if (get_ip_version($peer_ip))
3599      {
3600        $peer['jnxBgpM2PeerRemoteAddr'] = $peer_ip;
3601      }
3602      break;
3603
3604    case 'FORCE10-BGP4-V2-MIB':
3605      // 1. f10BgpM2PeerInstance
3606      $peer['f10BgpM2PeerInstance'] = array_shift($index_parts);
3607      // 2. f10BgpM2PeerLocalAddrType
3608      $local_addr_type = array_shift($index_parts);
3609      if (strlen($peer['f10BgpM2PeerLocalAddrType']) == 0)
3610      {
3611        $peer['f10BgpM2PeerLocalAddrType'] = $local_addr_type;
3612      }
3613      if (isset($address_types[$peer['f10BgpM2PeerLocalAddrType']]))
3614      {
3615        $peer['f10BgpM2PeerLocalAddrType'] = $address_types[$peer['f10BgpM2PeerLocalAddrType']];
3616      }
3617      // 3. length of the local IP address
3618      $ip_len = (strstr($peer['f10BgpM2PeerLocalAddrType'], 'ipv6') ? 16 : 4);
3619      // 4. IP address
3620      $ip_parts = array_slice($index_parts, 0, $ip_len);
3621
3622      // 5. f10BgpM2PeerLocalAddr
3623      $local_ip = implode('.', $ip_parts);
3624      if ($ip_len == 16)
3625      {
3626        $local_ip = snmp2ipv6($local_ip);
3627      }
3628      if (get_ip_version($local_ip))
3629      {
3630        $peer['f10BgpM2PeerLocalAddr'] = $local_ip;
3631      }
3632
3633      // Get second part of index
3634      $index_parts = array_slice($index_parts, $ip_len);
3635      // 6. f10BgpM2PeerRemoteAddrType
3636      $peer_addr_type = array_shift($index_parts);
3637      if (strlen($peer['f10BgpM2PeerRemoteAddrType']) == 0)
3638      {
3639        $peer['f10BgpM2PeerRemoteAddrType'] = $peer_addr_type;
3640      }
3641      if (isset($address_types[$peer['f10BgpM2PeerRemoteAddrType']]))
3642      {
3643        $peer['f10BgpM2PeerRemoteAddrType'] = $address_types[$peer['f10BgpM2PeerRemoteAddrType']];
3644      }
3645      // 7. length of the remote IP address
3646      $ip_len = (strstr($peer['f10BgpM2PeerRemoteAddrType'], 'ipv6') ? 16 : 4);
3647      // 8. IP address
3648      $ip_parts = array_slice($index_parts, 0, $ip_len);
3649
3650      // 9. f10BgpM2PeerRemoteAddr
3651      $peer_ip = implode('.', $ip_parts);
3652      if ($ip_len == 16)
3653      {
3654        $peer_ip = snmp2ipv6($peer_ip);
3655      }
3656      if (get_ip_version($peer_ip))
3657      {
3658        $peer['f10BgpM2PeerRemoteAddr'] = $peer_ip;
3659      }
3660      break;
3661
3662  }
3663
3664}
3665
3666# Parse CSV files with or without header, and return a multidimensional array
3667// DOCME needs phpdoc block
3668// TESTME needs unit testing
3669// MOVEME to includes/common.inc.php
3670function parse_csv($content, $has_header = 1, $separator = ",")
3671{
3672  $lines = explode("\n", $content);
3673  $result = array();
3674
3675  # If the CSV file has a header, load up the titles into $headers
3676  if ($has_header)
3677  {
3678    $headcount = 1;
3679    $header = array_shift($lines);
3680    foreach (explode($separator,$header) as $heading)
3681    {
3682      if (trim($heading) != "")
3683      {
3684        $headers[$headcount] = trim($heading);
3685        $headcount++;
3686      }
3687    }
3688  }
3689
3690  # Process every line
3691  foreach ($lines as $line)
3692  {
3693    if ($line != "")
3694    {
3695      $entrycount = 1;
3696      foreach (explode($separator,$line) as $entry)
3697      {
3698        # If we use header, place the value inside the named array entry
3699        # Otherwise, just stuff it in numbered fields in the array
3700        if (trim($entry) != "")
3701        {
3702          if ($has_header)
3703          {
3704            $line_array[$headers[$entrycount]] = trim($entry);
3705          } else {
3706            $line_array[] = trim($entry);
3707          }
3708        }
3709        $entrycount++;
3710      }
3711
3712      # Add resulting line array to final result
3713      $result[] = $line_array; unset($line_array);
3714    }
3715  }
3716
3717  return $result;
3718}
3719
3720function get_defined_settings()
3721{
3722  $config = [];
3723  include($GLOBALS['config']['install_dir'] . "/config.php");
3724
3725  return $config;
3726}
3727
3728function get_default_settings()
3729{
3730  $config = [];
3731  include($GLOBALS['config']['install_dir'] . "/includes/defaults.inc.php");
3732
3733  return $config;
3734}
3735
3736// Load configuration from SQL into supplied variable (pass by reference!)
3737function load_sqlconfig(&$config)
3738{
3739  $config_defined = get_defined_settings(); // defined in config.php
3740
3741  // Override some whitelisted definitions from config.php
3742  foreach ($config_defined as $key => $definition)
3743  {
3744    if (in_array($key, $config['definitions_whitelist']) && version_compare(PHP_VERSION, '5.3.0') >= 0 &&
3745        is_array($definition) && is_array($config[$key]))
3746    {
3747      /* Fix mib definitions for dumb users, who copied old defaults.php
3748         where mibs was just MIB => 1,
3749         This definition should be array */
3750      // Fetch first element and validate that this is array
3751      if ($key == 'mibs' && !is_array(array_shift(array_values($definition)))) { continue; }
3752
3753      $config[$key] = array_replace_recursive($config[$key], $definition);
3754    }
3755  }
3756
3757  foreach (dbFetchRows("SELECT * FROM `config`") as $item)
3758  {
3759    // Convert boo|bee|baa config value into $config['boo']['bee']['baa']
3760    $tree = explode('|', $item['config_key']);
3761
3762    //if (array_key_exists($tree[0], $config_defined)) { continue; } // This complete skip option if first level key defined in $config
3763
3764    // Unfortunately, I don't know of a better way to do this...
3765    // Perhaps using array_map() ? Unclear... hacky. :[
3766    // FIXME use a loop with references! (cf. nested location menu)
3767    switch (count($tree))
3768    {
3769      case 1:
3770        //if (isset($config_defined[$tree[0]])) { continue; } // Note, false for null values
3771        if (array_key_exists($tree[0], $config_defined)) { break; }
3772        $config[$tree[0]] = unserialize($item['config_value']);
3773        break;
3774      case 2:
3775        if (isset($config_defined[$tree[0]][$tree[1]])) { break; } // Note, false for null values
3776        $config[$tree[0]][$tree[1]] = unserialize($item['config_value']);
3777        break;
3778      case 3:
3779        if (isset($config_defined[$tree[0]][$tree[1]][$tree[2]])) { break; } // Note, false for null values
3780        $config[$tree[0]][$tree[1]][$tree[2]] = unserialize($item['config_value']);
3781        break;
3782      case 4:
3783        if (isset($config_defined[$tree[0]][$tree[1]][$tree[2]][$tree[3]])) { break; } // Note, false for null values
3784        $config[$tree[0]][$tree[1]][$tree[2]][$tree[3]] = unserialize($item['config_value']);
3785        break;
3786      case 5:
3787        if (isset($config_defined[$tree[0]][$tree[1]][$tree[2]][$tree[3]][$tree[4]])) { break; } // Note, false for null values
3788        $config[$tree[0]][$tree[1]][$tree[2]][$tree[3]][$tree[4]] = unserialize($item['config_value']);
3789        break;
3790      default:
3791        print_error("Too many array levels for SQL configuration parser!");
3792    }
3793  }
3794}
3795
3796function isset_array_key($key, &$array, $split = '|')
3797{
3798  // Convert boo|bee|baa key into $array['boo']['bee']['baa']
3799  $tree = explode($split, $key);
3800
3801  switch (count($tree))
3802  {
3803    case 1:
3804      //if (isset($array[$tree[0]])) { continue; } // Note, false for null values
3805      return array_key_exists($tree[0], $array);
3806      break;
3807    case 2:
3808      //if (isset($array[$tree[0]][$tree[1]])) { continue; } // Note, false for null values
3809      return isset($array[$tree[0]]) && array_key_exists($tree[1], $array[$tree[0]]);
3810      break;
3811    case 3:
3812      //if (isset($array[$tree[0]][$tree[1]][$tree[2]])) { continue; } // Note, false for null values
3813      return isset($array[$tree[0]][$tree[1]]) && array_key_exists($tree[2], $array[$tree[0]][$tree[1]]);
3814      break;
3815    case 4:
3816      //if (isset($array[$tree[0]][$tree[1]][$tree[2]][$tree[3]])) { continue; } // Note, false for null values
3817      return isset($array[$tree[0]][$tree[1]][$tree[2]]) && array_key_exists($tree[3], $array[$tree[0]][$tree[1]][$tree[2]]);
3818      break;
3819    case 5:
3820      //if (isset($array[$tree[0]][$tree[1]][$tree[2]][$tree[3]][$tree[4]])) { continue; } // Note, false for null values
3821      return isset($array[$tree[0]][$tree[1]][$tree[2]][$tree[3]]) && array_key_exists($tree[4], $array[$tree[0]][$tree[1]][$tree[2]][$tree[3]]);
3822      break;
3823    default:
3824      print_error("Too many array levels for array");
3825  }
3826
3827  return FALSE;
3828}
3829
3830function set_sql_config($key, $value, $force = TRUE)
3831{
3832
3833  if (!$force && // Currently configuration store forced also if not exist in defaults
3834      !isset_array_key($key, $GLOBALS['config']))
3835  {
3836    print_error("Not exist config key ($key).");
3837    return FALSE;
3838  }
3839
3840  $s_value = serialize($value); // in db we store serialized config value
3841
3842  $sql = 'SELECT * FROM `config` WHERE `config_key` = ? LIMIT 1';
3843  if ($in_db = dbFetchRow($sql, [$key]))
3844  {
3845    // Exist, compare? and update
3846    if ($s_value != $in_db[$key])
3847    {
3848      dbUpdate(['config_value' => $s_value], 'config', '`config_key` = ?', [$key]);
3849    }
3850  }
3851  else {
3852    // Insert new
3853    dbInsert(array('config_key' => $key, 'config_value' => $s_value), 'config');
3854  }
3855
3856  return TRUE;
3857}
3858
3859function del_sql_config($key)
3860{
3861  if (dbExist('config', '`config_key` = ?', [$key]))
3862  {
3863    dbDelete('config', '`config_key` = ?', [$key]);
3864  }
3865
3866  return TRUE;
3867}
3868
3869// Convert SI scales to scalar scale. Example return:
3870// si_to_scale('milli');    // return 0.001
3871// si_to_scale('femto', 8); // return 1.0E-23
3872// si_to_scale('-2');       // return 0.01
3873// DOCME needs phpdoc block
3874// MOVEME to includes/common.inc.php
3875function si_to_scale($si = 'units', $precision = NULL)
3876{
3877  // See all scales here: http://tools.cisco.com/Support/SNMP/do/BrowseOID.do?local=en&translate=Translate&typeName=SensorDataScale
3878  $si       = strtolower($si);
3879  $si_array = array('yocto' => -24, 'zepto' => -21, 'atto'  => -18,
3880                    'femto' => -15, 'pico'  => -12, 'nano'  => -9,
3881                    'micro' => -6,  'milli' => -3,  'centi' => -2,
3882                    'deci'  => -1,  'units' => 0,   'deca'  => 1,
3883                    'hecto' => 2,   'kilo'  => 3,   'mega'  => 6,
3884                    'giga'  => 9,   'tera'  => 12,  'peta'  => 15,
3885                    'exa'   => 18,  'zetta' => 21,  'yotta' => 24);
3886  $exp = 0;
3887  if (isset($si_array[$si]))
3888  {
3889    $exp = $si_array[$si];
3890  }
3891  else if (is_numeric($si))
3892  {
3893    $exp = (int)$si;
3894  }
3895
3896  if (is_numeric($precision) && $precision > 0)
3897  {
3898    /**
3899     * NOTES. For EntitySensorPrecision:
3900     *  If an object of this type contains a value in the range 1 to 9, it represents the number of decimal places in the
3901     *  fractional part of an associated EntitySensorValue fixed-point number.
3902     *  If an object of this type contains a value in the range -8 to -1, it represents the number of accurate digits in the
3903     *  associated EntitySensorValue fixed-point number.
3904     */
3905    $exp -= (int)$precision;
3906  }
3907
3908  $scale = pow(10, $exp);
3909
3910  return $scale;
3911}
3912
3913/**
3914 * Compare variables considering epsilon for float numbers
3915 * returns: 0 - variables same, 1 - $a greater than $b, -1 - $a less than $b
3916 *
3917 * @param mixed $a First compare number
3918 * @param mixed $b Second compare number
3919 * @param float $epsilon
3920 *
3921 * @return integer $compare
3922 */
3923// MOVEME to includes/common.inc.php
3924function float_cmp($a, $b, $epsilon = NULL)
3925{
3926  $epsilon = (is_numeric($epsilon) ? abs((float)$epsilon) : 0.00001); // Default epsilon for float compare
3927  $compare = FALSE;
3928  $both    = 0;
3929  // Convert to float if possible
3930  if (is_numeric($a)) { $a = (float)$a; $both++; }
3931  if (is_numeric($b)) { $b = (float)$b; $both++; }
3932
3933  if ($both === 2)
3934  {
3935    // Compare numeric variables as float numbers
3936    // Based on compare logic from http://floating-point-gui.de/errors/comparison/
3937    if ($a === $b)
3938    {
3939      $compare = 0; // Variables same
3940      $test = 0;
3941    } else {
3942      $diff = abs($a - $b);
3943      //$pow_epsilon = pow($epsilon, 2);
3944      if ($a == 0 || $b == 0)
3945      {
3946        // Around zero
3947        $test    = $diff;
3948        $epsilon = pow($epsilon, 2);
3949        if ($test < $epsilon) { $compare = 0; }
3950      } else {
3951        // Note, still exist issue with numbers around zero (ie: -0.00000001, 0.00000002)
3952        $test = $diff / min(abs($a) + abs($b), PHP_INT_MAX);
3953        if ($test < $epsilon) { $compare = 0; }
3954      }
3955    }
3956
3957    if (OBS_DEBUG > 1)
3958    {
3959      print_message('Compare float numbers: "'.$a.'" with "'.$b.'", epsilon: "'.$epsilon.'", comparision: "'.$test.' < '.$epsilon.'", numbers: '.($compare === 0 ? 'SAME' : 'DIFFERENT'));
3960    }
3961  } else {
3962    // All other compare as usual
3963    if ($a === $b)
3964    {
3965      $compare = 0; // Variables same
3966    }
3967  }
3968  if ($compare === FALSE)
3969  {
3970    // Compare if variables not same
3971    if ($a > $b)
3972    {
3973      $compare = 1;  // $a greater than $b
3974    } else {
3975      $compare = -1; // $a less than $b
3976    }
3977  }
3978
3979  return $compare;
3980}
3981
3982/**
3983 * Add integer numbers.
3984 * This function better to use with big Counter64 numbers
3985 *
3986 * @param int|string $a The first number
3987 * @param int|string $b The second number
3988 * @return string       A number representing the sum of the arguments.
3989 */
3990function int_add($a, $b)
3991{
3992  switch (OBS_MATH)
3993  {
3994    case 'gmp':
3995      // Convert values to string
3996      $a = gmp_init_float($a);
3997      $b = gmp_init_float($b);
3998      // Better to use GMP extension, for more correct operations with big numbers
3999      // $a = "18446742978492891134"; $b = "0"; $sum = gmp_add($a, $b); echo gmp_strval($sum) . "\n"; // Result: 18446742978492891134
4000      // $a = "18446742978492891134"; $b = "0"; $sum = $a + $b; printf("%.0f\n", $sum);               // Result: 18446742978492891136
4001      $sum = gmp_add($a, $b);
4002      $sum = gmp_strval($sum); // Convert GMP number to string
4003      print_debug("GMP ADD: $a + $b = $sum");
4004      break;
4005    case 'bc':
4006      // Convert values to string
4007      $a = strval($a);
4008      $b = strval($b);
4009      $sum = bcadd($a, $b);
4010      print_debug("BC ADD: $a + $b = $sum");
4011      break;
4012    default:
4013      // Fallback to php math
4014      $sum = $a + $b;
4015      // Convert this values to int string, for prevent rrd update error with big Counter64 numbers,
4016      // see: http://jira.observium.org/browse/OBSERVIUM-1749
4017      $sum = sprintf("%.0f", $sum);
4018      print_debug("PHP ADD: $a + $b = $sum");
4019  }
4020
4021  return $sum;
4022}
4023
4024/**
4025 * Subtract integer numbers.
4026 * This function better to use with big Counter64 numbers
4027 *
4028 * @param int|string $a The first number
4029 * @param int|string $b The second number
4030 * @return string       A number representing the subtract of the arguments.
4031 */
4032function int_sub($a, $b)
4033{
4034  switch (OBS_MATH)
4035  {
4036    case 'gmp':
4037      // Convert values to string
4038      $a = gmp_init_float($a);
4039      $b = gmp_init_float($b);
4040      $sub = gmp_sub($a, $b);
4041      $sub = gmp_strval($sub); // Convert GMP number to string
4042      print_debug("GMP SUB: $a - $b = $sub");
4043      break;
4044    case 'bc':
4045      // Convert values to string
4046      $a = strval($a);
4047      $b = strval($b);
4048      $sub = bcsub($a, $b);
4049      print_debug("BC SUB: $a - $b = $sub");
4050      break;
4051    default:
4052      // Fallback to php math
4053      $sub = $a - $b;
4054      // Convert this values to int string, for prevent rrd update error with big Counter64 numbers,
4055      // see: http://jira.observium.org/browse/OBSERVIUM-1749
4056      $sub = sprintf("%.0f", $sub);
4057      print_debug("PHP SUB: $a - $b = $sub");
4058  }
4059
4060  return $sub;
4061}
4062
4063/**
4064 * GMP have troubles with float number math
4065 *
4066 * php > $sum = 1111111111111111111111111.1; echo sprintf("%.0f", $sum)."\n"; echo sprintf("%d", $sum)."\n"; echo strval($sum)."\n"; echo $sum;
40671111111111111111092469760
40688375319363669983232
40691.1111111111111E+24
40701.1111111111111E+24
4071php > $sum = "1111111111111111111111111.1"; echo sprintf("%.0f", $sum)."\n"; echo sprintf("%d", $sum)."\n"; echo strval($sum)."\n"; echo $sum;
40721111111111111111092469760
40739223372036854775807
40741111111111111111111111111.1
40751111111111111111111111111.1
4076 */
4077function gmp_init_float($value)
4078{
4079  if (is_int($value))
4080  {
4081    return $value;
4082  }
4083  if (is_float($value))
4084  {
4085    return sprintf("%.0f", $value);
4086  }
4087  if (strpos($value, '.') !== FALSE)
4088  {
4089    // Return int part of string
4090    list($value) = explode('.', $value);
4091    return $value;
4092  }
4093
4094  return "$value";
4095}
4096
4097// Translate syslog priorities from string to numbers
4098// ie: ('emerg','alert','crit','err','warning','notice') >> ('0', '1', '2', '3', '4', '5')
4099// Note, this is safe function, for unknown data return 15
4100// DOCME needs phpdoc block
4101function priority_string_to_numeric($value)
4102{
4103  $priority = 15; // Default priority for unknown data
4104  if (!is_numeric($value))
4105  {
4106    foreach ($GLOBALS['config']['syslog']['priorities'] as $pri => $entry)
4107    {
4108      if (stripos($entry['name'], substr($value, 0, 3)) === 0) { $priority = $pri; break; }
4109    }
4110  }
4111  else if ($value == (int)$value && $value >= 0 && $value < 16)
4112  {
4113    $priority = (int)$value;
4114  }
4115
4116  return $priority;
4117}
4118
4119/**
4120 * Translate syslog facilities from string to numeric
4121 *
4122 */
4123function facility_string_to_numeric($facility)
4124{
4125  if (!is_numeric($facility))
4126  {
4127    foreach ($GLOBALS['config']['syslog']['facilities'] as $f => $entry)
4128    {
4129      if ($entry['name'] == $facility)
4130      {
4131        $facility = $f;
4132        break;
4133      }
4134    }
4135  }
4136  else if ($facility == (int)$facility && $facility >= 0 && $facility <= 23)
4137  {
4138    $facility = (int)$facility;
4139  }
4140
4141  return $facility;
4142}
4143
4144function array_merge_indexed(&$array1, &$array2)
4145{
4146  $merged = $array1;
4147  //print_vars($merged);
4148
4149  foreach ($array2 as $key => &$value)
4150  {
4151    if (is_array($value) && isset($merged[$key]) && is_array($merged[$key]))
4152    {
4153      $merged[$key] = array_merge_indexed($merged[$key], $value);
4154    } else {
4155      $merged[$key] = $value;
4156    }
4157  }
4158
4159  //print_vars($merged);
4160  return $merged;
4161}
4162
4163// Merge 2 arrays by their index, ie:
4164//  Array( [1] => [TestCase] = '1' ) + Array( [1] => [Bananas] = 'Yes )
4165// becomes
4166//  Array( [1] => [TestCase] = '1', [Bananas] = 'Yes' )
4167//
4168// array_merge_recursive() only works for string keys, not numeric as we get from snmp functions.
4169//
4170// Accepts infinite parameters.
4171//
4172// Currently not used. Does not cope well with multilevel arrays.
4173// DOCME needs phpdoc block
4174// MOVEME to includes/common.inc.php
4175/*
4176function array_merge_indexed()
4177{
4178  $array = array();
4179
4180  foreach (func_get_args() as $array2)
4181  {
4182    if (count($array2) == 0) continue; // Skip for loop for empty array, infinite loop ahead.
4183    for ($i = 0; $i <= count($array2); $i++)
4184    {
4185      foreach (array_keys($array2[$i]) as $key)
4186      {
4187        $array[$i][$key] = $array2[$i][$key];
4188      }
4189    }
4190  }
4191
4192  return $array;
4193}
4194*/
4195
4196// DOCME needs phpdoc block
4197// TESTME needs unit testing
4198function print_cli_heading($contents, $level = 2)
4199{
4200  if (OBS_QUIET || !is_cli()) { return; } // Silent exit if not cli or quiet
4201
4202//  $tl = html_entity_decode('&#x2554;', ENT_NOQUOTES, 'UTF-8'); // top left corner
4203//  $tr = html_entity_decode('&#x2557;', ENT_NOQUOTES, 'UTF-8'); // top right corner
4204//  $bl = html_entity_decode('&#x255a;', ENT_NOQUOTES, 'UTF-8'); // bottom left corner
4205//  $br = html_entity_decode('&#x255d;', ENT_NOQUOTES, 'UTF-8'); // bottom right corner
4206//  $v = html_entity_decode('&#x2551;', ENT_NOQUOTES, 'UTF-8');  // vertical wall
4207//  $h = html_entity_decode('&#x2550;', ENT_NOQUOTES, 'UTF-8');  // horizontal wall
4208
4209//  print_message($tl . str_repeat($h, strlen($contents)+2)  . $tr . "\n" .
4210//                $v  . ' '.$contents.' '   . $v  . "\n" .
4211//                $bl . str_repeat($h, strlen($contents)+2)  . $br . "\n", 'color');
4212
4213  $level_colours = array('0' => '%W', '1' => '%g', '2' => '%c' , '3' => '%p');
4214
4215  //print_message(str_repeat("  ", $level). $level_colours[$level]."#####  %W". $contents ."%n\n", 'color');
4216  print_message($level_colours[$level]."#####  %W". $contents .$level_colours[$level]."  #####%n\n", 'color');
4217}
4218
4219// DOCME needs phpdoc block
4220// TESTME needs unit testing
4221function print_cli_data($field, $data = NULL, $level = 2)
4222{
4223  if (OBS_QUIET || !is_cli()) { return; } // Silent exit if not cli or quiet
4224
4225  //$level_colours = array('0' => '%W', '1' => '%g', '2' => '%c' , '3' => '%p');
4226
4227  //print_cli(str_repeat("  ", $level) . $level_colours[$level]."  o %W".str_pad($field, 20). "%n ");
4228  //print_cli($level_colours[$level]." o %W".str_pad($field, 20). "%n "); // strlen == 24
4229  print_cli_data_field($field, $level);
4230
4231  $field_len = 0;
4232  $max_len = 110;
4233
4234  $lines = explode("\n", $data);
4235
4236  foreach ($lines as $line)
4237  {
4238    $len = strlen($line) + 24;
4239    if ($len > $max_len)
4240    {
4241      $len = $field_len;
4242      $data = explode(" ", $line);
4243      foreach ($data as $datum)
4244      {
4245        $len = $len + strlen($datum);
4246        if ($len > $max_len)
4247        {
4248          $len = strlen($datum);
4249          //$datum = "\n". str_repeat(" ", 26+($level * 2)). $datum;
4250          $datum = "\n". str_repeat(" ", 24). $datum;
4251        } else {
4252          $datum .= ' ';
4253        }
4254        print_cli($datum);
4255      }
4256    } else {
4257      $datum = str_repeat(" ", $field_len). $line;
4258      print_cli($datum);
4259    }
4260    $field_len = 24;
4261    print_cli(PHP_EOL);
4262  }
4263}
4264
4265// DOCME needs phpdoc block
4266// TESTME needs unit testing
4267function print_cli_data_field($field, $level = 2)
4268{
4269  if (OBS_QUIET || !is_cli()) { return; } // Silent exit if not cli or quiet
4270
4271  $level_colours = array('0' => '%W', '1' => '%g', '2' => '%c' , '3' => '%p', '4' => '%y');
4272
4273  // print_cli(str_repeat("  ", $level) . $level_colours[$level]."  o %W".str_pad($field, 20). "%n ");
4274  print_cli($level_colours[$level]." o %W".str_pad($field, 20). "%n ");
4275}
4276
4277// DOCME needs phpdoc block
4278// TESTME needs unit testing
4279function print_cli_table($table_rows, $table_header = array(), $descr = NULL, $options = array())
4280{
4281  // FIXME, probably need ability to view this tables in WUI?!
4282  if (OBS_QUIET || !is_cli()) { return; } // Silent exit if not cli or quiet
4283
4284  if (!is_array($table_rows)) { print_debug("print_cli_table() argument $table_rows should be an array. Please report this error to developers."); return; }
4285
4286  if (!cli_is_piped() || OBS_DEBUG)
4287  {
4288    $count_rows   = count($table_rows);
4289    if ($count_rows == 0) { return; }
4290
4291    if (strlen($descr))
4292    {
4293      print_cli_data($descr, '', 3);
4294    }
4295
4296    // Init table and renderer
4297    $table = new \cli\Table();
4298    cli\Colors::enable(TRUE);
4299
4300    // Set default maximum width globally
4301    if (!isset($options['max-table-width']))
4302    {
4303      $options['max-table-width'] = 240;
4304      //$options['max-table-width']  = TRUE;
4305    }
4306    // WARNING, min-column-width not worked in cli Class, I wait when issue will fixed
4307    //$options['min-column-width'] = 30;
4308    if (!empty($options))
4309    {
4310      $renderer = new cli\Table\Ascii;
4311      if (isset($options['max-table-width']))
4312      {
4313        if ($options['max-table-width'] === TRUE)
4314        {
4315          // Set maximum table width as available columns in terminal
4316          $options['max-table-width'] = cli\Shell::columns();
4317        }
4318        if (is_numeric($options['max-table-width']))
4319        {
4320          $renderer->setConstraintWidth($options['max-table-width']);
4321        }
4322      }
4323      if (isset($options['min-column-width']))
4324      {
4325        $cols = array();
4326        foreach (current($table_rows) as $col)
4327        {
4328          $cols[] = $options['min-column-width'];
4329        }
4330        //var_dump($cols);
4331        $renderer->setWidths($cols);
4332      }
4333      $table->setRenderer($renderer);
4334    }
4335
4336    $count_header = count($table_header);
4337    if ($count_header)
4338    {
4339      $table->setHeaders($table_header);
4340    }
4341    $table->setRows($table_rows);
4342    $table->display();
4343    echo(PHP_EOL);
4344  } else {
4345    print_cli_data("Notice", "Table output suppressed due to piped output.".PHP_EOL);
4346  }
4347}
4348
4349/**
4350 * Prints Observium banner containing ASCII logo and version information for use in CLI utilities.
4351 */
4352function print_cli_banner()
4353{
4354  if (OBS_QUIET || !is_cli()) { return; } // Silent exit if not cli or quiet
4355
4356  print_message("%W
4357  ___   _                              _
4358 / _ \ | |__   ___   ___  _ __ __   __(_) _   _  _ __ ___
4359| | | || '_ \ / __| / _ \| '__|\ \ / /| || | | || '_ ` _ \
4360| |_| || |_) |\__ \|  __/| |    \ V / | || |_| || | | | | |
4361 \___/ |_.__/ |___/ \___||_|     \_/  |_| \__,_||_| |_| |_|%c
4362".
4363  str_pad(OBSERVIUM_PRODUCT_LONG." ".OBSERVIUM_VERSION, 59, " ", STR_PAD_LEFT)."\n".
4364  str_pad("http://www.observium.org" , 59, " ", STR_PAD_LEFT)."%N\n", 'color');
4365
4366  // One time alert about deprecated (eol) php version
4367  if (version_compare(PHP_VERSION, OBS_MIN_PHP_VERSION, '<'))
4368  {
4369    $php_version = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
4370    print_message("
4371
4372+---------------------------------------------------------+
4373|                                                         |
4374|                %rDANGER! ACHTUNG! BHUMAHUE!%n               |
4375|                                                         |
4376".
4377    str_pad("| %WYour PHP version is too old (%r".$php_version."%W),", 64, ' ')."%n|
4378| %Wfunctionality may be broken. Please update your PHP!%n    |
4379| %WCurrently recommended version(s): >%g7.1.x%n                |
4380|                                                         |
4381| See additional information here:                        |
4382| %c".
4383  str_pad(OBSERVIUM_URL . '/docs/software_requirements/' , 56, ' ')."%n|
4384|                                                         |
4385+---------------------------------------------------------+
4386", 'color');
4387  }
4388}
4389
4390// TESTME needs unit testing
4391/**
4392 * Creates a list of php files available in the html/pages/front directory, to show in a
4393 * dropdown on the web configuration page.
4394 *
4395 * @return array List of front page files available
4396 */
4397function config_get_front_page_files()
4398{
4399  global $config;
4400
4401  $frontpages = array();
4402
4403  foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($config['html_dir'] . '/pages/front')) as $file)
4404  {
4405    $filename = $file->getFileName();
4406    if ($filename[0] != '.')
4407    {
4408      $frontpages["pages/front/$filename"] = nicecase(basename($filename,'.php'));
4409    }
4410  }
4411
4412  return $frontpages;
4413}
4414
4415/**
4416 * Triggers a rediscovery of the given device at the following discovery -h new run.
4417 *
4418 * @param array $device  Device array.
4419 * @param array $modules Array with modules required for rediscovery, if empty rediscover device full
4420 *
4421 * @return mixed Status of added or not force device discovery
4422 */
4423// TESTME needs unit testing
4424function force_discovery($device, $modules = array())
4425{
4426  $return = FALSE;
4427
4428  if (count($modules) == 0)
4429  {
4430    // Modules not passed, just full rediscover device
4431    $return = dbUpdate(array('force_discovery' => 1), 'devices', '`device_id` = ?', array($device['device_id']));
4432  } else {
4433    // Modules passed, check if modules valid and enabled
4434    $modules = (array)$modules;
4435    $forced_modules = get_entity_attrib('device', $device['device_id'], 'force_discovery_modules');
4436    if ($forced_modules)
4437    {
4438      // Already forced modules exist, merge it with new
4439      $modules = array_unique(array_merge($modules, json_decode($forced_modules, TRUE)));
4440    }
4441
4442    $valid_modules = array();
4443    foreach ($GLOBALS['config']['discovery_modules'] as $module => $ok)
4444    {
4445      // Filter by valid and enabled modules
4446      if ($ok && in_array($module, $modules))
4447      {
4448        $valid_modules[] = $module;
4449      }
4450    }
4451
4452    if (count($valid_modules))
4453    {
4454      $return = dbUpdate(array('force_discovery' => 1), 'devices', '`device_id` = ?', array($device['device_id']));
4455      set_entity_attrib('device', $device['device_id'], 'force_discovery_modules', json_encode($valid_modules));
4456    }
4457  }
4458
4459  return $return;
4460}
4461
4462// From http://stackoverflow.com/questions/9339619/php-checking-if-the-last-character-is-a-if-not-then-tack-it-on
4463// Assumed free to use :)
4464// DOCME needs phpdoc block
4465// TESTME needs unit testing
4466function fix_path_slash($p)
4467{
4468    $p = str_replace('\\','/',trim($p));
4469    return (substr($p,-1)!='/') ? $p.='/' : $p;
4470}
4471
4472/**
4473 * Calculates missing fields of a mempool based on supplied information and returns them all.
4474 * This function also applies the scaling as requested.
4475 * Also works for storage.
4476 *
4477 * @param float $scale   Scaling to apply to the supplied values.
4478 * @param int   $used    Used value of mempool, before scaling, or NULL.
4479 * @param int   $total   Total value of mempool, before scaling, or NULL.
4480 * @param int   $free    Free value of mempool, before scaling, or NULL.
4481 * @param int   $perc    Used percentage value of mempool, or NULL.
4482 * @param array $options Additional options, ie separate scales for used/total/free
4483 *
4484 * @return array Array consisting of 'used', 'total', 'free' and 'perc' fields
4485 */
4486function calculate_mempool_properties($scale, $used, $total, $free, $perc = NULL, $options = array())
4487{
4488  // Scale, before maths!
4489  foreach (array('total', 'used', 'free') as $param)
4490  {
4491    if (is_numeric($$param))
4492    {
4493      if (isset($options['scale_'.$param]))
4494      {
4495        // Separate sclae for current param
4496        $$param *= $options['scale_'.$param];
4497      }
4498      else if ($scale != 0 && $scale != 1)
4499      {
4500        // Common scale
4501        $$param *= $scale;
4502      }
4503    }
4504  }
4505
4506  if (is_numeric($total) && is_numeric($free))
4507  {
4508    $used = $total - $free;
4509    $perc = round($used / $total * 100, 2);
4510  }
4511  else if (is_numeric($used) && is_numeric($free))
4512  {
4513    $total = $used + $free;
4514    $perc = round($used / $total * 100, 2);
4515  }
4516  else if (is_numeric($total) && is_numeric($perc))
4517  {
4518    $used = $total * $perc / 100;
4519    $free = $total - $used;
4520  }
4521  else if (is_numeric($total) && is_numeric($used))
4522  {
4523    $free = $total - $used;
4524    $perc = round($used / $total * 100, 2);
4525  }
4526  else if (is_numeric($perc))
4527  {
4528    $total  = 100;
4529    $used   = $perc;
4530    $free   = 100 - $perc;
4531    //$scale  = 1; // Reset scale for percentage-only
4532  }
4533  if (OBS_DEBUG && ($perc < 0 || $perc > 100))
4534  {
4535    print_error('Incorrect scales or passed params to function ' . __FUNCTION__ . '()');
4536  }
4537
4538  return array('used' => $used, 'total' => $total, 'free' => $free, 'perc' => $perc);
4539}
4540
4541/**
4542 * Get all values from specific key in a multidimensional array
4543 *
4544 * @param $key string
4545 * @param $arr array
4546 * @return null|string|array
4547 */
4548
4549function array_value_recursive($key, array $arr){
4550    $val = array();
4551    array_walk_recursive($arr, function($v, $k) use($key, &$val){
4552        if($k == $key) array_push($val, $v);
4553    });
4554    return count($val) > 1 ? $val : array_pop($val);
4555}
4556
4557function discovery_check_if_type_exist(&$valid, $entry, $entity_type)
4558{
4559
4560  if (isset($entry['skip_if_valid_exist']))
4561  {
4562    $tree = explode('->', $entry['skip_if_valid_exist']);
4563    //print_vars($tree);
4564    switch (count($tree))
4565    {
4566      case 1:
4567        if (isset($valid[$entity_type][$tree[0]]) &&
4568            count($valid[$entity_type][$tree[0]]))
4569        {
4570          print_debug("Excluded by valid exist: ".$entry['skip_if_valid_exist']);
4571          return TRUE;
4572        }
4573        break;
4574      case 2:
4575        if (isset($valid[$entity_type][$tree[0]][$tree[1]]) &&
4576            count($valid[$entity_type][$tree[0]][$tree[1]]))
4577        {
4578          print_debug("Excluded by valid exist: ".$entry['skip_if_valid_exist']);
4579          return TRUE;
4580        }
4581        break;
4582      case 3:
4583        if (isset($valid[$entity_type][$tree[0]][$tree[1]][$tree[2]]) &&
4584            count($valid[$entity_type][$tree[0]][$tree[1]][$tree[2]]))
4585        {
4586          print_debug("Excluded by valid exist: ".$entry['skip_if_valid_exist']);
4587          return TRUE;
4588        }
4589        break;
4590      default:
4591        print_debug("Too many array levels for valid sensor!");
4592    }
4593  }
4594  return FALSE;
4595}
4596
4597function discovery_check_requires_pre($device, $entry, $entity_type)
4598{
4599
4600  if (isset($entry['pre_test']) && is_array($entry['pre_test']))
4601  {
4602    // Convert single test condition to multi-level condition
4603    if (isset($entry['pre_test']['operator'])) {
4604      $entry['pre_test'] = array($entry['pre_test']);
4605    }
4606
4607    foreach ($entry['pre_test'] as $test)
4608    {
4609      if (isset($test['oid']))
4610      {
4611        // Fetch just the value eof the OID.
4612        $test['data'] = snmp_cache_oid($device, $test['oid'], NULL, NULL, OBS_SNMP_ALL);
4613        $oid = $test['oid'];
4614      }
4615      else if (isset($test['field']))
4616      {
4617        $test['data'] = $entry[$test['field']];
4618        $oid = $test['field'];
4619      } else {
4620        print_debug("Not correct Field (".$test['field'].") passed to discovery_check_requires(). Need add it to 'oid_extra' definition.");
4621        return FALSE;
4622      }
4623      if (test_condition($test['data'], $test['operator'], $test['value']) === FALSE)
4624      {
4625        print_debug("Excluded by not test condition: $oid [".$test['data']."] ".$test['operator']." [".implode(', ', (array)$test['value'])."]");
4626        return TRUE;
4627      }
4628    }
4629  }
4630
4631  return FALSE;
4632}
4633
4634function discovery_check_requires($device, $entry, $array, $entity_type)
4635{
4636  if (isset($entry['test']) && is_array($entry['test']))
4637  {
4638    // Convert single test condition to multi-level condition
4639    if (isset($entry['test']['operator'])) {
4640      $entry['test'] = array($entry['test']);
4641    }
4642
4643    foreach ($entry['test'] as $test)
4644    {
4645      if (isset($test['oid']))
4646      {
4647        // Fetch just the value eof the OID.
4648        $test['data'] = snmp_cache_oid($device, $test['oid'], NULL, NULL, OBS_SNMP_ALL);
4649        $oid = $test['oid'];
4650      }
4651      else if (isset($test['field']))
4652      {
4653        $test['data'] = $array[$test['field']];
4654        if (!isset($array[$test['field']]))
4655        {
4656          // Show debug error (some time Oid fetched, but not exist for current index)
4657          print_debug("Not correct Field (" . $test['field'] . ") passed to discovery_check_requires(). Need add it to 'oid_extra' definition.");
4658          //return FALSE;
4659        }
4660        $oid = $test['field'];
4661      }
4662      if (test_condition($test['data'], $test['operator'], $test['value']) === FALSE)
4663      {
4664        print_debug("Excluded by not test condition: $oid [".$test['data']."] ".$test['operator']." [".implode(', ', (array)$test['value'])."]");
4665        return TRUE;
4666      }
4667    }
4668  }
4669
4670  return FALSE;
4671}
4672
4673
4674// EOF
4675