settingsConfiguration = $this->generateSettingsConfiguration(); $this->setTable(false); $this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.'); $this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.'); $this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.'); $this->settingValidator = new SettingValidator(); } /** * getSettingsConfiguration Return the setting configuration and merge existing settings into it if provided * * @param null|array $settings - Settings to be merged in the provided setting configuration * @return array */ public function getSettingsConfiguration($settings = null) { $settingConf = $this->settingsConfiguration; if (!is_null($settings)) { $settingConf = $this->mergeSettingsIntoSettingConfiguration($settingConf, $settings); } return $settingConf; } /** * mergeSettingsIntoSettingConfiguration Inject the provided settings into the configuration while performing depencency and validation checks * * @param array $settingConf the setting configuration to have the setting injected into * @param array $settings the settings * @return void */ private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array { foreach ($settingConf as $key => $value) { if ($this->isLeaf($value)) { if (isset($settings[$key])) { $settingConf[$key]['value'] = $settings[$key]; } if (empty($settingConf[$key]['severity'])) { $settingConf[$key]['severity'] = 'warning'; } $settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf); $settingConf[$key]['setting-path'] = $path; $settingConf[$key]['true-name'] = $key; } else { $currentPath = empty($path) ? $key : sprintf('%s.%s', $path, $key); $settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings, $currentPath); } } return $settingConf; } public function flattenSettingsConfiguration(array $settingsProvider, $flattenedSettings=[]): array { foreach ($settingsProvider as $key => $value) { if ($this->isLeaf($value)) { $flattenedSettings[$key] = $value; } else { $flattenedSettings = $this->flattenSettingsConfiguration($value, $flattenedSettings); } } return $flattenedSettings; } /** * getNoticesFromSettingsConfiguration Summarize the validation errors * * @param array $settingsProvider the setting configuration having setting value assigned * @return void */ public function getNoticesFromSettingsConfiguration(array $settingsProvider): array { $notices = []; foreach ($settingsProvider as $key => $value) { if ($this->isLeaf($value)) { if (!empty($value['error'])) { if (empty($notices[$value['severity']])) { $notices[$value['severity']] = []; } $notices[$value['severity']][] = $key; } } else { $notices = array_merge($notices, $this->getNoticesFromSettingsConfiguration($value)); } } return $notices; } private function isLeaf($setting) { return !empty($setting['name']) && !empty($setting['type']); } private function evaluateLeaf($setting, $settingSection) { $skipValidation = false; if ($setting['type'] == 'select') { if (!empty($setting['options']) && is_callable($setting['options'])) { $setting['options'] = $setting['options']($this); } } if (isset($setting['dependsOn'])) { $parentSetting = null; foreach ($settingSection as $settingSectionName => $settingSectionConfig) { if ($settingSectionName == $setting['dependsOn']) { $parentSetting = $settingSectionConfig; } } if (!is_null($parentSetting)) { $parentSetting = $this->evaluateLeaf($parentSetting, $settingSection); $skipValidation = $parentSetting['error'] === true || empty($parentSetting['value']); } } if (!$skipValidation) { if (isset($setting['test'])) { $error = false; $setting['value'] = $setting['value'] ?? ''; if (is_callable($setting['test'])) { // Validate with anonymous function $error = $setting['test']($setting['value'], $setting, new Validator()); } else if (method_exists($this->settingValidator, $setting['test'])) { // Validate with function defined in settingValidator class $error = $this->settingValidator->{$setting['test']}($setting['value'], $setting); } else { $validator = new Validator(); if (method_exists($validator, $setting['test'])) { // Validate with cake's validator function $validator->{$setting['test']}; $error = $validator->validate($setting['value']); } } if ($error !== true) { $setting['severity'] = $setting['severity'] ?? 'warning'; if (!in_array($setting['severity'], $this->severities)) { $setting['severity'] = 'warning'; } $setting['errorMessage'] = $error; } $setting['error'] = $error !== false ? true : false; } } return $setting; } /** * Support up to 3 level: * Application -> Network -> Proxy -> Proxy.URL * * Leave errorMessage empty to let the validator generate the error message */ private function generateSettingsConfiguration() { return [ 'Application' => [ 'General' => [ 'Essentials' => [ 'app.baseurl' => [ 'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'), 'severity' => 'critical', 'default' => '', 'name' => __('Base URL'), 'test' => 'testBaseURL', 'type' => 'string', ], 'app.uuid' => [ 'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'), 'severity' => 'critical', 'default' => '', 'name' => 'UUID', 'test' => 'testUuid', 'type' => 'string' ], ], 'Miscellaneous' => [ 'to-del' => [ 'description' => 'to del', 'errorMessage' => 'to del', 'default' => 'A-default-value', 'name' => 'To DEL', 'test' => function ($value) { return empty($value) ? __('Oh not! it\'s not valid!') : ''; }, 'type' => 'string' ], 'sc2.hero' => [ 'description' => 'The true hero', 'default' => '', 'options' => [ 'Jim Raynor' => 'Jim Raynor', 'Sarah Kerrigan' => 'Sarah Kerrigan', 'Artanis' => 'Artanis', 'Zeratul' => 'Zeratul', ], 'name' => 'Hero', 'type' => 'select' ], 'sc2.antagonist' => [ 'description' => 'The real bad guy', 'default' => '', 'options' => function($settingsProviders) { return [ 'Amon' => 'Amon', 'Sarah Kerrigan' => 'Sarah Kerrigan', 'Narud' => 'Narud', ]; }, 'name' => 'Antagonist', 'type' => 'select' ], ], 'floating-setting' => [ 'description' => 'floaringSetting', 'errorMessage' => 'floaringSetting', 'default' => 'A default value', 'name' => 'Uncategorized Setting', 'test' => 'testEmptyBecomesDefault', 'type' => 'string' ], ], 'Network' => [ 'Proxy' => [ 'proxy.host' => [ 'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'), 'default' => '', 'name' => __('Host'), 'test' => 'testHostname', 'type' => 'string', ], 'proxy.port' => [ 'description' => __('The TCP port for the HTTP proxy.'), 'default' => '', 'name' => __('Port'), 'test' => 'testForRangeXY', 'type' => 'integer', ], 'proxy.user' => [ 'description' => __('The authentication username for the HTTP proxy.'), 'default' => 'admin', 'name' => __('User'), 'test' => 'testEmptyBecomesDefault', 'dependsOn' => 'proxy.host', 'type' => 'string', ], 'proxy.password' => [ 'description' => __('The authentication password for the HTTP proxy.'), 'default' => '', 'name' => __('Password'), 'test' => 'testEmptyBecomesDefault', 'dependsOn' => 'proxy.host', 'type' => 'string', ], ], ], 'UI' => [ 'General' => [ 'app.ui.dark' => [ 'description' => __('Enable the dark theme of the application'), 'default' => false, 'name' => __('Dark theme'), 'test' => function() { return 'Fake error'; }, 'type' => 'boolean', ], ], ], ], 'Security' => [ 'Development' => [ 'Debugging' => [ 'Debug' => [ 'description' => __('The debug level of the instance'), 'default' => 0, 'dependsOn' => 'host', 'name' => __('User'), 'test' => function($value, $setting, $validator) { $validator->range('value', [0, 3]); return testValidator($value, $validator); }, 'type' => 'select', 'options' => [ 0 => __('Debug Off'), 1 => __('Debug On'), 2 => __('Debug On + SQL Dump'), ] ], ], ] ], 'Features' => [ ], ]; } } function testValidator($value, $validator) { $errors = $validator->validate(['value' => $value]); return !empty($errors) ? implode(', ', $errors['value']) : true; } class SettingValidator { public function testEmptyBecomesDefault($value, &$setting) { if (!empty($value)) { return true; } else if (!empty($setting['default'])) { $setting['severity'] = 'info'; return __('Setting is not set, fallback to default value: {0}', $setting['default']); } else { return __('Cannot be empty'); } } public function testForEmpty($value, &$setting) { return !empty($value) ? true : __('Cannot be empty'); } public function testBaseURL($value, &$setting) { if (empty($value)) { return __('Cannot be empty'); } if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) { return __('Invalid URL, please make sure that the protocol is set.'); } return true; } public function testUuid($value, &$setting) { if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) { return __('Invalid UUID.'); } return true; } }