Passed
Push — master ( 4cf584...63442e )
by Tony
10:02
created

Config::reload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Config.php
4
 *
5
 * Config convenience class to access and set config variables.
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 * @package    LibreNMS
21
 * @link       http://librenms.org
22
 * @copyright  2017 Tony Murray
23
 * @author     Tony Murray <[email protected]>
24
 */
25
26
namespace LibreNMS;
27
28
use App\Models\GraphType;
29
use Illuminate\Database\QueryException;
30
use Illuminate\Support\Arr;
31
use LibreNMS\DB\Eloquent;
32
33
class Config
34
{
35
    private static $config;
36
37
    /**
38
     * Load the config, if the database connected, pull in database settings.
39
     *
40
     * return &array
41
     */
42
    public static function load()
43
    {
44
        if (!is_null(self::$config)) {
45
            return self::$config;
46
        }
47
48
        self::loadFiles();
49
50
        // Make sure the database is connected
51
        if (Eloquent::isConnected()) {
52
            // pull in the database config settings
53
            self::mergeDb();
54
55
            // load graph types from the database
56
            self::loadGraphsFromDb();
57
58
            // process $config to tidy up
59
            self::processConfig(true);
60
        } else {
61
            // just process $config
62
            self::processConfig(false);
63
        }
64
65
        // set to global for legacy/external things
66
        global $config;
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...
67
        $config = self::$config;
68
69
        return self::$config;
70
    }
71
72
    /**
73
     * Reload the config from files/db
74
     * @return mixed
75
     */
76
    public static function reload()
77
    {
78
        self::$config = null;
79
        return self::load();
80
    }
81
82
    /**
83
     * Load the user config from config.php, defaults.inc.php and definitions.inc.php, etc.
84
     * Erases existing config.
85
     *
86
     * @return array
87
     */
88
    private static function &loadFiles()
89
    {
90
        $config = []; // start fresh
91
92
        $install_dir = realpath(__DIR__ . '/../');
93
        $config['install_dir'] = $install_dir;
94
95
        // load defaults
96
        require $install_dir . '/includes/defaults.inc.php';
97
        require $install_dir . '/includes/definitions.inc.php';
98
99
        // import standard settings
100
        $macros = json_decode(file_get_contents($install_dir . '/misc/macros.json'), true);
101
        $config['alert']['macros']['rule'] = $macros;
102
103
        // Load user config
104
        @include $install_dir . '/config.php';
105
106
        // set it
107
        self::$config = $config;
108
109
        return self::$config;
110
    }
111
112
113
    /**
114
     * Get a config value, if non existent null (or default if set) will be returned
115
     *
116
     * @param string $key period separated config variable name
117
     * @param mixed $default optional value to return if the setting is not set
118
     * @return mixed
119
     */
120
    public static function get($key, $default = null)
121
    {
122
        if (isset(self::$config[$key])) {
123
            return self::$config[$key];
124
        }
125
126
        if (!str_contains($key, '.')) {
127
            return $default;
128
        }
129
130
        $keys = explode('.', $key);
131
132
        $curr = &self::$config;
133
        foreach ($keys as $k) {
134
            // do not add keys that don't exist
135
            if (!isset($curr[$k])) {
136
                return $default;
137
            }
138
            $curr = &$curr[$k];
139
        }
140
141
        if (is_null($curr)) {
142
            return $default;
143
        }
144
145
        return $curr;
146
    }
147
148
    /**
149
     * Unset a config setting
150
     * or multiple
151
     *
152
     * @param string|array $key
153
     */
154
    public static function forget($key)
155
    {
156
        Arr::forget(self::$config, $key);
157
    }
158
159
    /**
160
     * Get a setting from a device, if that is not set,
161
     * fall back to the global config setting prefixed by $global_prefix
162
     * The key must be the same for the global setting and the device setting.
163
     *
164
     * @param array $device Device array
165
     * @param string $key Name of setting to fetch
166
     * @param string $global_prefix specify where the global setting lives in the global config
167
     * @param mixed $default will be returned if the setting is not set on the device or globally
168
     * @return mixed
169
     */
170
    public static function getDeviceSetting($device, $key, $global_prefix = null, $default = null)
171
    {
172
        if (isset($device[$key])) {
173
            return $device[$key];
174
        }
175
176
        if (isset($global_prefix)) {
177
            $key = "$global_prefix.$key";
178
        }
179
180
        return self::get($key, $default);
181
    }
182
183
    /**
184
     * Get a setting from the $config['os'] array using the os of the given device
185
     * If that is not set, fallback to the same global config key
186
     *
187
     * @param string $os The os name
188
     * @param string $key period separated config variable name
189
     * @param mixed $default optional value to return if the setting is not set
190
     * @return mixed
191
     */
192
    public static function getOsSetting($os, $key, $default = null)
193
    {
194
        if ($os) {
195
            if (isset(self::$config['os'][$os][$key])) {
196
                return self::$config['os'][$os][$key];
197
            }
198
199
            if (!str_contains($key, '.')) {
200
                return self::get($key, $default);
201
            }
202
203
            $os_key = "os.$os.$key";
204
            if (self::has($os_key)) {
205
                return self::get($os_key);
206
            }
207
        }
208
209
        return self::get($key, $default);
210
    }
211
212
    /**
213
     * Get the merged array from the global and os settings for the specified key.
214
     * Removes any duplicates.
215
     * When the arrays have keys, os settings take precedence over global settings
216
     *
217
     * @param string $os The os name
218
     * @param string $key period separated config variable name
219
     * @param array $default optional array to return if the setting is not set
220
     * @return array
221
     */
222
    public static function getCombined($os, $key, $default = array())
223
    {
224
        if (!self::has($key)) {
225
            return self::get("os.$os.$key", $default);
226
        }
227
228
        if (!isset(self::$config['os'][$os][$key])) {
229
            if (!str_contains($key, '.')) {
230
                return self::get($key, $default);
231
            }
232
            if (!self::has("os.$os.$key")) {
233
                return self::get($key, $default);
234
            }
235
        }
236
237
        return array_unique(array_merge(
238
            (array)self::get($key, $default),
239
            (array)self::getOsSetting($os, $key, $default)
240
        ));
241
    }
242
243
    /**
244
     * Set a variable in the global config
245
     *
246
     * @param mixed $key period separated config variable name
247
     * @param mixed $value
248
     * @param bool $persist set the setting in the database so it persists across runs
249
     * @param string $default default (only set when initially created)
250
     * @param string $descr webui description (only set when initially created)
251
     * @param string $group webui group (only set when initially created)
252
     * @param string $sub_group webui subgroup (only set when initially created)
253
     */
254
    public static function set($key, $value, $persist = false, $default = null, $descr = null, $group = null, $sub_group = null)
255
    {
256
        if ($persist) {
257
            try {
258
                \App\Models\Config::updateOrCreate(['config_name' => $key], collect([
259
                    'config_name' => $key,
260
                    'config_default' => $default,
261
                    'config_descr' => $descr,
262
                    'config_group' => $group,
263
                    'config_sub_group' => $sub_group,
264
                ])->filter(function ($value, $field) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

264
                ])->filter(function ($value, /** @scrutinizer ignore-unused */ $field) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
265
                    return !is_null($value);
266
                })->put('config_value', $value)->toArray());
267
            } catch (QueryException $e) {
268
                if (class_exists(\Log::class)) {
269
                    \Log::error($e);
270
                }
271
                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...
272
                if ($debug) {
273
                    echo $e;
274
                }
275
            }
276
        }
277
278
        $keys = explode('.', $key);
279
280
        $curr = &self::$config;
281
        foreach ($keys as $k) {
282
            $curr = &$curr[$k];
283
        }
284
285
        $curr = $value;
286
    }
287
288
    /**
289
     * Check if a setting is set
290
     *
291
     * @param string $key period separated config variable name
292
     * @return bool
293
     */
294
    public static function has($key)
295
    {
296
        if (isset(self::$config[$key])) {
297
            return true;
298
        }
299
300
        if (!str_contains($key, '.')) {
301
            return false;
302
        }
303
304
        $keys = explode('.', $key);
305
        $last = array_pop($keys);
306
307
        $curr = &self::$config;
308
        foreach ($keys as $k) {
309
            // do not add keys that don't exist
310
            if (!isset($curr[$k])) {
311
                return false;
312
            }
313
            $curr = &$curr[$k];
314
        }
315
316
        return is_array($curr) && isset($curr[$last]);
317
    }
318
319
    /**
320
     * Serialise the whole configuration to json for use in external processes.
321
     *
322
     * @return string
323
     */
324
    public static function json_encode()
325
    {
326
        return json_encode(self::$config);
327
    }
328
329
    /**
330
     * Get the full configuration array
331
     * @return array
332
     */
333
    public static function getAll()
334
    {
335
        return self::$config;
336
    }
337
338
    /**
339
     * merge the database config with the global config
340
     * Global config overrides db
341
     */
342
    private static function mergeDb()
343
    {
344
        $db_config = [];
345
346
        try {
347
            \App\Models\Config::get(['config_name', 'config_value'])
348
                ->each(function ($item) use (&$db_config) {
349
                    Arr::set($db_config, $item->config_name, $item->config_value);
350
                });
351
        } catch (QueryException $e) {
352
            // possibly table config doesn't exist yet
353
        }
354
355
        self::$config = array_replace_recursive($db_config, self::$config);
356
    }
357
358
    private static function loadGraphsFromDb()
359
    {
360
        try {
361
            $graph_types = GraphType::all()->toArray();
362
        } catch (QueryException $e) {
363
            // possibly table config doesn't exist yet
364
            $graph_types = [];
365
        }
366
367
        // load graph types from the database
368
        foreach ($graph_types as $graph) {
369
            $g = [];
370
            foreach ($graph as $k => $v) {
371
                if (strpos($k, 'graph_') == 0) {
372
                    // remove leading 'graph_' from column name
373
                    $key = str_replace('graph_', '', $k);
374
                } else {
375
                    $key = $k;
376
                }
377
                $g[$key] = $v;
378
            }
379
380
            self::$config['graph_types'][$g['type']][$g['subtype']] = $g;
381
        }
382
    }
383
384
    /**
385
     * Proces the config after it has been loaded.
386
     * Make sure certain variables have been set properly and
387
     *
388
     * @param bool $persist Save binary locations and other settings to the database.
389
     */
390
    private static function processConfig($persist = true)
391
    {
392
        if (!self::get('email_from')) {
393
            self::set('email_from', '"' . self::get('project_name') . '" <' . self::get('email_user') . '@' . php_uname('n') . '>');
394
        }
395
396
        // If we're on SSL, let's properly detect it
397
        if (isset($_SERVER['HTTPS'])) {
398
            self::set('base_url', preg_replace('/^http:/', 'https:', self::get('base_url')));
399
        }
400
401
        // Define some variables if they aren't set by user definition in config.php
402
        self::setDefault('html_dir', '%s/html', ['install_dir']);
403
        self::setDefault('rrd_dir', '%s/rrd', ['install_dir']);
404
        self::setDefault('mib_dir', '%s/mibs', ['install_dir']);
405
        self::setDefault('log_dir', '%s/logs', ['install_dir']);
406
        self::setDefault('log_file', '%s/%s.log', ['log_dir', 'project_id']);
407
        self::setDefault('plugin_dir', '%s/plugins', ['html_dir']);
408
        self::setDefault('temp_dir', sys_get_temp_dir() ?: '/tmp');
409
//        self::setDefault('email_from', '"%s" <%s@' . php_uname('n') . '>', ['project_name', 'email_user']);  // FIXME email_from set because alerting config
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% 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...
410
411
        // deprecated variables
412
        self::deprecatedVariable('rrdgraph_real_95th', 'rrdgraph_real_percentile');
413
        self::deprecatedVariable('fping_options.millisec', 'fping_options.interval');
414
        self::deprecatedVariable('discovery_modules.cisco-vrf', 'discovery_modules.vrf');
415
        self::deprecatedVariable('oxidized.group', 'oxidized.maps.group');
416
417
        // make sure we have full path to binaries in case PATH isn't set
418
        foreach (array('fping', 'fping6', 'snmpgetnext', 'rrdtool', 'traceroute', 'traceroute6') as $bin) {
419
            if (!is_executable(self::get($bin))) {
420
                self::set($bin, self::locateBinary($bin), $persist, $bin, "Path to $bin", 'external', 'paths');
421
            }
422
        }
423
    }
424
425
    /**
426
     * Set default values for defaults that depend on other settings, if they are not already loaded
427
     *
428
     * @param string $key
429
     * @param string $value value to set to key or vsprintf() format string for values below
430
     * @param array $format_values array of keys to send to vsprintf()
431
     */
432
    private static function setDefault($key, $value, $format_values = [])
433
    {
434
        if (!self::has($key)) {
435
            if (is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
436
                $format_values = array_map('self::get', $format_values);
437
                self::set($key, vsprintf($value, $format_values));
438
            } else {
439
                self::set($key, $value);
440
            }
441
        }
442
    }
443
444
    /**
445
     * Copy data from old variables to new ones.
446
     *
447
     * @param $old
448
     * @param $new
449
     */
450
    private static function deprecatedVariable($old, $new)
451
    {
452
        if (self::has($old)) {
453
            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...
454
            if ($debug) {
455
                echo "Copied deprecated config $old to $new\n";
456
            }
457
            self::set($new, self::get($old));
458
        }
459
    }
460
461
    /**
462
     * Get just the database connection settings from config.php
463
     *
464
     * @return array (keys: db_host, db_port, db_name, db_user, db_pass, db_socket)
465
     */
466
    public static function getDatabaseSettings()
467
    {
468
        // Do not access global $config in this function!
469
470
        $keys = $config = [
471
            'db_host' => '',
472
            'db_port' => '',
473
            'db_name' => '',
474
            'db_user' => '',
475
            'db_pass' => '',
476
            'db_socket' => '',
477
        ];
478
479
        if (is_file(__DIR__ . '/../config.php')) {
480
            include __DIR__ . '/../config.php';
481
        }
482
483
        // Check for testing database
484
        if (isset($config['test_db_name'])) {
485
            putenv('DB_TEST_DATABASE=' . $config['test_db_name']);
486
        }
487
        if (isset($config['test_db_user'])) {
488
            putenv('DB_TEST_USERNAME=' . $config['test_db_user']);
489
        }
490
        if (isset($config['test_db_pass'])) {
491
            putenv('DB_TEST_PASSWORD=' . $config['test_db_pass']);
492
        }
493
494
        return array_intersect_key($config, $keys); // return only the db settings
495
    }
496
497
    /**
498
     * Locate the actual path of a binary
499
     *
500
     * @param $binary
501
     * @return mixed
502
     */
503
    public static function locateBinary($binary)
504
    {
505
        if (!str_contains($binary, '/')) {
506
            $output = `whereis -b $binary`;
507
            $list = trim(substr($output, strpos($output, ':') + 1));
508
            $targets = explode(' ', $list);
509
            foreach ($targets as $target) {
510
                if (is_executable($target)) {
511
                    return $target;
512
                }
513
            }
514
        }
515
        return $binary;
516
    }
517
}
518