Passed
Push — master ( 164f3a...71da38 )
by Neil
13:25 queued 01:00
created

compare_var()   D

Complexity

Conditions 19
Paths 19

Size

Total Lines 41
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 39
nc 19
nop 3
dl 0
loc 41
rs 4.5166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * LibreNMS
5
 *
6
 *   This file is part of LibreNMS.
7
 *
8
 * @package    LibreNMS
9
 * @subpackage functions
10
 * @copyright  (C) 2006 - 2012 Adam Armstrong
11
 *
12
 */
13
14
use Illuminate\Database\Events\QueryExecuted;
15
use LibreNMS\Authentication\LegacyAuth;
16
use LibreNMS\Config;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Config. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use LibreNMS\Exceptions\HostExistsException;
18
use LibreNMS\Exceptions\HostIpExistsException;
19
use LibreNMS\Exceptions\HostUnreachableException;
20
use LibreNMS\Exceptions\HostUnreachablePingException;
21
use LibreNMS\Exceptions\InvalidPortAssocModeException;
22
use LibreNMS\Exceptions\LockException;
23
use LibreNMS\Exceptions\SnmpVersionUnsupportedException;
24
use LibreNMS\Util\IPv4;
25
use LibreNMS\Util\IPv6;
26
use LibreNMS\Util\MemcacheLock;
27
use Symfony\Component\Process\Process;
28
use PHPMailer\PHPMailer\PHPMailer;
29
use LibreNMS\Util\Time;
30
31
if (!function_exists('set_debug')) {
32
    /**
33
     * Set debugging output
34
     *
35
     * @param bool $state If debug is enabled or not
36
     * @param bool $silence When not debugging, silence every php error
37
     * @return bool
38
     */
39
    function set_debug($state = true, $silence = false)
40
    {
41
        global $debug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
42
43
        $debug = $state; // set to global
44
45
        restore_error_handler(); // disable Laravel error handler
46
47
        if (isset($debug) && $debug) {
48
            ini_set('display_errors', 1);
49
            ini_set('display_startup_errors', 1);
50
            ini_set('log_errors', 0);
51
            error_reporting(E_ALL & ~E_NOTICE);
52
53
            \LibreNMS\Util\Laravel::enableCliDebugOutput();
54
            \LibreNMS\Util\Laravel::enableQueryDebug();
55
        } else {
56
            ini_set('display_errors', 0);
57
            ini_set('display_startup_errors', 0);
58
            ini_set('log_errors', 1);
59
            error_reporting($silence ? 0 : E_ERROR);
60
61
            \LibreNMS\Util\Laravel::disableCliDebugOutput();
62
            \LibreNMS\Util\Laravel::disableQueryDebug();
63
        }
64
65
        return $debug;
66
    }
67
}//end set_debug()
68
69
function array_sort_by_column($array, $on, $order = SORT_ASC)
70
{
71
    $new_array = array();
72
    $sortable_array = array();
73
74
    if (count($array) > 0) {
75
        foreach ($array as $k => $v) {
76
            if (is_array($v)) {
77
                foreach ($v as $k2 => $v2) {
78
                    if ($k2 == $on) {
79
                        $sortable_array[$k] = $v2;
80
                    }
81
                }
82
            } else {
83
                $sortable_array[$k] = $v;
84
            }
85
        }
86
87
        switch ($order) {
88
            case SORT_ASC:
89
                asort($sortable_array);
90
                break;
91
            case SORT_DESC:
92
                arsort($sortable_array);
93
                break;
94
        }
95
96
        foreach ($sortable_array as $k => $v) {
97
            $new_array[$k] = $array[$k];
98
        }
99
    }
100
    return $new_array;
101
}
102
103
function mac_clean_to_readable($mac)
104
{
105
    return \LibreNMS\Util\Rewrite::readableMac($mac);
106
}
107
108
function only_alphanumeric($string)
109
{
110
    return preg_replace('/[^a-zA-Z0-9]/', '', $string);
111
}
112
113
/**
114
 * Parse cli discovery or poller modules and set config for this run
115
 *
116
 * @param string $type discovery or poller
117
 * @param array $options get_opts array (only m key is checked)
118
 * @return bool
119
 */
120
function parse_modules($type, $options)
121
{
122
    $override = false;
123
124
    if ($options['m']) {
125
        Config::set("{$type}_modules", []);
126
        foreach (explode(',', $options['m']) as $module) {
127
            // parse submodules (only supported by some modules)
128
            if (str_contains($module, '/')) {
129
                list($module, $submodule) = explode('/', $module, 2);
130
                $existing_submodules = Config::get("{$type}_submodules.$module", []);
131
                $existing_submodules[] = $submodule;
132
                Config::set("{$type}_submodules.$module", $existing_submodules);
133
            }
134
135
            $dir = $type == 'poller' ? 'polling' : $type;
136
            if (is_file("includes/$dir/$module.inc.php")) {
137
                Config::set("{$type}_modules.$module", 1);
138
                $override = true;
139
            }
140
        }
141
142
        // display selected modules
143
        $modules = array_map(function ($module) use ($type) {
144
            $submodules = Config::get("{$type}_submodules.$module");
145
            return $module . ($submodules ? '(' . implode(',', $submodules) . ')' : '');
146
        }, array_keys(Config::get("{$type}_modules", [])));
147
148
        d_echo("Override $type modules: " . implode(', ', $modules) . PHP_EOL);
149
    }
150
151
    return $override;
152
}
153
154
function logfile($string)
155
{
156
    $fd = fopen(Config::get('log_file'), 'a');
157
    fputs($fd, $string . "\n");
0 ignored issues
show
Bug introduced by
It seems like $fd can also be of type false; however, parameter $handle of fputs() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
    fputs(/** @scrutinizer ignore-type */ $fd, $string . "\n");
Loading history...
158
    fclose($fd);
0 ignored issues
show
Bug introduced by
It seems like $fd can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
    fclose(/** @scrutinizer ignore-type */ $fd);
Loading history...
159
}
160
161
/**
162
 * Detect the os of the given device.
163
 *
164
 * @param array $device device to check
165
 * @return string the name of the os
166
 */
167
function getHostOS($device)
168
{
169
    $device['sysDescr']    = snmp_get($device, "SNMPv2-MIB::sysDescr.0", "-Ovq");
170
    $device['sysObjectID'] = snmp_get($device, "SNMPv2-MIB::sysObjectID.0", "-Ovqn");
171
172
    d_echo("| {$device['sysDescr']} | {$device['sysObjectID']} | \n");
173
174
    $deferred_os = array(
175
        'freebsd',
176
        'linux',
177
    );
178
179
    // check yaml files
180
    $os_defs = Config::get('os');
181
    foreach ($os_defs as $os => $def) {
182
        if (isset($def['discovery']) && !in_array($os, $deferred_os)) {
183
            foreach ($def['discovery'] as $item) {
184
                if (checkDiscovery($device, $item)) {
185
                    return $os;
186
                }
187
            }
188
        }
189
    }
190
191
    // check include files
192
    $os = null;
193
    $pattern = Config::get('install_dir') . '/includes/discovery/os/*.inc.php';
194
    foreach (glob($pattern) as $file) {
195
        include $file;
196
        if (isset($os)) {
197
            return $os;
198
        }
199
    }
200
201
    // check deferred os
202
    foreach ($deferred_os as $os) {
203
        if (isset($os_defs[$os]['discovery'])) {
204
            foreach ($os_defs[$os]['discovery'] as $item) {
205
                if (checkDiscovery($device, $item)) {
206
                    return $os;
207
                }
208
            }
209
        }
210
    }
211
212
    return 'generic';
213
}
214
215
/**
216
 * Check an array of conditions if all match, return true
217
 * sysObjectID if sysObjectID starts with any of the values under this item
218
 * sysDescr if sysDescr contains any of the values under this item
219
 * sysDescr_regex if sysDescr matches any of the regexes under this item
220
 * snmpget perform an snmpget on `oid` and check if the result contains `value`. Other subkeys: options, mib, mibdir
221
 *
222
 * Appending _except to any condition will invert the match.
223
 *
224
 * @param array $device
225
 * @param array $array Array of items, keys should be sysObjectID, sysDescr, or sysDescr_regex
226
 * @return bool the result (all items passed return true)
227
 */
228
function checkDiscovery($device, $array)
229
{
230
    // all items must be true
231
    foreach ($array as $key => $value) {
232
        if ($check = ends_with($key, '_except')) {
233
            $key = substr($key, 0, -7);
234
        }
235
236
        if ($key == 'sysObjectID') {
237
            if (starts_with($device['sysObjectID'], $value) == $check) {
238
                return false;
239
            }
240
        } elseif ($key == 'sysDescr') {
241
            if (str_contains($device['sysDescr'], $value) == $check) {
242
                return false;
243
            }
244
        } elseif ($key == 'sysDescr_regex') {
245
            if (preg_match_any($device['sysDescr'], $value) == $check) {
246
                return false;
247
            }
248
        } elseif ($key == 'sysObjectID_regex') {
249
            if (preg_match_any($device['sysObjectID'], $value) == $check) {
250
                return false;
251
            }
252
        } elseif ($key == 'snmpget') {
253
            $options = isset($value['options']) ? $value['options'] : '-Oqv';
254
            $mib = isset($value['mib']) ? $value['mib'] : null;
255
            $mib_dir = isset($value['mib_dir']) ? $value['mib_dir'] : null;
256
            $op = isset($value['op']) ? $value['op'] : 'contains';
257
258
            $get_value = snmp_get($device, $value['oid'], $options, $mib, $mib_dir);
259
            if (compare_var($get_value, $value['value'], $op) == $check) {
260
                return false;
261
            }
262
        }
263
    }
264
265
    return true;
266
}
267
268
/**
269
 * Check an array of regexes against a subject if any match, return true
270
 *
271
 * @param string $subject the string to match against
272
 * @param array|string $regexes an array of regexes or single regex to check
273
 * @return bool if any of the regexes matched, return true
274
 */
275
function preg_match_any($subject, $regexes)
276
{
277
    foreach ((array)$regexes as $regex) {
278
        if (preg_match($regex, $subject)) {
279
            return true;
280
        }
281
    }
282
    return false;
283
}
284
285
/**
286
 * Perform comparison of two items based on give comparison method
287
 * Valid comparisons: =, !=, ==, !==, >=, <=, >, <, contains, starts, ends, regex
288
 * contains, starts, ends: $a haystack, $b needle(s)
289
 * regex: $a subject, $b regex
290
 *
291
 * @param mixed $a
292
 * @param mixed $b
293
 * @param string $comparison =, !=, ==, !== >=, <=, >, <, contains, starts, ends, regex
294
 * @return bool
295
 */
296
function compare_var($a, $b, $comparison = '=')
297
{
298
    switch ($comparison) {
299
        case "=":
300
            return $a == $b;
301
        case "!=":
302
            return $a != $b;
303
        case "==":
304
            return $a === $b;
305
        case "!==":
306
            return $a !== $b;
307
        case ">=":
308
            return $a >= $b;
309
        case "<=":
310
            return $a <= $b;
311
        case ">":
312
            return $a > $b;
313
        case "<":
314
            return $a < $b;
315
        case "contains":
316
            return str_contains($a, $b);
317
        case "not_contains":
318
            return !str_contains($a, $b);
319
        case "starts":
320
            return starts_with($a, $b);
321
        case "not_starts":
322
            return !starts_with($a, $b);
323
        case "ends":
324
            return ends_with($a, $b);
325
        case "not_ends":
326
            return !ends_with($a, $b);
327
        case "regex":
328
            return (bool)preg_match($b, $a);
329
        case "not regex":
330
            return !((bool)preg_match($b, $a));
331
        case "in_array":
332
            return in_array($a, $b);
333
        case "not_in_array":
334
            return !in_array($a, $b);
335
        default:
336
            return false;
337
    }
338
}
339
340
function percent_colour($perc)
341
{
342
    $r = min(255, 5 * ($perc - 25));
343
    $b = max(0, 255 - (5 * ($perc + 25)));
344
345
    return sprintf('#%02x%02x%02x', $r, $b, $b);
346
}
347
348
// Returns the last in/out errors value in RRD
349
function interface_errors($rrd_file, $period = '-1d')
350
{
351
    $errors = array();
352
353
    $cmd = Config::get('rrdtool') . " fetch -s $period -e -300s $rrd_file AVERAGE | grep : | cut -d\" \" -f 4,5";
354
    $data = trim(shell_exec($cmd));
355
    $in_errors = 0;
356
    $out_errors = 0;
357
    foreach (explode("\n", $data) as $entry) {
358
        list($in, $out) = explode(" ", $entry);
359
        $in_errors += ($in * 300);
360
        $out_errors += ($out * 300);
361
    }
362
    $errors['in'] = round($in_errors);
363
    $errors['out'] = round($out_errors);
364
365
    return $errors;
366
}
367
368
/**
369
 * @param $device
370
 * @return string the logo image path for this device. Images are often wide, not square.
371
 */
372
function getLogo($device)
373
{
374
    $img = getImageName($device, true, 'images/logos/');
375
    if (!starts_with($img, 'generic')) {
376
        return 'images/logos/' . $img;
377
    }
378
379
    return getIcon($device);
380
}
381
382
/**
383
 * @param array $device
384
 * @param string $class to apply to the image tag
385
 * @return string an image tag with the logo for this device. Images are often wide, not square.
386
 */
387
function getLogoTag($device, $class = null)
388
{
389
    $tag = '<img src="' . url(getLogo($device)) . '" title="' . getImageTitle($device) . '"';
390
    if (isset($class)) {
391
        $tag .= " class=\"$class\" ";
392
    }
393
    $tag .= ' />';
394
    return  $tag;
395
}
396
397
/**
398
 * @param $device
399
 * @return string the path to the icon image for this device.  Close to square.
400
 */
401
function getIcon($device)
402
{
403
    return 'images/os/' . getImageName($device);
404
}
405
406
/**
407
 * @param $device
408
 * @return string an image tag with the icon for this device.  Close to square.
409
 */
410
function getIconTag($device)
411
{
412
    return '<img src="' . getIcon($device) . '" title="' . getImageTitle($device) . '"/>';
413
}
414
415
function getImageTitle($device)
416
{
417
    return $device['icon'] ? str_replace(array('.svg', '.png'), '', $device['icon']) : $device['os'];
418
}
419
420
function getImageName($device, $use_database = true, $dir = 'images/os/')
421
{
422
    return \LibreNMS\Util\Url::findOsImage($device['os'], $device['features'], $use_database ? $device['icon'] : null, $dir);
423
}
424
425
function renamehost($id, $new, $source = 'console')
426
{
427
    $host = gethostbyid($id);
428
429
    if (!is_dir(get_rrd_dir($new)) && rename(get_rrd_dir($host), get_rrd_dir($new)) === true) {
430
        dbUpdate(['hostname' => $new, 'ip' => null], 'devices', 'device_id=?', [$id]);
431
        log_event("Hostname changed -> $new ($source)", $id, 'system', 3);
432
        return '';
433
    }
434
435
    log_event("Renaming of $host failed", $id, 'system', 5);
436
    return "Renaming of $host failed\n";
437
}
438
439
function delete_device($id)
440
{
441
    global $debug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
442
443
    if (isCli() === false) {
444
        ignore_user_abort(true);
445
        set_time_limit(0);
446
    }
447
448
    $ret = '';
449
450
    $host = dbFetchCell("SELECT hostname FROM devices WHERE device_id = ?", array($id));
451
    if (empty($host)) {
452
        return "No such host.";
453
    }
454
455
    // Remove IPv4/IPv6 addresses before removing ports as they depend on port_id
456
    dbQuery("DELETE `ipv4_addresses` FROM `ipv4_addresses` INNER JOIN `ports` ON `ports`.`port_id`=`ipv4_addresses`.`port_id` WHERE `device_id`=?", array($id));
457
    dbQuery("DELETE `ipv6_addresses` FROM `ipv6_addresses` INNER JOIN `ports` ON `ports`.`port_id`=`ipv6_addresses`.`port_id` WHERE `device_id`=?", array($id));
458
459
    foreach (dbFetch("SELECT * FROM `ports` WHERE `device_id` = ?", array($id)) as $int_data) {
460
        $int_if = $int_data['ifDescr'];
461
        $int_id = $int_data['port_id'];
462
        delete_port($int_id);
463
        $ret .= "Removed interface $int_id ($int_if)\n";
464
    }
465
466
    // Remove sensors manually due to constraints
467
    foreach (dbFetchRows("SELECT * FROM `sensors` WHERE `device_id` = ?", array($id)) as $sensor) {
468
        $sensor_id = $sensor['sensor_id'];
469
        dbDelete('sensors_to_state_indexes', "`sensor_id` = ?", array($sensor_id));
470
    }
471
    $fields = array('device_id','host');
472
473
    $db_name = dbFetchCell('SELECT DATABASE()');
474
    foreach ($fields as $field) {
475
        foreach (dbFetch("SELECT table_name FROM information_schema.columns WHERE table_schema = ? AND column_name = ?", [$db_name, $field]) as $table) {
476
            $table = $table['table_name'];
477
            $entries = (int) dbDelete($table, "`$field` =  ?", array($id));
478
            if ($entries > 0 && $debug === true) {
479
                $ret .= "$field@$table = #$entries\n";
480
            }
481
        }
482
    }
483
484
    $ex = shell_exec("bash -c '( [ ! -d ".trim(get_rrd_dir($host))." ] || rm -vrf ".trim(get_rrd_dir($host))." 2>&1 ) && echo -n OK'");
485
    $tmp = explode("\n", $ex);
486
    if ($tmp[sizeof($tmp)-1] != "OK") {
487
        $ret .= "Could not remove files:\n$ex\n";
488
    }
489
490
    $ret .= "Removed device $host\n";
491
    log_event("Device $host has been removed", 0, 'system', 3);
492
    oxidized_reload_nodes();
493
    return $ret;
494
}
495
496
/**
497
 * Add a device to LibreNMS
498
 *
499
 * @param string $host dns name or ip address
500
 * @param string $snmp_version If this is empty, try v2c,v3,v1.  Otherwise, use this specific version.
501
 * @param string $port the port to connect to for snmp
502
 * @param string $transport udp or tcp
503
 * @param string $poller_group the poller group this device will belong to
504
 * @param boolean $force_add add even if the device isn't reachable
505
 * @param string $port_assoc_mode snmp field to use to determine unique ports
506
 * @param array $additional an array with additional parameters to take into consideration when adding devices
507
 *
508
 * @return int returns the device_id of the added device
509
 *
510
 * @throws HostExistsException This hostname already exists
511
 * @throws HostIpExistsException We already have a host with this IP
512
 * @throws HostUnreachableException We could not reach this device is some way
513
 * @throws HostUnreachablePingException We could not ping the device
514
 * @throws InvalidPortAssocModeException The given port association mode was invalid
515
 * @throws SnmpVersionUnsupportedException The given snmp version was invalid
516
 */
517
function addHost($host, $snmp_version = '', $port = '161', $transport = 'udp', $poller_group = '0', $force_add = false, $port_assoc_mode = 'ifIndex', $additional = array())
518
{
519
    // Test Database Exists
520
    if (host_exists($host)) {
521
        throw new HostExistsException("Already have host $host");
522
    }
523
524
    // Valid port assoc mode
525
    if (!in_array($port_assoc_mode, get_port_assoc_modes())) {
526
        throw new InvalidPortAssocModeException("Invalid port association_mode '$port_assoc_mode'. Valid modes are: " . join(', ', get_port_assoc_modes()));
527
    }
528
529
    // check if we have the host by IP
530
    if (Config::get('addhost_alwayscheckip') === true) {
531
        $ip = gethostbyname($host);
532
    } else {
533
        $ip = $host;
534
    }
535
    if ($force_add !== true && $device = device_has_ip($ip)) {
536
        $message = "Cannot add $host, already have device with this IP $ip";
537
        if ($ip != $device->hostname) {
538
            $message .= " ($device->hostname)";
539
        }
540
        $message .= '. You may force add to ignore this.';
541
        throw new HostIpExistsException($message);
542
    }
543
544
    // Test reachability
545
    if (!$force_add) {
546
        $address_family = snmpTransportToAddressFamily($transport);
547
        $ping_result = isPingable($host, $address_family);
548
        if (!$ping_result['result']) {
549
            throw new HostUnreachablePingException("Could not ping $host");
550
        }
551
    }
552
553
    // if $snmpver isn't set, try each version of snmp
554
    if (empty($snmp_version)) {
555
        $snmpvers = Config::get('snmp.version');
556
    } else {
557
        $snmpvers = array($snmp_version);
558
    }
559
560
    if (isset($additional['snmp_disable']) && $additional['snmp_disable'] == 1) {
561
        return createHost($host, '', $snmp_version, $port, $transport, array(), $poller_group, 1, true, $additional);
0 ignored issues
show
Bug introduced by
$port of type string is incompatible with the type integer expected by parameter $port of createHost(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

561
        return createHost($host, '', $snmp_version, /** @scrutinizer ignore-type */ $port, $transport, array(), $poller_group, 1, true, $additional);
Loading history...
Bug introduced by
$poller_group of type string is incompatible with the type integer expected by parameter $poller_group of createHost(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

561
        return createHost($host, '', $snmp_version, $port, $transport, array(), /** @scrutinizer ignore-type */ $poller_group, 1, true, $additional);
Loading history...
562
    }
563
    $host_unreachable_exception = new HostUnreachableException("Could not connect to $host, please check the snmp details and snmp reachability");
564
    // try different snmp variables to add the device
565
    foreach ($snmpvers as $snmpver) {
566
        if ($snmpver === "v3") {
567
            // Try each set of parameters from config
568
            foreach (Config::get('snmp.v3') as $v3) {
569
                $device = deviceArray($host, null, $snmpver, $port, $transport, $v3, $port_assoc_mode);
570
                if ($force_add === true || isSNMPable($device)) {
571
                    return createHost($host, null, $snmpver, $port, $transport, $v3, $poller_group, $port_assoc_mode, $force_add);
572
                } else {
573
                    $host_unreachable_exception->addReason("SNMP $snmpver: No reply with credentials " . $v3['authname'] . "/" . $v3['authlevel']);
574
                }
575
            }
576
        } elseif ($snmpver === "v2c" || $snmpver === "v1") {
577
            // try each community from config
578
            foreach (Config::get('snmp.community') as $community) {
579
                $device = deviceArray($host, $community, $snmpver, $port, $transport, null, $port_assoc_mode);
580
581
                if ($force_add === true || isSNMPable($device)) {
582
                    return createHost($host, $community, $snmpver, $port, $transport, array(), $poller_group, $port_assoc_mode, $force_add);
583
                } else {
584
                    $host_unreachable_exception->addReason("SNMP $snmpver: No reply with community $community");
585
                }
586
            }
587
        } else {
588
            throw new SnmpVersionUnsupportedException("Unsupported SNMP Version \"$snmpver\", must be v1, v2c, or v3");
589
        }
590
    }
591
    if (isset($additional['ping_fallback']) && $additional['ping_fallback'] == 1) {
592
        $additional['snmp_disable'] = 1;
593
        $additional['os'] = "ping";
594
        return createHost($host, '', $snmp_version, $port, $transport, array(), $poller_group, 1, true, $additional);
595
    }
596
    throw $host_unreachable_exception;
597
}
598
599
function deviceArray($host, $community, $snmpver, $port = 161, $transport = 'udp', $v3 = array(), $port_assoc_mode = 'ifIndex')
600
{
601
    $device = array();
602
    $device['hostname'] = $host;
603
    $device['port'] = $port;
604
    $device['transport'] = $transport;
605
606
    /* Get port_assoc_mode id if neccessary
607
     * We can work with names of IDs here */
608
    if (! is_int($port_assoc_mode)) {
609
        $port_assoc_mode = get_port_assoc_mode_id($port_assoc_mode);
610
    }
611
    $device['port_association_mode'] = $port_assoc_mode;
612
613
    $device['snmpver'] = $snmpver;
614
    if ($snmpver === "v2c" or $snmpver === "v1") {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
615
        $device['community'] = $community;
616
    } elseif ($snmpver === "v3") {
617
        $device['authlevel']  = $v3['authlevel'];
618
        $device['authname']   = $v3['authname'];
619
        $device['authpass']   = $v3['authpass'];
620
        $device['authalgo']   = $v3['authalgo'];
621
        $device['cryptopass'] = $v3['cryptopass'];
622
        $device['cryptoalgo'] = $v3['cryptoalgo'];
623
    }
624
625
    return $device;
626
}
627
628
629
function formatUptime($diff, $format = "long")
630
{
631
    return Time::formatInterval($diff, $format);
632
}
633
634
function isSNMPable($device)
635
{
636
    $pos = snmp_check($device);
637
    if ($pos === true) {
638
        return true;
639
    } else {
640
        $pos = snmp_get($device, "sysObjectID.0", "-Oqv", "SNMPv2-MIB");
641
        if ($pos === '' || $pos === false) {
642
            return false;
643
        } else {
644
            return true;
645
        }
646
    }
647
}
648
649
/**
650
 * Check if the given host responds to ICMP echo requests ("pings").
651
 *
652
 * @param string $hostname The hostname or IP address to send ping requests to.
653
 * @param string $address_family The address family ('ipv4' or 'ipv6') to use. Defaults to IPv4.
654
 * Will *not* be autodetected for IP addresses, so it has to be set to 'ipv6' when pinging an IPv6 address or an IPv6-only host.
655
 * @param array $attribs The device attributes
656
 *
657
 * @return array  'result' => bool pingable, 'last_ping_timetaken' => int time for last ping, 'db' => fping results
658
 */
659
function isPingable($hostname, $address_family = 'ipv4', $attribs = [])
660
{
661
    if (can_ping_device($attribs) !== true) {
662
        return [
663
            'result' => true,
664
            'last_ping_timetaken' => 0
665
        ];
666
    }
667
668
    $status = fping(
669
        $hostname,
670
        Config::get('fping_options.count', 3),
671
        Config::get('fping_options.interval', 500),
672
        Config::get('fping_options.timeout', 500),
673
        $address_family
674
    );
675
676
    return [
677
        'result' => ($status['exitcode'] == 0 && $status['loss'] < 100),
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $status['exitcode'] of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
678
        'last_ping_timetaken' => $status['avg'],
679
        'db' => array_intersect_key($status, array_flip(['xmt','rcv','loss','min','max','avg']))
680
    ];
681
}
682
683
function getpollergroup($poller_group = '0')
684
{
685
    //Is poller group an integer
686
    if (is_int($poller_group) || ctype_digit($poller_group)) {
687
        return $poller_group;
688
    } else {
689
        //Check if it contains a comma
690
        if (strpos($poller_group, ',')!== false) {
691
            //If it has a comma use the first element as the poller group
692
            $poller_group_array=explode(',', $poller_group);
693
            return getpollergroup($poller_group_array[0]);
694
        } else {
695
            if (Config::get('distributed_poller_group')) {
696
                //If not use the poller's group from the config
697
                return getpollergroup(Config::get('distributed_poller_group'));
698
            } else {
699
                //If all else fails use default
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
700
                return '0';
701
            }
702
        }
703
    }
704
}
705
706
/**
707
 * Add a host to the database
708
 *
709
 * @param string $host The IP or hostname to add
710
 * @param string $community The snmp community
711
 * @param string $snmpver snmp version: v1 | v2c | v3
712
 * @param int $port SNMP port number
713
 * @param string $transport SNMP transport: udp | udp6 | udp | tcp6
714
 * @param array $v3 SNMPv3 settings required array keys: authlevel, authname, authpass, authalgo, cryptopass, cryptoalgo
715
 * @param int $poller_group distributed poller group to assign this host to
716
 * @param string $port_assoc_mode field to use to identify ports: ifIndex, ifName, ifDescr, ifAlias
717
 * @param bool $force_add Do not detect the host os
718
 * @param array $additional an array with additional parameters to take into consideration when adding devices
719
 * @return int the id of the added host
720
 * @throws HostExistsException Throws this exception if the host already exists
721
 * @throws Exception Throws this exception if insertion into the database fails
722
 */
723
function createHost(
724
    $host,
725
    $community,
726
    $snmpver,
727
    $port = 161,
728
    $transport = 'udp',
729
    $v3 = array(),
730
    $poller_group = 0,
731
    $port_assoc_mode = 'ifIndex',
732
    $force_add = false,
733
    $additional = array()
734
) {
735
    $host = trim(strtolower($host));
736
737
    $poller_group=getpollergroup($poller_group);
738
739
    /* Get port_assoc_mode id if necessary
740
     * We can work with names of IDs here */
741
    if (! is_int($port_assoc_mode)) {
0 ignored issues
show
introduced by
The condition is_int($port_assoc_mode) is always false.
Loading history...
742
        $port_assoc_mode = get_port_assoc_mode_id($port_assoc_mode);
743
    }
744
745
    $device = array(
746
        'hostname' => $host,
747
        'sysName' => $additional['sysName'] ? $additional['sysName'] : $host,
748
        'os' => $additional['os'] ? $additional['os'] : 'generic',
749
        'hardware' => $additional['hardware'] ? $additional['hardware'] : null,
750
        'community' => $community,
751
        'port' => $port,
752
        'transport' => $transport,
753
        'status' => '1',
754
        'snmpver' => $snmpver,
755
        'poller_group' => $poller_group,
756
        'status_reason' => '',
757
        'port_association_mode' => $port_assoc_mode,
758
        'snmp_disable' => $additional['snmp_disable'] ? $additional['snmp_disable'] : 0,
759
    );
760
761
    $device = array_merge($device, $v3);  // merge v3 settings
762
763
    if ($force_add !== true) {
764
        $device['os'] = getHostOS($device);
765
766
        $snmphost = snmp_get($device, "sysName.0", "-Oqv", "SNMPv2-MIB");
767
        if (host_exists($host, $snmphost)) {
0 ignored issues
show
Bug introduced by
It seems like $snmphost can also be of type false; however, parameter $sysName of host_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

767
        if (host_exists($host, /** @scrutinizer ignore-type */ $snmphost)) {
Loading history...
768
            throw new HostExistsException("Already have host $host ($snmphost) due to duplicate sysName");
769
        }
770
    }
771
772
    $device_id = dbInsert($device, 'devices');
773
    if ($device_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $device_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
774
        return $device_id;
775
    }
776
777
    throw new \Exception("Failed to add host to the database, please run ./validate.php");
778
}
779
780
function isDomainResolves($domain)
781
{
782
    if (gethostbyname($domain) != $domain) {
783
        return true;
784
    }
785
786
    $records = dns_get_record($domain);  // returns array or false
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
787
    return !empty($records);
788
}
789
790
function hoststatus($id)
791
{
792
    return dbFetchCell("SELECT `status` FROM `devices` WHERE `device_id` = ?", array($id));
793
}
794
795
function match_network($nets, $ip, $first = false)
796
{
797
    $return = false;
798
    if (!is_array($nets)) {
799
        $nets = array ($nets);
800
    }
801
    foreach ($nets as $net) {
802
        $rev = (preg_match("/^\!/", $net)) ? true : false;
803
        $net = preg_replace("/^\!/", "", $net);
804
        $ip_arr  = explode('/', $net);
805
        $net_long = ip2long($ip_arr[0]);
806
        $x        = ip2long($ip_arr[1]);
807
        $mask    = long2ip($x) == $ip_arr[1] ? $x : 0xffffffff << (32 - $ip_arr[1]);
808
        $ip_long  = ip2long($ip);
809
        if ($rev) {
810
            if (($ip_long & $mask) == ($net_long & $mask)) {
811
                return false;
812
            }
813
        } else {
814
            if (($ip_long & $mask) == ($net_long & $mask)) {
815
                $return = true;
816
            }
817
            if ($first && $return) {
818
                return true;
819
            }
820
        }
821
    }
822
823
    return $return;
824
}
825
826
// FIXME port to LibreNMS\Util\IPv6 class
827
function snmp2ipv6($ipv6_snmp)
828
{
829
    # Workaround stupid Microsoft bug in Windows 2008 -- this is fixed length!
830
    # < fenestro> "because whoever implemented this mib for Microsoft was ignorant of RFC 2578 section 7.7 (2)"
831
    $ipv6 = array_slice(explode('.', $ipv6_snmp), -16);
832
    $ipv6_2 = array();
833
834
    for ($i = 0; $i <= 15; $i++) {
835
        $ipv6[$i] = zeropad(dechex($ipv6[$i]));
836
    }
837
    for ($i = 0; $i <= 15; $i+=2) {
838
        $ipv6_2[] = $ipv6[$i] . $ipv6[$i+1];
839
    }
840
841
    return implode(':', $ipv6_2);
842
}
843
844
function get_astext($asn)
845
{
846
    global $cache;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
847
848
    if (Config::has("astext.$asn")) {
849
        return Config::get("astext.$asn");
850
    }
851
852
    if (isset($cache['astext'][$asn])) {
853
        return $cache['astext'][$asn];
854
    }
855
856
    $result = @dns_get_record("AS$asn.asn.cymru.com", DNS_TXT);
857
    if (!empty($result[0]['txt'])) {
858
        $txt = explode('|', $result[0]['txt']);
859
        $result = trim($txt[4], ' "');
860
        $cache['astext'][$asn] = $result;
861
        return $result;
862
    }
863
864
    return '';
865
}
866
867
/**
868
 * Log events to the event table
869
 *
870
 * @param string $text message describing the event
871
 * @param array|int $device device array or device_id
872
 * @param string $type brief category for this event. Examples: sensor, state, stp, system, temperature, interface
873
 * @param int $severity 1: ok, 2: info, 3: notice, 4: warning, 5: critical, 0: unknown
874
 * @param int $reference the id of the referenced entity.  Supported types: interface
875
 */
876
function log_event($text, $device = null, $type = null, $severity = 2, $reference = null)
877
{
878
    if (!is_array($device)) {
879
        $device = device_by_id_cache($device);
880
    }
881
882
    dbInsert([
883
        'device_id' => ($device['device_id'] ?: 0),
884
        'reference' => $reference,
885
        'type' => $type,
886
        'datetime' => \Carbon\Carbon::now(),
887
        'severity' => $severity,
888
        'message' => $text,
889
        'username'  => isset(LegacyAuth::user()->username) ? LegacyAuth::user()->username : '',
890
    ], 'eventlog');
891
}
892
893
// Parse string with emails. Return array with email (as key) and name (as value)
894
function parse_email($emails)
895
{
896
    $result = array();
897
    $regex = '/^[\"\']?([^\"\']+)[\"\']?\s{0,}<([^@]+@[^>]+)>$/';
898
    if (is_string($emails)) {
899
        $emails = preg_split('/[,;]\s{0,}/', $emails);
900
        foreach ($emails as $email) {
901
            if (preg_match($regex, $email, $out, PREG_OFFSET_CAPTURE)) {
902
                $result[$out[2][0]] = $out[1][0];
903
            } else {
904
                if (strpos($email, "@")) {
905
                    $from_name = Config::get('email_user');
906
                    $result[$email] = $from_name;
907
                }
908
            }
909
        }
910
    } else {
911
        // Return FALSE if input not string
912
        return false;
913
    }
914
    return $result;
915
}
916
917
function send_mail($emails, $subject, $message, $html = false)
918
{
919
    if (is_array($emails) || ($emails = parse_email($emails))) {
920
        d_echo("Attempting to email $subject to: " . implode('; ', array_keys($emails)) . PHP_EOL);
921
        $mail = new PHPMailer(true);
922
        try {
923
            $mail->Hostname = php_uname('n');
924
925
            foreach (parse_email(Config::get('email_from')) as $from => $from_name) {
926
                $mail->setFrom($from, $from_name);
927
            }
928
            foreach ($emails as $email => $email_name) {
929
                $mail->addAddress($email, $email_name);
930
            }
931
            $mail->Subject = $subject;
932
            $mail->XMailer = Config::get('project_name_version');
933
            $mail->CharSet = 'utf-8';
934
            $mail->WordWrap = 76;
935
            $mail->Body = $message;
936
            if ($html) {
937
                $mail->isHTML(true);
938
            }
939
            switch (strtolower(trim(Config::get('email_backend')))) {
940
                case 'sendmail':
941
                    $mail->Mailer = 'sendmail';
942
                    $mail->Sendmail = Config::get('email_sendmail_path');
943
                    break;
944
                case 'smtp':
945
                    $mail->isSMTP();
946
                    $mail->Host = Config::get('email_smtp_host');
947
                    $mail->Timeout = Config::get('email_smtp_timeout');
948
                    $mail->SMTPAuth = Config::get('email_smtp_auth');
949
                    $mail->SMTPSecure = Config::get('email_smtp_secure');
950
                    $mail->Port = Config::get('email_smtp_port');
951
                    $mail->Username = Config::get('email_smtp_username');
952
                    $mail->Password = Config::get('email_smtp_password');
953
                    $mail->SMTPAutoTLS = Config::get('email_auto_tls');
954
                    $mail->SMTPDebug  = false;
0 ignored issues
show
Documentation Bug introduced by
The property $SMTPDebug was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
955
                    break;
956
                default:
957
                    $mail->Mailer = 'mail';
958
                    break;
959
            }
960
            $mail->send();
961
            return true;
962
        } catch (\PHPMailer\PHPMailer\Exception $e) {
963
            return $e->errorMessage();
964
        } catch (Exception $e) {
965
            return $e->getMessage();
966
        }
967
    }
968
969
    return "No contacts found";
970
}
971
972
function formatCiscoHardware(&$device, $short = false)
973
{
974
    return \LibreNMS\Util\Rewrite::ciscoHardware($device, $short);
975
}
976
977
function hex2str($hex)
978
{
979
    $string='';
980
981
    for ($i = 0; $i < strlen($hex)-1; $i+=2) {
982
        $string .= chr(hexdec(substr($hex, $i, 2)));
0 ignored issues
show
Bug introduced by
It seems like hexdec(substr($hex, $i, 2)) can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

982
        $string .= chr(/** @scrutinizer ignore-type */ hexdec(substr($hex, $i, 2)));
Loading history...
983
    }
984
985
    return $string;
986
}
987
988
# Convert an SNMP hex string to regular string
989
function snmp_hexstring($hex)
990
{
991
    return hex2str(str_replace(' ', '', str_replace(' 00', '', $hex)));
992
}
993
994
# Check if the supplied string is an SNMP hex string
995
function isHexString($str)
996
{
997
    return (bool)preg_match("/^[a-f0-9][a-f0-9]( [a-f0-9][a-f0-9])*$/is", trim($str));
998
}
999
1000
# Include all .inc.php files in $dir
1001
function include_dir($dir, $regex = "")
1002
{
1003
    global $device, $valid;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1004
1005
    if ($regex == "") {
1006
        $regex = "/\.inc\.php$/";
1007
    }
1008
1009
    if ($handle = opendir(Config::get('install_dir') . '/' . $dir)) {
1010
        while (false !== ($file = readdir($handle))) {
1011
            if (filetype(Config::get('install_dir') . '/' . $dir . '/' . $file) == 'file' && preg_match($regex, $file)) {
1012
                d_echo("Including: " . Config::get('install_dir') . '/' . $dir . '/' . $file . "\n");
1013
1014
                include(Config::get('install_dir') . '/' . $dir . '/' . $file);
1015
            }
1016
        }
1017
1018
        closedir($handle);
1019
    }
1020
}
1021
1022
/**
1023
 * Check if port is valid to poll.
1024
 * Settings: empty_ifdescr, good_if, bad_if, bad_if_regexp, bad_ifname_regexp, bad_ifalias_regexp, bad_iftype
1025
 *
1026
 * @param array $port
1027
 * @param array $device
1028
 * @return bool
1029
 */
1030
function is_port_valid($port, $device)
1031
{
1032
    // check empty values first
1033
    if (empty($port['ifDescr'])) {
1034
        // If these are all empty, we are just going to show blank names in the ui
1035
        if (empty($port['ifAlias']) && empty($port['ifName'])) {
1036
            d_echo("ignored: empty ifDescr, ifAlias and ifName\n");
1037
            return false;
1038
        }
1039
1040
        // ifDescr should not be empty unless it is explicitly allowed
1041
        if (!Config::getOsSetting($device['os'], 'empty_ifdescr', false)) {
1042
            d_echo("ignored: empty ifDescr\n");
1043
            return false;
1044
        }
1045
    }
1046
1047
    $ifDescr = $port['ifDescr'];
1048
    $ifName  = $port['ifName'];
1049
    $ifAlias = $port['ifAlias'];
1050
    $ifType  = $port['ifType'];
1051
1052
    if (str_i_contains($ifDescr, Config::getOsSetting($device['os'], 'good_if'))) {
1053
        return true;
1054
    }
1055
1056
    foreach (Config::getCombined($device['os'], 'bad_if') as $bi) {
1057
        if (str_i_contains($ifDescr, $bi)) {
1058
            d_echo("ignored by ifDescr: $ifDescr (matched: $bi)\n");
1059
            return false;
1060
        }
1061
    }
1062
1063
    foreach (Config::getCombined($device['os'], 'bad_if_regexp') as $bir) {
1064
        if (preg_match($bir ."i", $ifDescr)) {
1065
            d_echo("ignored by ifDescr: $ifDescr (matched: $bir)\n");
1066
            return false;
1067
        }
1068
    }
1069
1070
    foreach (Config::getCombined($device['os'], 'bad_ifname_regexp') as $bnr) {
1071
        if (preg_match($bnr ."i", $ifName)) {
1072
            d_echo("ignored by ifName: $ifName (matched: $bnr)\n");
1073
            return false;
1074
        }
1075
    }
1076
1077
1078
    foreach (Config::getCombined($device['os'], 'bad_ifalias_regexp') as $bar) {
1079
        if (preg_match($bar ."i", $ifAlias)) {
1080
            d_echo("ignored by ifName: $ifAlias (matched: $bar)\n");
1081
            return false;
1082
        }
1083
    }
1084
1085
    foreach (Config::getCombined($device['os'], 'bad_iftype') as $bt) {
1086
        if (str_contains($ifType, $bt)) {
1087
            d_echo("ignored by ifType: $ifType (matched: $bt )\n");
1088
            return false;
1089
        }
1090
    }
1091
1092
    return true;
1093
}
1094
1095
/**
1096
 * Try to fill in data for ifDescr, ifName, and ifAlias if devices do not provide them.
1097
 * Will not fill ifAlias if the user has overridden it
1098
 *
1099
 * @param array $port
1100
 * @param array $device
1101
 */
1102
function port_fill_missing(&$port, $device)
1103
{
1104
    // When devices do not provide data, populate with other data if available
1105
    if ($port['ifDescr'] == '' || $port['ifDescr'] == null) {
1106
        $port['ifDescr'] = $port['ifName'];
1107
        d_echo(' Using ifName as ifDescr');
1108
    }
1109
    if (!empty($device['attribs']['ifName:' . $port['ifName']])) {
1110
        // ifAlias overridden by user, don't update it
1111
        unset($port['ifAlias']);
1112
        d_echo(' ifAlias overriden by user');
1113
    } elseif ($port['ifAlias'] == '' || $port['ifAlias'] == null) {
1114
        $port['ifAlias'] = $port['ifDescr'];
1115
        d_echo(' Using ifDescr as ifAlias');
1116
    }
1117
1118
    if ($port['ifName'] == '' || $port['ifName'] == null) {
1119
        $port['ifName'] = $port['ifDescr'];
1120
        d_echo(' Using ifDescr as ifName');
1121
    }
1122
}
1123
1124
function scan_new_plugins()
1125
{
1126
    $installed = 0; // Track how many plugins we install.
1127
1128
    if (file_exists(Config::get('plugin_dir'))) {
1129
        $plugin_files = scandir(Config::get('plugin_dir'));
1130
        foreach ($plugin_files as $name) {
1131
            if (is_dir(Config::get('plugin_dir') . '/' . $name)) {
1132
                if ($name != '.' && $name != '..') {
1133
                    if (is_file(Config::get('plugin_dir') . '/' . $name . '/' . $name . '.php') && is_file(Config::get('plugin_dir') . '/' . $name . '/' . $name . '.inc.php')) {
1134
                        $plugin_id = dbFetchRow("SELECT `plugin_id` FROM `plugins` WHERE `plugin_name` = '$name'");
1135
                        if (empty($plugin_id)) {
1136
                            if (dbInsert(array('plugin_name' => $name, 'plugin_active' => '0'), 'plugins')) {
1137
                                $installed++;
1138
                            }
1139
                        }
1140
                    }
1141
                }
1142
            }
1143
        }
1144
    }
1145
1146
    return( $installed );
1147
}
1148
1149
function validate_device_id($id)
1150
{
1151
    if (empty($id) || !is_numeric($id)) {
1152
        $return = false;
1153
    } else {
1154
        $device_id = dbFetchCell("SELECT `device_id` FROM `devices` WHERE `device_id` = ?", array($id));
1155
        if ($device_id == $id) {
1156
            $return = true;
1157
        } else {
1158
            $return = false;
1159
        }
1160
    }
1161
    return($return);
1162
}
1163
1164
// The original source of this code is from Stackoverflow (www.stackoverflow.com).
1165
// http://stackoverflow.com/questions/6054033/pretty-printing-json-with-php
1166
// Answer provided by stewe (http://stackoverflow.com/users/3202187/ulk200
1167
if (!defined('JSON_UNESCAPED_SLASHES')) {
1168
    define('JSON_UNESCAPED_SLASHES', 64);
1169
}
1170
if (!defined('JSON_PRETTY_PRINT')) {
1171
    define('JSON_PRETTY_PRINT', 128);
1172
}
1173
if (!defined('JSON_UNESCAPED_UNICODE')) {
1174
    define('JSON_UNESCAPED_UNICODE', 256);
1175
}
1176
1177
function _json_encode($data, $options = 448)
1178
{
1179
    if (version_compare(PHP_VERSION, '5.4', '>=')) {
1180
        return json_encode($data, $options);
1181
    } else {
1182
        return _json_format(json_encode($data), $options);
1183
    }
1184
}
1185
1186
function _json_format($json, $options = 448)
1187
{
1188
    $prettyPrint = (bool) ($options & JSON_PRETTY_PRINT);
1189
    $unescapeUnicode = (bool) ($options & JSON_UNESCAPED_UNICODE);
1190
    $unescapeSlashes = (bool) ($options & JSON_UNESCAPED_SLASHES);
1191
1192
    if (!$prettyPrint && !$unescapeUnicode && !$unescapeSlashes) {
1193
        return $json;
1194
    }
1195
1196
    $result = '';
1197
    $pos = 0;
1198
    $strLen = strlen($json);
1199
    $indentStr = ' ';
1200
    $newLine = "\n";
1201
    $outOfQuotes = true;
1202
    $buffer = '';
1203
    $noescape = true;
1204
1205
    for ($i = 0; $i < $strLen; $i++) {
1206
        // Grab the next character in the string
1207
        $char = substr($json, $i, 1);
1208
1209
        // Are we inside a quoted string?
1210
        if ('"' === $char && $noescape) {
1211
            $outOfQuotes = !$outOfQuotes;
0 ignored issues
show
introduced by
The condition $outOfQuotes is always true.
Loading history...
1212
        }
1213
1214
        if (!$outOfQuotes) {
1215
            $buffer .= $char;
1216
            $noescape = '\\' === $char ? !$noescape : true;
1217
            continue;
1218
        } elseif ('' !== $buffer) {
1219
            if ($unescapeSlashes) {
1220
                $buffer = str_replace('\\/', '/', $buffer);
1221
            }
1222
1223
            if ($unescapeUnicode && function_exists('mb_convert_encoding')) {
1224
                // http://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha
1225
                $buffer = preg_replace_callback(
1226
                    '/\\\\u([0-9a-f]{4})/i',
1227
                    function ($match) {
1228
                        return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
1229
                    },
1230
                    $buffer
1231
                );
1232
            }
1233
1234
            $result .= $buffer . $char;
1235
            $buffer = '';
1236
            continue;
1237
        } elseif (false !== strpos(" \t\r\n", $char)) {
1238
            continue;
1239
        }
1240
1241
        if (':' === $char) {
1242
            // Add a space after the : character
1243
            $char .= ' ';
1244
        } elseif (('}' === $char || ']' === $char)) {
1245
            $pos--;
1246
            $prevChar = substr($json, $i - 1, 1);
1247
1248
            if ('{' !== $prevChar && '[' !== $prevChar) {
1249
                // If this character is the end of an element,
1250
                // output a new line and indent the next line
1251
                $result .= $newLine;
1252
                for ($j = 0; $j < $pos; $j++) {
1253
                    $result .= $indentStr;
1254
                }
1255
            } else {
1256
                // Collapse empty {} and []
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1257
                $result = rtrim($result) . "\n\n" . $indentStr;
1258
            }
1259
        }
1260
1261
        $result .= $char;
1262
1263
        // If the last character was the beginning of an element,
1264
        // output a new line and indent the next line
1265
        if (',' === $char || '{' === $char || '[' === $char) {
1266
            $result .= $newLine;
1267
1268
            if ('{' === $char || '[' === $char) {
1269
                $pos++;
1270
            }
1271
1272
            for ($j = 0; $j < $pos; $j++) {
1273
                $result .= $indentStr;
1274
            }
1275
        }
1276
    }
1277
    // If buffer not empty after formating we have an unclosed quote
1278
    if (strlen($buffer) > 0) {
1279
        //json is incorrectly formatted
1280
        $result = false;
1281
    }
1282
1283
    return $result;
1284
}
1285
1286
function convert_delay($delay)
1287
{
1288
    $delay = preg_replace('/\s/', '', $delay);
1289
    if (strstr($delay, 'm', true)) {
1290
        $delay_sec = $delay * 60;
1291
    } elseif (strstr($delay, 'h', true)) {
1292
        $delay_sec = $delay * 3600;
1293
    } elseif (strstr($delay, 'd', true)) {
1294
        $delay_sec = $delay * 86400;
1295
    } elseif (is_numeric($delay)) {
1296
        $delay_sec = $delay;
1297
    } else {
1298
        $delay_sec = 300;
1299
    }
1300
    return($delay_sec);
1301
}
1302
1303
function guidv4($data)
1304
{
1305
    // http://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid#15875555
1306
    // From: Jack http://stackoverflow.com/users/1338292/ja%CD%A2ck
1307
    assert(strlen($data) == 16);
1308
1309
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
1310
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
1311
1312
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
1313
}
1314
1315
/**
1316
 * @param $curl
1317
 */
1318
function set_curl_proxy($curl)
1319
{
1320
    $proxy = get_proxy();
1321
1322
    $tmp = rtrim($proxy, "/");
0 ignored issues
show
Bug introduced by
It seems like $proxy can also be of type array; however, parameter $str of rtrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1322
    $tmp = rtrim(/** @scrutinizer ignore-type */ $proxy, "/");
Loading history...
1323
    $proxy = str_replace(array("http://", "https://"), "", $tmp);
1324
    if (!empty($proxy)) {
1325
        curl_setopt($curl, CURLOPT_PROXY, $proxy);
1326
    }
1327
}
1328
1329
/**
1330
 * Return the proxy url
1331
 *
1332
 * @return array|bool|false|string
1333
 */
1334
function get_proxy()
1335
{
1336
    if (getenv('http_proxy')) {
1337
        return getenv('http_proxy');
1338
    } elseif (getenv('https_proxy')) {
1339
        return getenv('https_proxy');
1340
    } elseif ($callback_proxy = Config::get('callback_proxy')) {
1341
        return $callback_proxy;
1342
    } elseif ($http_proxy = Config::get('http_proxy')) {
1343
        return $http_proxy;
1344
    }
1345
    return false;
1346
}
1347
1348
function target_to_id($target)
1349
{
1350
    if ($target[0].$target[1] == "g:") {
1351
        $target = "g".dbFetchCell('SELECT id FROM device_groups WHERE name = ?', array(substr($target, 2)));
1352
    } else {
1353
        $target = dbFetchCell('SELECT device_id FROM devices WHERE hostname = ?', array($target));
1354
    }
1355
    return $target;
1356
}
1357
1358
function id_to_target($id)
1359
{
1360
    if ($id[0] == "g") {
1361
        $id = 'g:'.dbFetchCell("SELECT name FROM device_groups WHERE id = ?", array(substr($id, 1)));
1362
    } else {
1363
        $id = dbFetchCell("SELECT hostname FROM devices WHERE device_id = ?", array($id));
1364
    }
1365
    return $id;
1366
}
1367
1368
function first_oid_match($device, $list)
1369
{
1370
    foreach ($list as $item) {
1371
        $tmp = trim(snmp_get($device, $item, "-Ovq"), '" ');
0 ignored issues
show
Bug introduced by
It seems like snmp_get($device, $item, '-Ovq') can also be of type false; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1371
        $tmp = trim(/** @scrutinizer ignore-type */ snmp_get($device, $item, "-Ovq"), '" ');
Loading history...
1372
        if (!empty($tmp)) {
1373
            return $tmp;
1374
        }
1375
    }
1376
}
1377
1378
1379
function fix_integer_value($value)
1380
{
1381
    if ($value < 0) {
1382
        $return = 4294967296+$value;
1383
    } else {
1384
        $return = $value;
1385
    }
1386
    return $return;
1387
}
1388
1389
/**
1390
 * Find a device that has this IP. Checks ipv4_addresses and ipv6_addresses tables.
1391
 *
1392
 * @param string $ip
1393
 * @return \App\Models\Device|false
1394
 */
1395
function device_has_ip($ip)
1396
{
1397
    if (IPv6::isValid($ip)) {
1398
        $ip_address = \App\Models\Ipv6Address::query()
1399
            ->where('ipv6_address', IPv6::parse($ip, true)->uncompressed())
1400
            ->with('port.device')
1401
            ->first();
1402
    } elseif (IPv4::isValid($ip)) {
1403
        $ip_address = \App\Models\Ipv4Address::query()
1404
            ->where('ipv4_address', $ip)
1405
            ->with('port.device')
1406
            ->first();
1407
    }
1408
1409
    if (isset($ip_address) && $ip_address->port) {
1410
        return $ip_address->port->device;
1411
    }
1412
1413
    return false; // not an ipv4 or ipv6 address...
1414
}
1415
1416
/**
1417
 * Run fping against a hostname/ip in count mode and collect stats.
1418
 *
1419
 * @param string $host
1420
 * @param int $count (min 1)
1421
 * @param int $interval (min 20)
1422
 * @param int $timeout (not more than $interval)
1423
 * @param string $address_family ipv4 or ipv6
1424
 * @return array
1425
 */
1426
function fping($host, $count = 3, $interval = 1000, $timeout = 500, $address_family = 'ipv4')
1427
{
1428
    // Default to ipv4
1429
    $fping_name = $address_family == 'ipv6' ? 'fping6' : 'fping';
1430
    $fping_path = Config::get($fping_name, $fping_name);
1431
1432
    // build the parameters
1433
    $params = '-e -q -c ' . max($count, 1);
1434
1435
    $interval = max($interval, 20);
1436
    $params .= ' -p ' . $interval;
1437
1438
    $params .= ' -t ' . max($timeout, $interval);
1439
1440
    $cmd = "$fping_path $params $host";
1441
1442
    d_echo("[FPING] $cmd\n");
1443
1444
    $process = new Process($cmd);
0 ignored issues
show
Bug introduced by
$cmd of type string is incompatible with the type array expected by parameter $command of Symfony\Component\Process\Process::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1444
    $process = new Process(/** @scrutinizer ignore-type */ $cmd);
Loading history...
1445
    $process->run();
1446
    $output = $process->getErrorOutput();
1447
1448
    preg_match('#= (\d+)/(\d+)/(\d+)%, min/avg/max = ([\d.]+)/([\d.]+)/([\d.]+)$#', $output, $parsed);
1449
    list(, $xmt, $rcv, $loss, $min, $avg, $max) = $parsed;
1450
1451
    if ($loss < 0) {
1452
        $xmt = 1;
1453
        $rcv = 1;
1454
        $loss = 100;
1455
    }
1456
1457
    $response = [
1458
        'xmt'  => set_numeric($xmt),
1459
        'rcv'  => set_numeric($rcv),
1460
        'loss' => set_numeric($loss),
1461
        'min'  => set_numeric($min),
1462
        'max'  => set_numeric($max),
1463
        'avg'  => set_numeric($avg),
1464
        'exitcode' => $process->getExitCode(),
1465
    ];
1466
    d_echo($response);
1467
1468
    return $response;
1469
}
1470
1471
function function_check($function)
1472
{
1473
    return function_exists($function);
1474
}
1475
1476
function force_influx_data($data)
1477
{
1478
   /*
1479
    * It is not trivial to detect if something is a float or an integer, and
1480
    * therefore may cause breakages on inserts.
1481
    * Just setting every number to a float gets around this, but may introduce
1482
    * inefficiencies.
1483
    * I've left the detection statement in there for a possible change in future,
1484
    * but currently everything just gets set to a float.
1485
    */
1486
1487
    if (is_numeric($data)) {
1488
        // If it is an Integer
1489
        if (ctype_digit($data)) {
1490
            return floatval($data);
1491
        // Else it is a float
1492
        } else {
1493
            return floatval($data);
1494
        }
1495
    } else {
1496
        return $data;
1497
    }
1498
}// end force_influx_data
1499
1500
/**
1501
 * Try to determine the address family (IPv4 or IPv6) associated with an SNMP
1502
 * transport specifier (like "udp", "udp6", etc.).
1503
 *
1504
 * @param string $transport The SNMP transport specifier, for example "udp",
1505
 *                          "udp6", "tcp", or "tcp6". See `man snmpcmd`,
1506
 *                          section "Agent Specification" for a full list.
1507
 *
1508
 * @return string The address family associated with the given transport
1509
 *             specifier: 'ipv4' (or local connections not associated
1510
 *             with an IP stack) or 'ipv6'.
1511
 */
1512
function snmpTransportToAddressFamily($transport)
1513
{
1514
    $ipv6_snmp_transport_specifiers = ['udp6', 'udpv6', 'udpipv6', 'tcp6', 'tcpv6', 'tcpipv6'];
1515
1516
    if (in_array($transport, $ipv6_snmp_transport_specifiers)) {
1517
        return 'ipv6';
1518
    }
1519
1520
    return 'ipv4';
1521
}
1522
1523
/**
1524
 * Checks if the $hostname provided exists in the DB already
1525
 *
1526
 * @param string $hostname The hostname to check for
1527
 * @param string $sysName The sysName to check
1528
 * @return bool true if hostname already exists
1529
 *              false if hostname doesn't exist
1530
 */
1531
function host_exists($hostname, $sysName = null)
1532
{
1533
    $query = "SELECT COUNT(*) FROM `devices` WHERE `hostname`=?";
1534
    $params = array($hostname);
1535
1536
    if (!empty($sysName) && !Config::get('allow_duplicate_sysName')) {
1537
        $query .= " OR `sysName`=?";
1538
        $params[] = $sysName;
1539
1540
        if (!empty(Config::get('mydomain'))) {
1541
            $full_sysname = rtrim($sysName, '.') . '.' . Config::get('mydomain');
1542
            $query .= " OR `sysName`=?";
1543
            $params[] = $full_sysname;
1544
        }
1545
    }
1546
    return dbFetchCell($query, $params) > 0;
1547
}
1548
1549
function oxidized_reload_nodes()
1550
{
1551
    if (Config::get('oxidized.enabled') === true && Config::get('oxidized.reload_nodes') === true && Config::has('oxidized.url')) {
1552
        $oxidized_reload_url = Config::get('oxidized.url') . '/reload.json';
1553
        $ch = curl_init($oxidized_reload_url);
1554
1555
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1555
        curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_TIMEOUT, 5);
Loading history...
1556
        curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000);
1557
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
1558
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1559
        curl_setopt($ch, CURLOPT_HEADER, 1);
1560
        curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1560
        curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
1561
        curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1561
        curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
1562
    }
1563
}
1564
1565
/**
1566
 * Perform DNS lookup
1567
 *
1568
 * @param array $device Device array from database
1569
 * @param string $type The type of record to lookup
1570
 *
1571
 * @return string ip
1572
 *
1573
**/
1574
function dnslookup($device, $type = false, $return = false)
1575
{
1576
    if (filter_var($device['hostname'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) == true || filter_var($device['hostname'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) == true) {
1577
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
1578
    }
1579
    if (empty($type)) {
1580
        // We are going to use the transport to work out the record type
1581
        if ($device['transport'] == 'udp6' || $device['transport'] == 'tcp6') {
1582
            $type = DNS_AAAA;
1583
            $return = 'ipv6';
1584
        } else {
1585
            $type = DNS_A;
1586
            $return = 'ip';
1587
        }
1588
    }
1589
    if (empty($return)) {
1590
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
1591
    }
1592
    $record = dns_get_record($device['hostname'], $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type string; however, parameter $type of dns_get_record() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1592
    $record = dns_get_record($device['hostname'], /** @scrutinizer ignore-type */ $type);
Loading history...
1593
    return $record[0][$return];
1594
}//end dnslookup
1595
1596
1597
1598
1599
/**
1600
 * Run rrdtool info on a file path
1601
 *
1602
 * @param string $path Path to pass to rrdtool info
1603
 * @param string $stdOutput Variable to recieve the output of STDOUT
1604
 * @param string $stdError Variable to recieve the output of STDERR
1605
 *
1606
 * @return int exit code
1607
 *
1608
**/
1609
1610
function rrdtest($path, &$stdOutput, &$stdError)
1611
{
1612
    //rrdtool info <escaped rrd path>
1613
    $command = Config::get('rrdtool') . ' info ' . escapeshellarg($path);
1614
    $process = proc_open(
1615
        $command,
1616
        array (
1617
            0 => array('pipe', 'r'),
1618
            1 => array('pipe', 'w'),
1619
            2 => array('pipe', 'w'),
1620
        ),
1621
        $pipes
1622
    );
1623
1624
    if (!is_resource($process)) {
1625
        throw new \RuntimeException('Could not create a valid process');
1626
    }
1627
1628
    $status = proc_get_status($process);
1629
    while ($status['running']) {
1630
        usleep(2000); // Sleep 2000 microseconds or 2 milliseconds
1631
        $status = proc_get_status($process);
1632
    }
1633
1634
    $stdOutput = stream_get_contents($pipes[1]);
1635
    $stdError  = stream_get_contents($pipes[2]);
1636
    proc_close($process);
1637
    return $status['exitcode'];
1638
}
1639
1640
/**
1641
 * Create a new state index.  Update translations if $states is given.
1642
 *
1643
 * For for backward compatibility:
1644
 *   Returns null if $states is empty, $state_name already exists, and contains state translations
1645
 *
1646
 * @param string $state_name the unique name for this state translation
1647
 * @param array $states array of states, each must contain keys: descr, graph, value, generic
1648
 * @return int|null
1649
 */
1650
function create_state_index($state_name, $states = array())
1651
{
1652
    $state_index_id = dbFetchCell('SELECT `state_index_id` FROM state_indexes WHERE state_name = ? LIMIT 1', array($state_name));
1653
    if (!is_numeric($state_index_id)) {
1654
        $state_index_id = dbInsert(array('state_name' => $state_name), 'state_indexes');
1655
1656
        // legacy code, return index so states are created
1657
        if (empty($states)) {
1658
            return $state_index_id;
1659
        }
1660
    }
1661
1662
    // check or synchronize states
1663
    if (empty($states)) {
1664
        $translations = dbFetchRows('SELECT * FROM `state_translations` WHERE `state_index_id` = ?', array($state_index_id));
1665
        if (count($translations) == 0) {
1666
            // If we don't have any translations something has gone wrong so return the state_index_id so they get created.
1667
            return $state_index_id;
1668
        }
1669
    } else {
1670
        sync_sensor_states($state_index_id, $states);
1671
    }
1672
1673
    return null;
1674
}
1675
1676
/**
1677
 * Synchronize the sensor state translations with the database
1678
 *
1679
 * @param int $state_index_id index of the state
1680
 * @param array $states array of states, each must contain keys: descr, graph, value, generic
1681
 */
1682
function sync_sensor_states($state_index_id, $states)
1683
{
1684
    $new_translations = array_reduce($states, function ($array, $state) use ($state_index_id) {
1685
        $array[$state['value']] = array(
1686
            'state_index_id' => $state_index_id,
1687
            'state_descr' => $state['descr'],
1688
            'state_draw_graph' => $state['graph'],
1689
            'state_value' => $state['value'],
1690
            'state_generic_value' => $state['generic']
1691
        );
1692
        return $array;
1693
    }, array());
1694
1695
    $existing_translations = dbFetchRows(
1696
        'SELECT `state_index_id`,`state_descr`,`state_draw_graph`,`state_value`,`state_generic_value` FROM `state_translations` WHERE `state_index_id`=?',
1697
        array($state_index_id)
1698
    );
1699
1700
    foreach ($existing_translations as $translation) {
1701
        $value = $translation['state_value'];
1702
        if (isset($new_translations[$value])) {
1703
            if ($new_translations[$value] != $translation) {
1704
                dbUpdate(
1705
                    $new_translations[$value],
1706
                    'state_translations',
1707
                    '`state_index_id`=? AND `state_value`=?',
1708
                    array($state_index_id, $value)
1709
                );
1710
            }
1711
1712
            // this translation is synchronized, it doesn't need to be inserted
1713
            unset($new_translations[$value]);
1714
        } else {
1715
            dbDelete('state_translations', '`state_index_id`=? AND `state_value`=?', array($state_index_id, $value));
1716
        }
1717
    }
1718
1719
    // insert any new translations
1720
    dbBulkInsert($new_translations, 'state_translations');
1721
}
1722
1723
function create_sensor_to_state_index($device, $state_name, $index)
1724
{
1725
    $sensor_entry = dbFetchRow('SELECT sensor_id FROM `sensors` WHERE `sensor_class` = ? AND `device_id` = ? AND `sensor_type` = ? AND `sensor_index` = ?', array(
1726
        'state',
1727
        $device['device_id'],
1728
        $state_name,
1729
        $index
1730
    ));
1731
    $state_indexes_entry = dbFetchRow('SELECT state_index_id FROM `state_indexes` WHERE `state_name` = ?', array(
1732
        $state_name
1733
    ));
1734
    if (!empty($sensor_entry['sensor_id']) && !empty($state_indexes_entry['state_index_id'])) {
1735
        $insert = array(
1736
            'sensor_id' => $sensor_entry['sensor_id'],
1737
            'state_index_id' => $state_indexes_entry['state_index_id'],
1738
        );
1739
        foreach ($insert as $key => $val_check) {
1740
            if (!isset($val_check)) {
1741
                unset($insert[$key]);
1742
            }
1743
        }
1744
1745
        dbInsert($insert, 'sensors_to_state_indexes');
1746
    }
1747
}
1748
1749
function delta_to_bits($delta, $period)
1750
{
1751
    return round(($delta * 8 / $period), 2);
1752
}
1753
1754
function report_this($message)
1755
{
1756
    return '<h2>' . $message . ' Please <a href="' . Config::get('project_issues') . '">report this</a> to the ' . Config::get('project_name') . ' developers.</h2>';
1757
}//end report_this()
1758
1759
function hytera_h2f($number, $nd)
1760
{
1761
    if (strlen(str_replace(" ", "", $number)) == 4) {
1762
        $hex = '';
1763
        for ($i = 0; $i < strlen($number); $i++) {
1764
            $byte = strtoupper(dechex(ord($number{$i})));
1765
            $byte = str_repeat('0', 2 - strlen($byte)).$byte;
1766
            $hex.=$byte." ";
1767
        }
1768
        $number = $hex;
1769
        unset($hex);
1770
    }
1771
    $r = '';
1772
    $y = explode(' ', $number);
1773
    foreach ($y as $z) {
1774
        $r = $z . '' . $r;
1775
    }
1776
1777
    $hex = array();
1778
    $number = substr($r, 0, -1);
1779
    //$number = str_replace(" ", "", $number);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1780
    for ($i=0; $i<strlen($number); $i++) {
1781
        $hex[]=substr($number, $i, 1);
1782
    }
1783
1784
    $dec = array();
1785
    $hexCount = count($hex);
1786
    for ($i=0; $i<$hexCount; $i++) {
1787
        $dec[]=hexdec($hex[$i]);
1788
    }
1789
1790
    $binfinal = "";
1791
    $decCount = count($dec);
1792
    for ($i=0; $i<$decCount; $i++) {
1793
        $binfinal.=sprintf("%04d", decbin($dec[$i]));
1794
    }
1795
1796
    $sign=substr($binfinal, 0, 1);
1797
    $exp=substr($binfinal, 1, 8);
1798
    $exp=bindec($exp);
1799
    $exp-=127;
1800
    $scibin=substr($binfinal, 9);
1801
    $binint=substr($scibin, 0, $exp);
0 ignored issues
show
Bug introduced by
It seems like $exp can also be of type double; however, parameter $length of substr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1801
    $binint=substr($scibin, 0, /** @scrutinizer ignore-type */ $exp);
Loading history...
1802
    $binpoint=substr($scibin, $exp);
0 ignored issues
show
Bug introduced by
It seems like $exp can also be of type double; however, parameter $start of substr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1802
    $binpoint=substr($scibin, /** @scrutinizer ignore-type */ $exp);
Loading history...
1803
    $intnumber=bindec("1".$binint);
1804
1805
    $tmppoint = [];
1806
    for ($i=0; $i<strlen($binpoint); $i++) {
1807
        $tmppoint[]=substr($binpoint, $i, 1);
1808
    }
1809
1810
    $tmppoint=array_reverse($tmppoint);
1811
    $tpointnumber=number_format($tmppoint[0]/2, strlen($binpoint), '.', '');
1812
1813
    $pointnumber = "";
1814
    for ($i=1; $i<strlen($binpoint); $i++) {
1815
        $pointnumber=number_format($tpointnumber/2, strlen($binpoint), '.', '');
1816
        $tpointnumber=$tmppoint[$i+1].substr($pointnumber, 1);
1817
    }
1818
1819
    $floatfinal=$intnumber+$pointnumber;
1820
1821
    if ($sign==1) {
1822
        $floatfinal=-$floatfinal;
1823
    }
1824
1825
    return number_format($floatfinal, $nd, '.', '');
1826
}
1827
1828
/*
1829
 * Cisco CIMC functions
1830
 */
1831
// Create an entry in the entPhysical table if it doesnt already exist.
1832
function setCIMCentPhysical($location, $data, &$entphysical, &$index)
1833
{
1834
    // Go get the location, this will create it if it doesnt exist.
1835
    $entPhysicalIndex = getCIMCentPhysical($location, $entphysical, $index);
1836
1837
    // See if we need to update
1838
    $update = array();
1839
    foreach ($data as $key => $value) {
1840
        // Is the Array(DB) value different to the supplied data
1841
        if ($entphysical[$location][$key] != $value) {
1842
            $update[$key] = $value;
1843
            $entphysical[$location][$key] = $value;
1844
        } // End if
1845
    } // end foreach
1846
1847
    // Do we need to update
1848
    if (count($update) > 0) {
1849
        dbUpdate($update, 'entPhysical', '`entPhysical_id` = ?', array($entphysical[$location]['entPhysical_id']));
1850
    }
1851
    $entPhysicalId = $entphysical[$location]['entPhysical_id'];
1852
    return array($entPhysicalId, $entPhysicalIndex);
1853
}
1854
1855
function getCIMCentPhysical($location, &$entphysical, &$index)
1856
{
1857
    global $device;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1858
1859
    // Level 1 - Does the location exist
1860
    if (isset($entphysical[$location])) {
1861
        // Yes, return the entPhysicalIndex.
1862
        return $entphysical[$location]['entPhysicalIndex'];
1863
    } else {
1864
        /*
1865
         * No, the entry doesnt exist.
1866
         * Find its parent so we can create it.
1867
         */
1868
1869
        // Pull apart the location
1870
        $parts = explode('/', $location);
1871
1872
        // Level 2 - Are we at the root
1873
        if (count($parts) == 1) {
1874
            // Level 2 - Yes. We are the root, there is no parent
1875
            d_echo("ROOT - ".$location."\n");
1876
            $shortlocation = $location;
1877
            $parent = 0;
1878
        } else {
1879
            // Level 2 - No. Need to go deeper.
1880
            d_echo("NON-ROOT - ".$location."\n");
1881
            $shortlocation = array_pop($parts);
1882
            $parentlocation = implode('/', $parts);
1883
            d_echo("Decend - parent location: ".$parentlocation."\n");
1884
            $parent = getCIMCentPhysical($parentlocation, $entphysical, $index);
1885
        } // end if - Level 2
1886
        d_echo("Parent: ".$parent."\n");
1887
1888
        // Now we have an ID, create the entry.
1889
        $index++;
1890
        $insert = array(
1891
            'device_id'                 => $device['device_id'],
1892
            'entPhysicalIndex'          => $index,
1893
            'entPhysicalClass'          => 'container',
1894
            'entPhysicalVendorType'     => $location,
1895
            'entPhysicalName'           => $shortlocation,
1896
            'entPhysicalContainedIn'    => $parent,
1897
            'entPhysicalParentRelPos'   => '-1',
1898
        );
1899
1900
        // Add to the DB and Array.
1901
        $id = dbInsert($insert, 'entPhysical');
1902
        $entphysical[$location] = dbFetchRow('SELECT * FROM entPhysical WHERE entPhysical_id=?', array($id));
1903
        return $index;
1904
    } // end if - Level 1
1905
} // end function
1906
1907
1908
/* idea from http://php.net/manual/en/function.hex2bin.php comments */
1909
function hex2bin_compat($str)
1910
{
1911
    if (strlen($str) % 2 !== 0) {
1912
        trigger_error(__FUNCTION__.'(): Hexadecimal input string must have an even length', E_USER_WARNING);
1913
    }
1914
    return pack("H*", $str);
1915
}
1916
1917
if (!function_exists('hex2bin')) {
1918
    // This is only a hack
1919
    function hex2bin($str)
1920
    {
1921
        return hex2bin_compat($str);
1922
    }
1923
}
1924
1925
function q_bridge_bits2indices($hex_data)
1926
{
1927
    /* convert hex string to an array of 1-based indices of the nonzero bits
1928
     * ie. '9a00' -> '100110100000' -> array(1, 4, 5, 7)
1929
    */
1930
    $hex_data = str_replace(' ', '', $hex_data);
1931
    $value = hex2bin($hex_data);
1932
    $length = strlen($value);
1933
    $indices = array();
1934
    for ($i = 0; $i < $length; $i++) {
1935
        $byte = ord($value[$i]);
1936
        for ($j = 7; $j >= 0; $j--) {
1937
            if ($byte & (1 << $j)) {
1938
                $indices[] = 8*$i + 8-$j;
1939
            }
1940
        }
1941
    }
1942
    return $indices;
1943
}
1944
1945
/**
1946
 * @param array $device
1947
 * @param int|string $raw_value The value returned from snmp
1948
 * @param int $capacity the normalized capacity
1949
 * @return int the toner level as a percentage
1950
 */
1951
function get_toner_levels($device, $raw_value, $capacity)
1952
{
1953
    // -3 means some toner is left
1954
    if ($raw_value == '-3') {
1955
        return 50;
1956
    }
1957
1958
    // -2 means unknown
1959
    if ($raw_value == '-2') {
1960
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
1961
    }
1962
1963
    // -1 mean no restrictions
1964
    if ($raw_value == '-1') {
1965
        return 0;  // FIXME: is 0 what we should return?
1966
    }
1967
1968
    // Non-standard snmp values
1969
    if ($device['os'] == 'ricoh' || $device['os'] == 'nrg' || $device['os'] == 'lanier') {
1970
        if ($raw_value == '-100') {
1971
            return 0;
1972
        }
1973
    } elseif ($device['os'] == 'brother') {
1974
        if (!str_contains($device['hardware'], 'MFC-L8850')) {
1975
            switch ($raw_value) {
1976
                case '0':
1977
                    return 100;
1978
                case '1':
1979
                    return 5;
1980
                case '2':
1981
                    return 0;
1982
                case '3':
1983
                    return 1;
1984
            }
1985
        }
1986
    }
1987
1988
    return round($raw_value / $capacity * 100);
1989
}
1990
1991
/**
1992
 * Intialize global stat arrays
1993
 */
1994
function initStats()
1995
{
1996
    global $snmp_stats, $rrd_stats;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1997
    global $snmp_stats_last, $rrd_stats_last;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1998
1999
    if (!isset($snmp_stats, $rrd_stats)) {
2000
        $snmp_stats = array(
2001
            'ops' => array(
2002
                'snmpget' => 0,
2003
                'snmpgetnext' => 0,
2004
                'snmpwalk' => 0,
2005
            ),
2006
            'time' => array(
2007
                'snmpget' => 0.0,
2008
                'snmpgetnext' => 0.0,
2009
                'snmpwalk' => 0.0,
2010
            )
2011
        );
2012
        $snmp_stats_last = $snmp_stats;
2013
2014
        $rrd_stats = array(
2015
            'ops' => array(
2016
                'update' => 0,
2017
                'create' => 0,
2018
                'other' => 0,
2019
            ),
2020
            'time' => array(
2021
                'update' => 0.0,
2022
                'create' => 0.0,
2023
                'other' => 0.0,
2024
            ),
2025
        );
2026
        $rrd_stats_last = $rrd_stats;
2027
    }
2028
}
2029
2030
/**
2031
 * Print out the stats totals since the last time this function was called
2032
 *
2033
 * @param bool $update_only Only update the stats checkpoint, don't print them
2034
 */
2035
function printChangedStats($update_only = false)
2036
{
2037
    global $snmp_stats, $db_stats, $rrd_stats;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2038
    global $snmp_stats_last, $db_stats_last, $rrd_stats_last;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2039
2040
    if (!$update_only) {
2041
        printf(
2042
            ">> SNMP: [%d/%.2fs] MySQL: [%d/%.2fs] RRD: [%d/%.2fs]\n",
2043
            array_sum($snmp_stats['ops']) - array_sum($snmp_stats_last['ops']),
2044
            array_sum($snmp_stats['time']) - array_sum($snmp_stats_last['time']),
2045
            array_sum($db_stats['ops']) - array_sum($db_stats_last['ops']),
2046
            array_sum($db_stats['time']) - array_sum($db_stats_last['time']),
2047
            array_sum($rrd_stats['ops']) - array_sum($rrd_stats_last['ops']),
2048
            array_sum($rrd_stats['time']) - array_sum($rrd_stats_last['time'])
2049
        );
2050
    }
2051
2052
    // make a new checkpoint
2053
    $snmp_stats_last = $snmp_stats;
2054
    $db_stats_last = $db_stats;
2055
    $rrd_stats_last = $rrd_stats;
2056
}
2057
2058
/**
2059
 * Print global stat arrays
2060
 */
2061
function printStats()
2062
{
2063
    global $snmp_stats, $db_stats, $rrd_stats;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2064
2065
    if ($snmp_stats) {
2066
        printf(
2067
            "SNMP [%d/%.2fs]: Get[%d/%.2fs] Getnext[%d/%.2fs] Walk[%d/%.2fs]\n",
2068
            array_sum($snmp_stats['ops']),
2069
            array_sum($snmp_stats['time']),
2070
            $snmp_stats['ops']['snmpget'],
2071
            $snmp_stats['time']['snmpget'],
2072
            $snmp_stats['ops']['snmpgetnext'],
2073
            $snmp_stats['time']['snmpgetnext'],
2074
            $snmp_stats['ops']['snmpwalk'],
2075
            $snmp_stats['time']['snmpwalk']
2076
        );
2077
    }
2078
2079
    if ($db_stats) {
2080
        printf(
2081
            "MySQL [%d/%.2fs]: Cell[%d/%.2fs] Row[%d/%.2fs] Rows[%d/%.2fs] Column[%d/%.2fs] Update[%d/%.2fs] Insert[%d/%.2fs] Delete[%d/%.2fs]\n",
2082
            array_sum($db_stats['ops']),
2083
            array_sum($db_stats['time']),
2084
            $db_stats['ops']['fetchcell'],
2085
            $db_stats['time']['fetchcell'],
2086
            $db_stats['ops']['fetchrow'],
2087
            $db_stats['time']['fetchrow'],
2088
            $db_stats['ops']['fetchrows'],
2089
            $db_stats['time']['fetchrows'],
2090
            $db_stats['ops']['fetchcolumn'],
2091
            $db_stats['time']['fetchcolumn'],
2092
            $db_stats['ops']['update'],
2093
            $db_stats['time']['update'],
2094
            $db_stats['ops']['insert'],
2095
            $db_stats['time']['insert'],
2096
            $db_stats['ops']['delete'],
2097
            $db_stats['time']['delete']
2098
        );
2099
    }
2100
2101
    if ($rrd_stats) {
2102
        printf(
2103
            "RRD [%d/%.2fs]: Update[%d/%.2fs] Create [%d/%.2fs] Other[%d/%.2fs]\n",
2104
            array_sum($rrd_stats['ops']),
2105
            array_sum($rrd_stats['time']),
2106
            $rrd_stats['ops']['update'],
2107
            $rrd_stats['time']['update'],
2108
            $rrd_stats['ops']['create'],
2109
            $rrd_stats['time']['create'],
2110
            $rrd_stats['ops']['other'],
2111
            $rrd_stats['time']['other']
2112
        );
2113
    }
2114
}
2115
2116
/**
2117
 * Update statistics for rrd operations
2118
 *
2119
 * @param string $stat create, update, and other
2120
 * @param float $start_time The time the operation started with 'microtime(true)'
2121
 * @return float  The calculated run time
2122
 */
2123
function recordRrdStatistic($stat, $start_time)
2124
{
2125
    global $rrd_stats;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2126
    initStats();
2127
2128
    $stat = ($stat == 'update' || $stat == 'create') ? $stat : 'other';
2129
2130
    $runtime = microtime(true) - $start_time;
2131
    $rrd_stats['ops'][$stat]++;
2132
    $rrd_stats['time'][$stat] += $runtime;
2133
2134
    return $runtime;
2135
}
2136
2137
/**
2138
 * @param string $stat snmpget, snmpwalk
2139
 * @param float $start_time The time the operation started with 'microtime(true)'
2140
 * @return float  The calculated run time
2141
 */
2142
function recordSnmpStatistic($stat, $start_time)
2143
{
2144
    global $snmp_stats;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
2145
    initStats();
2146
2147
    $runtime = microtime(true) - $start_time;
2148
    $snmp_stats['ops'][$stat]++;
2149
    $snmp_stats['time'][$stat] += $runtime;
2150
    return $runtime;
2151
}
2152
2153
function runTraceroute($device)
2154
{
2155
    $address_family = snmpTransportToAddressFamily($device['transport']);
2156
    $trace_name = $address_family == 'ipv6' ? 'traceroute6' : 'traceroute';
2157
    $trace_path = Config::get($trace_name, $trace_name);
2158
    $process = new Process([$trace_path, '-q', '1', '-w', '1', $device['hostname']]);
2159
    $process->run();
2160
    if ($process->isSuccessful()) {
2161
        return ['traceroute' => $process->getOutput()];
2162
    }
2163
    return ['output' => $process->getErrorOutput()];
2164
}
2165
2166
/**
2167
 * @param $device
2168
 * @param bool $record_perf
2169
 * @return array
2170
 */
2171
function device_is_up($device, $record_perf = false)
2172
{
2173
    $address_family = snmpTransportToAddressFamily($device['transport']);
2174
    $ping_response = isPingable($device['hostname'], $address_family, $device['attribs']);
2175
    $device_perf              = $ping_response['db'];
2176
    $device_perf['device_id'] = $device['device_id'];
2177
    $device_perf['timestamp'] = array('NOW()');
2178
2179
    if ($record_perf === true && can_ping_device($device['attribs'])) {
2180
        $trace_debug = [];
2181
        if ($ping_response['result'] === false && Config::get('debug.run_trace', false)) {
2182
            $trace_debug = runTraceroute($device);
2183
        }
2184
        $device_perf['debug'] = json_encode($trace_debug);
2185
        dbInsert($device_perf, 'device_perf');
2186
    }
2187
    $response              = array();
2188
    $response['ping_time'] = $ping_response['last_ping_timetaken'];
2189
    if ($ping_response['result']) {
2190
        if ($device['snmp_disable'] || isSNMPable($device)) {
2191
            $response['status']        = '1';
2192
            $response['status_reason'] = '';
2193
        } else {
2194
            echo 'SNMP Unreachable';
2195
            $response['status']        = '0';
2196
            $response['status_reason'] = 'snmp';
2197
        }
2198
    } else {
2199
        echo 'Unpingable';
2200
        $response['status']        = '0';
2201
        $response['status_reason'] = 'icmp';
2202
    }
2203
2204
    if ($device['status'] != $response['status'] || $device['status_reason'] != $response['status_reason']) {
2205
        dbUpdate(
2206
            array('status' => $response['status'], 'status_reason' => $response['status_reason']),
2207
            'devices',
2208
            'device_id=?',
2209
            array($device['device_id'])
2210
        );
2211
2212
        if ($response['status']) {
2213
            $type = 'up';
2214
            $reason = $device['status_reason'];
2215
        } else {
2216
            $type = 'down';
2217
            $reason = $response['status_reason'];
2218
        }
2219
2220
        log_event('Device status changed to ' . ucfirst($type) . " from $reason check.", $device, $type);
2221
    }
2222
    return $response;
2223
}
2224
2225
function update_device_logo(&$device)
2226
{
2227
    $icon = getImageName($device, false);
2228
    if ($icon != $device['icon']) {
2229
        log_event('Device Icon changed ' . $device['icon'] . " => $icon", $device, 'system', 3);
2230
        $device['icon'] = $icon;
2231
        dbUpdate(array('icon' => $icon), 'devices', 'device_id=?', array($device['device_id']));
2232
        echo "Changed Icon! : $icon\n";
2233
    }
2234
}
2235
2236
/**
2237
 * Function to generate PeeringDB Cache
2238
 */
2239
function cache_peeringdb()
2240
{
2241
    if (Config::get('peeringdb.enabled') === true) {
2242
        $peeringdb_url = 'https://peeringdb.com/api';
2243
        // We cache for 71 hours
2244
        $cached = dbFetchCell("SELECT count(*) FROM `pdb_ix` WHERE (UNIX_TIMESTAMP() - timestamp) < 255600");
2245
        if ($cached == 0) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $cached of type mixed|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
2246
            $rand = rand(3, 30);
2247
            echo "No cached PeeringDB data found, sleeping for $rand seconds" . PHP_EOL;
2248
            sleep($rand);
2249
            $peer_keep = [];
2250
            $ix_keep = [];
2251
            foreach (dbFetchRows("SELECT `bgpLocalAs` FROM `devices` WHERE `disabled` = 0 AND `ignore` = 0 AND `bgpLocalAs` > 0 AND (`bgpLocalAs` < 64512 OR `bgpLocalAs` > 65535) AND `bgpLocalAs` < 4200000000 GROUP BY `bgpLocalAs`") as $as) {
2252
                $asn = $as['bgpLocalAs'];
2253
                $get = Requests::get($peeringdb_url . '/net?depth=2&asn=' . $asn, array(), array('proxy' => get_proxy()));
2254
                $json_data = $get->body;
2255
                $data = json_decode($json_data);
2256
                $ixs = $data->{'data'}{0}->{'netixlan_set'};
2257
                foreach ($ixs as $ix) {
2258
                    $ixid = $ix->{'ix_id'};
2259
                    $tmp_ix = dbFetchRow("SELECT * FROM `pdb_ix` WHERE `ix_id` = ? AND asn = ?", array($ixid, $asn));
2260
                    if ($tmp_ix) {
2261
                        $pdb_ix_id = $tmp_ix['pdb_ix_id'];
2262
                        $update = array('name' => $ix->{'name'}, 'timestamp' => time());
2263
                        dbUpdate($update, 'pdb_ix', '`ix_id` = ? AND `asn` = ?', array($ixid, $asn));
2264
                    } else {
2265
                        $insert = array(
2266
                            'ix_id' => $ixid,
2267
                            'name' => $ix->{'name'},
2268
                            'asn' => $asn,
2269
                            'timestamp' => time()
2270
                        );
2271
                        $pdb_ix_id = dbInsert($insert, 'pdb_ix');
2272
                    }
2273
                    $ix_keep[] = $pdb_ix_id;
2274
                    $get_ix = Requests::get("$peeringdb_url/netixlan?ix_id=$ixid", array(), array('proxy' => get_proxy()));
2275
                    $ix_json = $get_ix->body;
2276
                    $ix_data = json_decode($ix_json);
2277
                    $peers = $ix_data->{'data'};
2278
                    foreach ($peers as $index => $peer) {
2279
                        $peer_name = get_astext($peer->{'asn'});
2280
                        $tmp_peer = dbFetchRow("SELECT * FROM `pdb_ix_peers` WHERE `peer_id` = ? AND `ix_id` = ?", array($peer->{'id'}, $ixid));
2281
                        if ($tmp_peer) {
2282
                            $peer_keep[] = $tmp_peer['pdb_ix_peers_id'];
2283
                            $update = array(
2284
                                'remote_asn'     => $peer->{'asn'},
2285
                                'remote_ipaddr4'  => $peer->{'ipaddr4'},
2286
                                'remote_ipaddr6' => $peer->{'ipaddr6'},
2287
                                'name'           => $peer_name,
2288
                            );
2289
                            dbUpdate($update, 'pdb_ix_peers', '`pdb_ix_peers_id` = ?', array($tmp_peer['pdb_ix_peers_id']));
2290
                        } else {
2291
                            $peer_insert = array(
2292
                                'ix_id'          => $ixid,
2293
                                'peer_id'        => $peer->{'id'},
2294
                                'remote_asn'     => $peer->{'asn'},
2295
                                'remote_ipaddr4' => $peer->{'ipaddr4'},
2296
                                'remote_ipaddr6' => $peer->{'ipaddr6'},
2297
                                'name'           => $peer_name,
2298
                                'timestamp'      => time()
2299
                            );
2300
                            $peer_keep[] = dbInsert($peer_insert, 'pdb_ix_peers');
2301
                        }
2302
                    }
2303
                }
2304
            }
2305
2306
            // cleanup
2307
            if (empty($peer_keep)) {
2308
                dbDelete('pdb_ix_peers');
2309
            } else {
2310
                dbDelete('pdb_ix_peers', "`pdb_ix_peers_id` NOT IN " . dbGenPlaceholders(count($peer_keep)), $peer_keep);
2311
            }
2312
            if (empty($ix_keep)) {
2313
                dbDelete('pdb_ix');
2314
            } else {
2315
                dbDelete('pdb_ix', "`pdb_ix_id` NOT IN " . dbGenPlaceholders(count($ix_keep)), $ix_keep);
2316
            }
2317
        } else {
2318
            echo "Cached PeeringDB data found....." . PHP_EOL;
2319
        }
2320
    } else {
2321
        echo 'Peering DB integration disabled' . PHP_EOL;
2322
    }
2323
}
2324
2325
/**
2326
 * Dump the database schema to an array.
2327
 * The top level will be a list of tables
2328
 * Each table contains the keys Columns and Indexes.
2329
 *
2330
 * Each entry in the Columns array contains these keys: Field, Type, Null, Default, Extra
2331
 * Each entry in the Indexes array contains these keys: Name, Columns(array), Unique
2332
 *
2333
 * @return array
2334
 */
2335
function dump_db_schema()
2336
{
2337
    $output = [];
2338
    $db_name = dbFetchCell('SELECT DATABASE()');
2339
2340
    foreach (dbFetchRows("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '$db_name' ORDER BY TABLE_NAME;") as $table) {
2341
        $table = $table['TABLE_NAME'];
2342
        foreach (dbFetchRows("SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$db_name' AND TABLE_NAME='$table'") as $data) {
2343
            $def = [
2344
                'Field'   => $data['COLUMN_NAME'],
2345
                'Type'    => $data['COLUMN_TYPE'],
2346
                'Null'    => $data['IS_NULLABLE'] === 'YES',
2347
                'Extra'   => str_replace('current_timestamp()', 'CURRENT_TIMESTAMP', $data['EXTRA']),
2348
            ];
2349
2350
            if (isset($data['COLUMN_DEFAULT']) && $data['COLUMN_DEFAULT'] != 'NULL') {
2351
                $default = trim($data['COLUMN_DEFAULT'], "'");
2352
                $def['Default'] = str_replace('current_timestamp()', 'CURRENT_TIMESTAMP', $default);
2353
            }
2354
2355
            $output[$table]['Columns'][] = $def;
2356
        }
2357
2358
        foreach (dbFetchRows("SHOW INDEX FROM `$table`") as $key) {
2359
            $key_name = $key['Key_name'];
2360
            if (isset($output[$table]['Indexes'][$key_name])) {
2361
                $output[$table]['Indexes'][$key_name]['Columns'][] = $key['Column_name'];
2362
            } else {
2363
                $output[$table]['Indexes'][$key_name] = [
2364
                    'Name'    => $key['Key_name'],
2365
                    'Columns' => [$key['Column_name']],
2366
                    'Unique'  => !$key['Non_unique'],
2367
                    'Type'    => $key['Index_type'],
2368
                ];
2369
            }
2370
        }
2371
2372
        $create = dbFetchRow("SHOW CREATE TABLE `$table`");
2373
        if (isset($create['Create Table'])) {
2374
            $constraint_regex = '/CONSTRAINT `(?<name>[A-Za-z_0-9]+)` FOREIGN KEY \(`(?<foreign_key>[A-Za-z_0-9]+)`\) REFERENCES `(?<table>[A-Za-z_0-9]+)` \(`(?<key>[A-Za-z_0-9]+)`\) ?(?<extra>[ A-Z]+)?/';
2375
            $constraint_count = preg_match_all($constraint_regex, $create['Create Table'], $constraints);
2376
            for ($i = 0; $i < $constraint_count; $i++) {
2377
                $constraint_name = $constraints['name'][$i];
2378
                $output[$table]['Constraints'][$constraint_name] = [
2379
                    'name' => $constraint_name,
2380
                    'foreign_key' => $constraints['foreign_key'][$i],
2381
                    'table' => $constraints['table'][$i],
2382
                    'key' => $constraints['key'][$i],
2383
                    'extra' => $constraints['extra'][$i],
2384
                ];
2385
            }
2386
        }
2387
    }
2388
2389
    return $output;
2390
}
2391
2392
2393
2394
2395
2396
2397
/**
2398
 * Get an array of the schema files.
2399
 * schema_version => full_file_name
2400
 *
2401
 * @return mixed
2402
 */
2403
function get_schema_list()
2404
{
2405
    // glob returns an array sorted by filename
2406
    $files = glob(Config::get('install_dir') . '/sql-schema/*.sql');
2407
2408
    // set the keys to the db schema version
2409
    $files = array_reduce($files, function ($array, $file) {
0 ignored issues
show
Bug introduced by
It seems like $files can also be of type false; however, parameter $input of array_reduce() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2409
    $files = array_reduce(/** @scrutinizer ignore-type */ $files, function ($array, $file) {
Loading history...
2410
        $array[(int)basename($file, '.sql')] = $file;
2411
        return $array;
2412
    }, []);
2413
2414
    ksort($files); // fix dbSchema 1000 order
2415
    return $files;
2416
}
2417
2418
/**
2419
 * Get the current database schema, will return 0 if there is no schema.
2420
 *
2421
 * @return int
2422
 */
2423
function get_db_schema()
2424
{
2425
    try {
2426
        $db = \LibreNMS\DB\Eloquent::DB();
2427
        if ($db) {
0 ignored issues
show
introduced by
$db is of type Illuminate\Database\Connection, thus it always evaluated to true.
Loading history...
2428
            return (int)$db->table('dbSchema')
2429
                ->orderBy('version', 'DESC')
2430
                ->value('version');
2431
        }
2432
    } catch (PDOException $e) {
2433
        // return default
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2434
    }
2435
2436
    return 0;
2437
}
2438
2439
/**
2440
 * @param $device
2441
 * @return int|null
2442
 */
2443
function get_device_oid_limit($device)
2444
{
2445
    // device takes priority
2446
    if ($device['snmp_max_oid'] > 0) {
2447
        return $device['snmp_max_oid'];
2448
    }
2449
2450
    // then os
2451
    $os_max = Config::getOsSetting($device['os'], 'snmp_max_oid', 0);
2452
    if ($os_max > 0) {
2453
        return $os_max;
2454
    }
2455
2456
    // then global
2457
    $global_max = Config::get('snmp.max_oid', 10);
2458
    return $global_max > 0 ? $global_max : 10;
2459
}
2460
2461
/**
2462
 * Strip out non-numeric characters
2463
 */
2464
function return_num($entry)
2465
{
2466
    if (!is_numeric($entry)) {
2467
        preg_match('/-?\d*\.?\d+/', $entry, $num_response);
2468
        return $num_response[0];
2469
    }
2470
}
2471
2472
/**
2473
 * If Distributed, create a lock, then purge the mysql table
2474
 *
2475
 * @param string $table
2476
 * @param string $sql
2477
 * @return int exit code
2478
 */
2479
function lock_and_purge($table, $sql)
2480
{
2481
    try {
2482
        $purge_name = $table . '_purge';
2483
2484
        if (Config::get('distributed_poller')) {
2485
            MemcacheLock::lock($purge_name, 0, 86000);
2486
        }
2487
        $purge_days = Config::get($purge_name);
2488
2489
        $name = str_replace('_', ' ', ucfirst($table));
2490
        if (is_numeric($purge_days)) {
2491
            if (dbDelete($table, $sql, array($purge_days))) {
2492
                echo "$name cleared for entries over $purge_days days\n";
2493
            }
2494
        }
2495
        return 0;
2496
    } catch (LockException $e) {
2497
        echo $e->getMessage() . PHP_EOL;
2498
        return -1;
2499
    }
2500
}
2501
2502
/**
2503
 * Convert space separated hex OID content to character
2504
 *
2505
 * @param string $hex_string
2506
 * @return string $chr_string
2507
 */
2508
2509
function hexbin($hex_string)
2510
{
2511
    $chr_string = '';
2512
    foreach (explode(' ', $hex_string) as $a) {
2513
        $chr_string .= chr(hexdec($a));
0 ignored issues
show
Bug introduced by
It seems like hexdec($a) can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2513
        $chr_string .= chr(/** @scrutinizer ignore-type */ hexdec($a));
Loading history...
2514
    }
2515
    return $chr_string;
2516
}
2517
2518
/**
2519
 * Check if disk is valid to poll.
2520
 * Settings: bad_disk_regexp
2521
 *
2522
 * @param array $disk
2523
 * @param array $device
2524
 * @return bool
2525
 */
2526
function is_disk_valid($disk, $device)
2527
{
2528
    foreach (Config::getCombined($device['os'], 'bad_disk_regexp') as $bir) {
2529
        if (preg_match($bir ."i", $disk['diskIODevice'])) {
2530
            d_echo("Ignored Disk: {$disk['diskIODevice']} (matched: $bir)\n");
2531
            return false;
2532
        }
2533
    }
2534
    return true;
2535
}
2536
2537
2538
/**
2539
 * Queues a hostname to be refreshed by Oxidized
2540
 * Settings: oxidized.url
2541
 *
2542
 * @param string $hostname
2543
 * @param string $msg
2544
 * @param string $username
2545
 * @return bool
2546
 */
2547
function oxidized_node_update($hostname, $msg, $username = 'not_provided')
2548
{
2549
    // Work around https://github.com/rack/rack/issues/337
2550
    $msg = str_replace("%", "", $msg);
2551
    $postdata = ["user" => $username, "msg" => $msg];
2552
    $oxidized_url = Config::get('oxidized.url');
2553
    if (!empty($oxidized_url)) {
2554
        Requests::put("$oxidized_url/node/next/$hostname", [], json_encode($postdata), ['proxy' => get_proxy()]);
2555
        return true;
2556
    }
2557
    return false;
2558
}//end oxidized_node_update()
2559