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
Bug
introduced
by
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
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
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
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
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
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
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
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
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
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 |