Issues (2963)

app/Console/Commands/SetConfigCommand.php (10 issues)

1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Console\Commands\Traits\CompletesConfigArgument;
6
use App\Console\LnmsCommand;
7
use Illuminate\Support\Arr;
8
use Illuminate\Support\Str;
9
use JsonSchema\Constraints\Constraint;
10
use JsonSchema\Exception\ValidationException;
11
use JsonSchema\Validator;
12
use LibreNMS\Config;
13
use LibreNMS\DB\Eloquent;
14
use LibreNMS\Util\DynamicConfig;
15
use LibreNMS\Util\OS;
16
use Symfony\Component\Console\Input\InputArgument;
17
18
class SetConfigCommand extends LnmsCommand
19
{
20
    use CompletesConfigArgument;
21
22
    protected $name = 'config:set';
23
24
    /**
25
     * Create a new command instance.
26
     *
27
     * @return void
28
     */
29
    public function __construct()
30
    {
31
        parent::__construct();
32
33
        $this->addArgument('setting', InputArgument::REQUIRED);
34
        $this->addArgument('value', InputArgument::OPTIONAL);
35
        $this->addOption('ignore-checks');
36
    }
37
38
    /**
39
     * Execute the console command.
40
     *
41
     * @return mixed
42
     */
43
    public function handle(DynamicConfig $definition)
44
    {
45
        $setting = $this->argument('setting');
46
        $value = $this->argument('value');
47
        $force = $this->option('ignore-checks');
48
        $parent = null;
49
50
        if (preg_match('/^os\.(?<os>[a-z_\-]+)\.(?<setting>.*)$/', $setting, $matches)) {
0 ignored issues
show
It seems like $setting can also be of type null and string[]; however, parameter $subject of preg_match() 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

50
        if (preg_match('/^os\.(?<os>[a-z_\-]+)\.(?<setting>.*)$/', /** @scrutinizer ignore-type */ $setting, $matches)) {
Loading history...
51
            $os = $matches['os'];
52
            try {
53
                $this->validateOsSetting($os, $matches['setting'], $value);
54
            } catch (ValidationException $e) {
55
                $this->error(trans('commands.config:set.errors.invalid'));
0 ignored issues
show
It seems like trans('commands.config:set.errors.invalid') can also be of type array and array; however, parameter $string of Illuminate\Console\Command::error() 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

55
                $this->error(/** @scrutinizer ignore-type */ trans('commands.config:set.errors.invalid'));
Loading history...
56
                $this->line($e->getMessage());
57
58
                return 2;
59
            }
60
        } elseif (! $definition->isValidSetting($setting)) {
0 ignored issues
show
It seems like $setting can also be of type string[]; however, parameter $name of LibreNMS\Util\DynamicConfig::isValidSetting() 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

60
        } elseif (! $definition->isValidSetting(/** @scrutinizer ignore-type */ $setting)) {
Loading history...
61
            $parent = $this->findParentSetting($definition, $setting);
62
            if (! $force && ! $parent) {
63
                $this->error(trans('commands.config:set.errors.invalid'));
64
65
                return 2;
66
            }
67
        }
68
69
        if (! Eloquent::isConnected()) {
70
            $this->error(trans('commands.config:set.errors.nodb'));
71
72
            return 1;
73
        }
74
75
        if (! $force && $value === null) {
76
            $message = $parent
77
                ? trans('commands.config:set.forget_from', ['path' => $this->getChildPath($setting, $parent), 'parent' => $parent])
78
                : trans('commands.config:set.confirm', ['setting' => $setting]);
79
80
            if ($this->confirm($message)) {
0 ignored issues
show
It seems like $message can also be of type array and array; however, parameter $question of Illuminate\Console\Command::confirm() 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

80
            if ($this->confirm(/** @scrutinizer ignore-type */ $message)) {
Loading history...
81
                return $this->erase($setting, $parent) ? 0 : 1;
82
            }
83
84
            return 3;
85
        }
86
87
        $value = $this->juggleType($value);
0 ignored issues
show
It seems like $value can also be of type string[]; however, parameter $value of App\Console\Commands\Set...igCommand::juggleType() does only seem to accept null|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

87
        $value = $this->juggleType(/** @scrutinizer ignore-type */ $value);
Loading history...
88
89
        // handle appending to arrays
90
        if (Str::endsWith($setting, '.+')) {
0 ignored issues
show
It seems like $setting can also be of type string[]; however, parameter $haystack of Illuminate\Support\Str::endsWith() 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

90
        if (Str::endsWith(/** @scrutinizer ignore-type */ $setting, '.+')) {
Loading history...
91
            $setting = substr($setting, 0, -2);
0 ignored issues
show
It seems like $setting can also be of type null and string[]; however, parameter $string of substr() 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

91
            $setting = substr(/** @scrutinizer ignore-type */ $setting, 0, -2);
Loading history...
92
            $sub_data = Config::get($setting, []);
93
            if (! is_array($sub_data)) {
94
                $this->error(trans('commands.config:set.errors.append'));
95
96
                return 2;
97
            }
98
99
            array_push($sub_data, $value);
100
            $value = $sub_data;
101
        }
102
103
        // handle setting value inside multi-dimensional array
104
        if ($parent && $parent !== $setting) {
105
            $parent_data = Config::get($parent);
106
            Arr::set($parent_data, $this->getChildPath($setting, $parent), $value);
107
            $value = $parent_data;
108
            $setting = $parent;
109
        }
110
111
        $configItem = $definition->get($setting);
0 ignored issues
show
It seems like $setting can also be of type string[]; however, parameter $name of LibreNMS\Util\DynamicConfig::get() 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

111
        $configItem = $definition->get(/** @scrutinizer ignore-type */ $setting);
Loading history...
112
        if (! $force
113
            && empty($os) // if os is set, value was already validated against os config
114
            && ! $configItem->checkValue($value)
115
        ) {
116
            $message = ($configItem->type || $configItem->validate)
117
                ? $configItem->getValidationMessage($value)
118
                : trans('commands.config:set.errors.no-validation', ['setting' => $setting]);
119
            $this->error($message);
120
121
            return 2;
122
        }
123
124
        if (Config::persist($setting, $value)) {
125
            return 0;
126
        }
127
128
        $this->error(trans('commands.config:set.errors.failed', ['setting' => $setting]));
129
130
        return 1;
131
    }
132
133
    /**
134
     * Convert the string input into the appropriate PHP native type
135
     *
136
     * @return mixed
137
     */
138
    private function juggleType(?string $value)
139
    {
140
        $json = json_decode($value, true);
0 ignored issues
show
It seems like $value can also be of type null; however, parameter $json of json_decode() 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

140
        $json = json_decode(/** @scrutinizer ignore-type */ $value, true);
Loading history...
141
142
        return json_last_error() ? $value : $json;
143
    }
144
145
    private function findParentSetting(DynamicConfig $definition, $setting): ?string
146
    {
147
        $parts = explode('.', $setting);
148
        array_pop($parts); // looking for parent, not this setting
149
150
        while (! empty($parts)) {
151
            $name = implode('.', $parts);
152
            if ($definition->isValidSetting($name)) {
153
                return $name;
154
            }
155
            array_pop($parts);
156
        }
157
158
        return null;
159
    }
160
161
    private function erase($setting, $parent = null)
162
    {
163
        if ($parent) {
164
            $data = Config::get($parent);
165
166
            if (preg_match("/^$parent\.?(?<sub>.+)\\.(?<index>\\d+)\$/", $setting, $matches)) {
167
                // nested inside the parent setting, update just the required part
168
                $sub_data = Arr::get($data, $matches['sub']);
169
                $this->forgetWithIndex($sub_data, $matches['index']);
170
                Arr::set($data, $matches['sub'], $sub_data);
171
            } else {
172
                // not nested, just forget the setting
173
                $this->forgetWithIndex($data, $this->getChildPath($setting, $parent));
174
            }
175
176
            return Config::persist($parent, $data);
177
        }
178
179
        return Config::erase($setting);
180
    }
181
182
    private function getChildPath($setting, $parent = null): string
183
    {
184
        return ltrim(Str::after($setting, $parent), '.');
185
    }
186
187
    private function hasSequentialIndex($array): bool
188
    {
189
        if (! is_array($array) || $array === []) {
190
            return false;
191
        }
192
193
        return array_keys($array) === range(0, count($array) - 1);
194
    }
195
196
    private function forgetWithIndex(&$data, $matches)
197
    {
198
        // detect sequentially numeric indexed array so we can re-index the array
199
        if ($this->hasSequentialIndex($data)) {
200
            array_splice($data, (int) $matches, 1);
201
        } else {
202
            Arr::forget($data, $matches);
203
        }
204
    }
205
206
    /**
207
     * @param  string  $os
208
     * @param  string  $setting
209
     * @param  mixed  $value
210
     *
211
     * @throws \JsonSchema\Exception\ValidationException
212
     */
213
    private function validateOsSetting(string $os, string $setting, $value)
214
    {
215
        // prep data to be validated
216
        OS::loadDefinition($os);
217
        $os_data = \LibreNMS\Config::get("os.$os");
218
        if ($os_data === null) {
219
            throw new ValidationException(trans('commands.config:set.errors.invalid_os', ['os' => $os]));
0 ignored issues
show
It seems like trans('commands.config:s...s', array('os' => $os)) can also be of type array and array; however, parameter $message of JsonSchema\Exception\Val...xception::__construct() 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

219
            throw new ValidationException(/** @scrutinizer ignore-type */ trans('commands.config:set.errors.invalid_os', ['os' => $os]));
Loading history...
220
        }
221
        $value = $this->juggleType($value);
222
223
        // append value if requested
224
        if (Str::endsWith($setting, '.+')) {
225
            $setting = substr($setting, 0, -2);
226
            $container = Arr::get($os_data, $setting, []);
227
            $container[] = $value;
228
            $value = $container;
229
        }
230
231
        Arr::set($os_data, $setting, $value);
232
        unset($os_data['definition_loaded']);
233
234
        $validator = new Validator;
235
        $validator->validate(
236
            $os_data,
237
            (object) ['$ref' => 'file://' . base_path('/misc/os_schema.json')],
238
            Constraint::CHECK_MODE_TYPE_CAST
239
        );
240
241
        $code = 0;
242
243
        $errors = collect($validator->getErrors())->filter(function ($error) use ($value, &$code) {
244
            if ($error['constraint'] == 'additionalProp') {
245
                $code = 1;
246
247
                return true;
248
            }
249
250
            // only check type if value is set (otherwise we are unsetting it)
251
            if (! empty($value) && $error['constraint'] == 'type') {
252
                if ($code === 0) {
253
                    $code = 2; // wrong path takes precedence over wrong type
254
                }
255
256
                return true;
257
            }
258
259
            return false;
260
        });
261
262
        if ($errors->isNotEmpty()) {
263
            throw new ValidationException($errors->pluck('message')->implode(PHP_EOL), $code);
264
        }
265
    }
266
}
267