addArgument('setting', InputArgument::REQUIRED); $this->addArgument('value', InputArgument::OPTIONAL); $this->addOption('ignore-checks'); } /** * Execute the console command. * * @return mixed */ public function handle(DynamicConfig $definition) { $setting = $this->argument('setting'); $value = $this->argument('value'); $force = $this->option('ignore-checks'); $parent = null; if (preg_match('/^os\.(?[a-z_\-]+)\.(?.*)$/', $setting, $matches)) { $os = $matches['os']; try { $this->validateOsSetting($os, $matches['setting'], $value); } catch (ValidationException $e) { $this->error(trans('commands.config:set.errors.invalid')); $this->line($e->getMessage()); return 2; } } elseif (! $definition->isValidSetting($setting)) { $parent = $this->findParentSetting($definition, $setting); if (! $force && ! $parent) { $this->error(trans('commands.config:set.errors.invalid')); return 2; } } if (! Eloquent::isConnected()) { $this->error(trans('commands.config:set.errors.nodb')); return 1; } if (! $force && $value === null) { $message = $parent ? trans('commands.config:set.forget_from', ['path' => $this->getChildPath($setting, $parent), 'parent' => $parent]) : trans('commands.config:set.confirm', ['setting' => $setting]); if ($this->confirm($message)) { return $this->erase($setting, $parent) ? 0 : 1; } return 3; } $value = $this->juggleType($value); // handle appending to arrays if (Str::endsWith($setting, '.+')) { $setting = substr($setting, 0, -2); $sub_data = Config::get($setting, []); if (! is_array($sub_data)) { $this->error(trans('commands.config:set.errors.append')); return 2; } array_push($sub_data, $value); $value = $sub_data; } // handle setting value inside multi-dimensional array if ($parent && $parent !== $setting) { $parent_data = Config::get($parent); Arr::set($parent_data, $this->getChildPath($setting, $parent), $value); $value = $parent_data; $setting = $parent; } $configItem = $definition->get($setting); if (! $force && empty($os) // if os is set, value was already validated against os config && ! $configItem->checkValue($value) ) { $message = ($configItem->type || $configItem->validate) ? $configItem->getValidationMessage($value) : trans('commands.config:set.errors.no-validation', ['setting' => $setting]); $this->error($message); return 2; } if (Config::persist($setting, $value)) { return 0; } $this->error(trans('commands.config:set.errors.failed', ['setting' => $setting])); return 1; } /** * Convert the string input into the appropriate PHP native type * * @return mixed */ private function juggleType(?string $value) { $json = json_decode($value, true); return json_last_error() ? $value : $json; } private function findParentSetting(DynamicConfig $definition, $setting): ?string { $parts = explode('.', $setting); array_pop($parts); // looking for parent, not this setting while (! empty($parts)) { $name = implode('.', $parts); if ($definition->isValidSetting($name)) { return $name; } array_pop($parts); } return null; } private function erase($setting, $parent = null) { if ($parent) { $data = Config::get($parent); if (preg_match("/^$parent\.?(?.+)\\.(?\\d+)\$/", $setting, $matches)) { // nested inside the parent setting, update just the required part $sub_data = Arr::get($data, $matches['sub']); $this->forgetWithIndex($sub_data, $matches['index']); Arr::set($data, $matches['sub'], $sub_data); } else { // not nested, just forget the setting $this->forgetWithIndex($data, $this->getChildPath($setting, $parent)); } return Config::persist($parent, $data); } return Config::erase($setting); } private function getChildPath($setting, $parent = null): string { return ltrim(Str::after($setting, $parent), '.'); } private function hasSequentialIndex($array): bool { if (! is_array($array) || $array === []) { return false; } return array_keys($array) === range(0, count($array) - 1); } private function forgetWithIndex(&$data, $matches) { // detect sequentially numeric indexed array so we can re-index the array if ($this->hasSequentialIndex($data)) { array_splice($data, (int) $matches, 1); } else { Arr::forget($data, $matches); } } /** * @param string $os * @param string $setting * @param mixed $value * * @throws \JsonSchema\Exception\ValidationException */ private function validateOsSetting(string $os, string $setting, $value) { // prep data to be validated OS::loadDefinition($os); $os_data = \LibreNMS\Config::get("os.$os"); if ($os_data === null) { throw new ValidationException(trans('commands.config:set.errors.invalid_os', ['os' => $os])); } $value = $this->juggleType($value); // append value if requested if (Str::endsWith($setting, '.+')) { $setting = substr($setting, 0, -2); $container = Arr::get($os_data, $setting, []); $container[] = $value; $value = $container; } Arr::set($os_data, $setting, $value); unset($os_data['definition_loaded']); $validator = new Validator; $validator->validate( $os_data, (object) ['$ref' => 'file://' . base_path('/misc/os_schema.json')], Constraint::CHECK_MODE_TYPE_CAST ); $code = 0; $errors = collect($validator->getErrors())->filter(function ($error) use ($value, &$code) { if ($error['constraint'] == 'additionalProp') { $code = 1; return true; } // only check type if value is set (otherwise we are unsetting it) if (! empty($value) && $error['constraint'] == 'type') { if ($code === 0) { $code = 2; // wrong path takes precedence over wrong type } return true; } return false; }); if ($errors->isNotEmpty()) { throw new ValidationException($errors->pluck('message')->implode(PHP_EOL), $code); } } }