chg: [instance:settings] Added notice if setting have issues

pull/70/head
mokaddem 2021-07-20 11:54:55 +02:00
parent f2d5c65fed
commit d501969c1d
5 changed files with 263 additions and 39 deletions

View File

@ -105,8 +105,9 @@ class InstanceController extends AppController
public function settings()
{
$this->Settings = $this->getTableLocator()->get('Settings');
$all = $this->Settings->getSettings();
$all = $this->Settings->getSettings(true);
$this->set('settingsProvider', $all['settingsProvider']);
$this->set('settings', $all['settings']);
$this->set('notices', $all['notices']);
}
}

View File

@ -7,12 +7,20 @@ use Cake\Validation\Validator;
class SettingsProviderTable extends AppTable
{
private $settingsConfiguration = [];
private $error_critical = '',
$error_warning = '',
$error_info = '';
private $severities = ['info', 'warning', 'critical'];
public function initialize(array $config): void
{
parent::initialize($config);
$this->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();
}
/**
@ -28,26 +36,101 @@ class SettingsProviderTable extends AppTable
}
return $settingConf;
}
private function mergeSettingsIntoSettingConfiguration($settingConf, $settings)
/**
* 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): array
{
foreach ($settingConf as $key => $value) {
if ($this->isLeaf($value)) {
if (isset($settings[$key])) {
$settingConf[$key]['value'] = $settings[$key];
}
$settingConf[$key] = $this->evaluateLeaf($settingConf[$key], $settingConf);
} else {
$settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings);
}
}
return $settingConf;
}
/**
* 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 (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);
} 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
@ -60,17 +143,17 @@ class SettingsProviderTable extends AppTable
'Application' => [
'General' => [
'Essentials' => [
'baseurl' => [
'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.'),
'errorMessage' => __('The currently set baseurl does not match the URL through which you have accessed the page. Disregard this if you are accessing the page via an alternate URL (for example via IP address).'),
'severity' => 'critical',
'default' => '',
'name' => __('Base URL'),
'test' => 'testBaseURL',
'type' => 'string',
],
'uuid' => [
'app.uuid' => [
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
'errorMessage' => __('No valid UUID set'),
'severity' => 'critical',
'default' => '',
'name' => 'UUID',
'test' => 'testUuid',
@ -81,8 +164,11 @@ class SettingsProviderTable extends AppTable
'to-del' => [
'description' => 'to del',
'errorMessage' => 'to del',
'default' => '',
'default' => 'A-default-value',
'name' => 'To DEL',
'test' => function ($value) {
return empty($value) ? __('Oh not! it\'s not valid!') : '';
},
'type' => 'string'
],
'to-del2' => [
@ -110,32 +196,34 @@ class SettingsProviderTable extends AppTable
],
'Network' => [
'Proxy' => [
'host' => [
'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',
],
'port' => [
'proxy.port' => [
'description' => __('The TCP port for the HTTP proxy.'),
'default' => '',
'name' => __('Port'),
'test' => 'testForRangeXY',
'type' => 'integer',
],
'user' => [
'proxy.user' => [
'description' => __('The authentication username for the HTTP proxy.'),
'default' => '',
'default' => 'admin',
'name' => __('User'),
'test' => 'testForEmpty',
'test' => 'testEmptyBecomesDefault',
'dependsOn' => 'proxy.host',
'type' => 'string',
],
'password' => [
'proxy.password' => [
'description' => __('The authentication password for the HTTP proxy.'),
'default' => '',
'name' => __('Password'),
'test' => 'testForEmpty',
'test' => 'testEmptyBecomesDefault',
'dependsOn' => 'proxy.host',
'type' => 'string',
],
],
@ -158,20 +246,20 @@ class SettingsProviderTable extends AppTable
'description' => __('The authentication username for the HTTP proxy.'),
'default' => '',
'name' => __('User'),
'test' => 'testForEmpty',
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
'password' => [
'description' => __('The authentication password for the HTTP proxy.'),
'default' => '',
'name' => __('Password'),
'test' => 'testForEmpty',
'test' => 'testEmptyBecomesDefault',
'type' => 'string',
],
],
],
'UI' => [
'dark' => [
'app.ui.dark' => [
'description' => __('Enable the dark theme of the application'),
'default' => false,
'name' => __('Dark theme'),
@ -179,11 +267,48 @@ class SettingsProviderTable extends AppTable
],
],
],
'Features' => [
],
'Security' => [
],
'Features' => [
],
];
}
}
class SettingValidator
{
public function testEmptyBecomesDefault($value, $setting)
{
if (!empty($value)) {
return true;
} else if (!empty($setting['default'])) {
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;
}
}

View File

@ -16,13 +16,19 @@ class SettingsTable extends AppTable
$this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider');
}
public function getSettings(): array
public function getSettings($full=false): array
{
$settings = Configure::read()['Cerebrate'];
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
return [
'settings' => $settings,
'settingsProvider' => $settingsProvider
];
if (empty($full)) {
return $settings;
} else {
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
$notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings);
return [
'settings' => $settings,
'settingsProvider' => $settingsProvider,
'notices' => $notices,
];
}
}
}

View File

@ -1,9 +1,73 @@
<?php
// debug($settings);
// debug($settingsProvider);
// debug($notices);
$mainNoticeHeading = [
'critical' => __('Your Cerebrate instance requires immediate attention.'),
'warning' => __('Issues found, it is recommended that you resolve them.'),
'info' => __('There are some optional settings that are incorrect or not set.'),
];
$noticeDescriptionPerLevel = [
'critical' => __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.'),
'warning' => __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.'),
'info' => __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.'),
];
$headingPerLevel = [
'critical' => __('Critical settings'),
'warning' => __('Warning settings'),
'info' => __('Info settings'),
];
$settingTable = genLevel0($settingsProvider, $this);
$alertVariant = 'info';
$alertBody = '';
$skipHeading = false;
$tableItems = [];
foreach (array_keys($mainNoticeHeading) as $level) {
if(!empty($notices[$level])) {
$variant = $level == 'critical' ? 'danger' : $level;
if (!$skipHeading) {
$alertBody .= sprintf('<h5 class="alert-heading">%s</h5>', $mainNoticeHeading[$level]);
$alertVariant = $variant;
$skipHeading = true;
}
$tableItems[] = [
'severity' => $headingPerLevel[$level],
'issues' => count($notices[$level]),
'badge-variant' => $variant,
'description' => $noticeDescriptionPerLevel[$level],
];
}
}
$alertBody .= $this->Bootstrap->table([
'small' => true,
'striped' => false,
'hover' => false,
'borderless' => true,
'bordered' => false,
'tableClass' => 'mb-0'
], [
'fields' => [
['key' => 'severity', 'label' => __('Severity')],
['key' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
return $this->Bootstrap->badge([
'variant' => $row['badge-variant'],
'text' => $count
]);
}],
['key' => 'description', 'label' => __('Description')]
],
'items' => $tableItems,
]);
$settingNotice = $this->Bootstrap->alert([
'variant' => $alertVariant,
'html' => $alertBody
]);
?>
<div class="px-5">
<div class="">
<?= $settingNotice ?>
</div>
<div class="mb-3">
<input class="form-control" type="text" id="search" placeholder="<?= __('Search settings') ?>" aria-describedby="<?= __('Search setting input') ?>">
</div>
@ -28,7 +92,7 @@ function genLevel0($settingsProvider, $appView)
'card' => false,
'pills' => false,
'justify' => 'center',
'content-class' => ['mt-2'],
'content-class' => [''],
'data' => [
'navs' => $level0,
'content' => $content0
@ -100,10 +164,21 @@ function genLevel3($level2Name, $settingGroupName, $setting, $appView)
function genSingleSetting($settingName, $setting, $appView)
{
$dependsOnHtml = '';
if (!empty($setting['dependsOn'])) {
$dependsOnHtml = $appView->Bootstrap->genNode('span', [
], $appView->Bootstrap->genNode('sup', [
'class' => [
$appView->FontAwesome->getClass('info'),
'ml-1',
],
'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn']))
]));
}
$label = $appView->Bootstrap->genNode('label', [
'class' => ['font-weight-bolder', 'mb-0'],
'for' => $settingName
], h($setting['name']));
], h($setting['name']) . $dependsOnHtml);
$description = '';
if (!empty($setting['description'])) {
$description = $appView->Bootstrap->genNode('small', [
@ -111,7 +186,16 @@ function genSingleSetting($settingName, $setting, $appView)
'id' => "{$settingName}Help"
], h($setting['description']));
}
$inputGroup = '';
$error = '';
if (!empty($setting['error'])) {
$textColor = '';
if ($setting['severity'] != 'critical') {
$textColor = "text-{$setting['severity']}";
}
$error = $appView->Bootstrap->genNode('div', [
'class' => ['d-block', 'invalid-feedback', $textColor],
], h($setting['errorMessage']));
}
if (empty($setting['type'])) {
$setting['type'] = 'string';
}
@ -131,19 +215,23 @@ function genSingleSetting($settingName, $setting, $appView)
}
$container = $appView->Bootstrap->genNode('div', [
'class' => ['form-group', 'mb-2']
], implode('', [$label, $input, $description]));
], implode('', [$label, $input, $description, $error]));
return $container;
}
function genInputString($settingName, $setting, $appView)
{
// debug($setting);
return $appView->Bootstrap->genNode('input', [
'class' => [
'form-control'
'form-control',
(!empty($setting['error']) ? 'is-invalid' : ''),
(!empty($setting['error']) ? ($setting['severity'] != 'critical' ? "border-{$setting['severity']} warning" : '') : ''),
],
'type' => 'text',
'id' => $settingName,
'value' => isset($setting['value']) ? $setting['value'] : "",
'placeholder' => $setting['default'] ?? '',
'aria-describedby' => "{$settingName}Help"
]);
}

View File

@ -109,31 +109,35 @@ div.progress-timeline .progress-line.progress-inactive {
opacity: 0.5;
}
.bd-callout {
.callout {
border: 1px solid #eee;
border-left-color: rgb(238, 238, 238);
border-left-width: 1px;
border-left-width: .25rem;
border-radius: .25rem;
}
.bd-callout-primary {
.callout-primary {
border-left-color: var(--primary);
}
.bd-callout-info {
.callout-info {
border-left-color: var(--info);
}
.bd-callout-success {
.callout-success {
border-left-color: var(--success);
}
.bd-callout-warning {
.callout-warning {
border-left-color: var(--warning);
}
.bd-callout-danger {
.callout-danger {
border-left-color: var(--danger);
}
.bd-callout-dark {
.callout-dark {
border-left-color: var(--dark);
}
.bd-callout-light {
.callout-light {
border-left-color: var(--light);
}
.form-control.is-invalid.warning {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e")
}