diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 2c4704d..14f4c4f 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -184,7 +184,7 @@ class CRUDComponent extends Component $this->Controller->set('entity', $data); } - private function prepareValidationMessage($errors) + public function prepareValidationMessage($errors) { $validationMessage = ''; if (!empty($errors)) { @@ -280,8 +280,9 @@ class CRUDComponent extends Component $validationErrors = $data->getErrors(); $validationMessage = $this->prepareValidationMessage($validationErrors); $message = __( - __('{0} could not be modified.'), - $this->ObjectAlias + '{0} could not be modified.{1}', + $this->ObjectAlias, + empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php index fe5a384..73e3f83 100644 --- a/src/Controller/LocalToolsController.php +++ b/src/Controller/LocalToolsController.php @@ -152,9 +152,6 @@ class LocalToolsController extends AppController if (!empty($responsePayload)) { return $responsePayload; } - if ($this->ParamHandler->isAjax() && !empty($this->ajaxResponsePayload)) { - return $this->ajaxResponsePayload; - } $localConnectors = $this->LocalTools->extractMeta($this->LocalTools->getConnectors()); $dropdownData = ['connectors' => []]; $connector = false; diff --git a/src/Lib/default/local_tool_connectors/MispConnector.php b/src/Lib/default/local_tool_connectors/MispConnector.php index 5d95b45..940db8b 100644 --- a/src/Lib/default/local_tool_connectors/MispConnector.php +++ b/src/Lib/default/local_tool_connectors/MispConnector.php @@ -97,26 +97,29 @@ class MispConnector extends CommonConnectorTools ] ]; public $version = '0.1'; - public $parameters = [ + public $settings = [ 'url' => [ - 'required' => true, - // 'validation' => 'url', + 'type' => 'text' ], 'authkey' => [ - 'required' => true, - // 'validation' => 'authkeyLength', + 'type' => 'text' ], 'skip_ssl' => [ - // 'validation' => 'boolean', + 'type' => 'boolean' ], - 'test_param' => [ - 'options' => [ - 'John', - 'Doe' - ] - ] ]; + public function addSettingValidatorRules($validator) + { + return $validator + ->requirePresence('url') + ->notEmpty('url', __('An URL must be provided')) + ->requirePresence('authkey') + ->notEmpty('authkey', __('An Authkey must be provided')) + ->lengthBetween('authkey', [40, 40], __('The authkey must be 40 character long')) + ->boolean('skip_ssl'); + } + public function addExposedFunction(string $functionName): void { $this->exposedFunctions[] = $functionName; diff --git a/src/Model/Table/LocalToolsTable.php b/src/Model/Table/LocalToolsTable.php index 8bbe098..d061ba3 100644 --- a/src/Model/Table/LocalToolsTable.php +++ b/src/Model/Table/LocalToolsTable.php @@ -4,6 +4,7 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; +use Cake\ORM\RulesChecker; use Cake\Validation\Validator; use Migrations\Migrations; use Cake\Filesystem\Folder; @@ -33,7 +34,14 @@ class LocalToolsTable extends AppTable public function validationDefault(Validator $validator): Validator { - return $validator; + return $validator->add('settings', 'validSettings', [ + 'rule' => 'isValidSettings', + 'provider' => 'table', + 'message' => __('Invalid settings'), + 'on' => function ($context) { + return !empty($context['data']['settings']); + } + ]); } public function loadConnector(string $connectorName): void @@ -133,7 +141,7 @@ class LocalToolsTable extends AppTable 'connector' => $connector_type, 'connector_version' => $connector_class->version, 'connector_description' => $connector_class->description, - 'connector_parameters' => $connector_class->parameters ?? [] + 'connector_settings' => $connector_class->settings ?? [] ]; if ($includeConnections) { $connector['connections'] = $this->healthCheck($connector_type, $connector_class); @@ -298,4 +306,32 @@ class LocalToolsTable extends AppTable } return $connection; } + + public function isValidSettings($settings, array $context) + { + $settings = json_decode($settings, true); + $validationErrors = $this->getLocalToolsSettingValidationErrors($context['data']['id'], $settings); + return $this->getValidationMessage($validationErrors); + } + + public function getValidationMessage($validationErrors) + { + $messages = []; + foreach ($validationErrors as $key => $errors) { + $messages[] = sprintf('%s: %s', $key, implode(', ', $errors)); + } + return empty($messages) ? true : implode('; ', $messages); + } + + public function getLocalToolsSettingValidationErrors($connectionId, array $settings): array + { + $connector = array_values($this->getConnectorByConnectionId($connectionId))[0]; + $errors = []; + if (method_exists($connector, 'addSettingValidatorRules')) { + $validator = new Validator(); + $validator = $connector->addSettingValidatorRules($validator); + $errors = $validator->validate($settings); + } + return $errors; + } } diff --git a/templates/LocalTools/add.php b/templates/LocalTools/add.php index 02aa7d9..184f0e6 100644 --- a/templates/LocalTools/add.php +++ b/templates/LocalTools/add.php @@ -21,7 +21,7 @@ 'type' => 'codemirror', 'codemirror' => [ 'height' => '10rem', - 'hints' => $connectors[0]['connector_parameters'] + 'hints' => $connectors[0]['connector_settings'] ] ], [ diff --git a/templates/element/genericElements/Form/Fields/codemirrorField.php b/templates/element/genericElements/Form/Fields/codemirrorField.php index b5f9c00..1ca79ab 100644 --- a/templates/element/genericElements/Form/Fields/codemirrorField.php +++ b/templates/element/genericElements/Form/Fields/codemirrorField.php @@ -1,9 +1,4 @@ FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData); echo $this->element('genericElements/codemirror', [ 'data' => $fieldData, 'params' => $params, diff --git a/templates/element/genericElements/codemirror.php b/templates/element/genericElements/codemirror.php index f52ff25..dd2ad65 100644 --- a/templates/element/genericElements/codemirror.php +++ b/templates/element/genericElements/codemirror.php @@ -68,9 +68,23 @@ } cm.save() }) + registerObserver(cm, $textArea[0]) + synchronizeClasses(cm, $textArea[0]) postInit() } + // Used to synchronize textarea classes (such as `is-invalid` for content validation) + function registerObserver(cm, textarea) { + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.attributeName == 'class') { + synchronizeClasses(cm, textarea) + } + }); + }); + observer.observe(textarea, {attributes: true}) + } + function postInit() { if ($(cm.getInputField()).closest('.modal').length) { // editor is in modal setTimeout(() => { @@ -79,6 +93,14 @@ } } + function synchronizeClasses(cm, textarea) { + const classes = Array.from(textarea.classList).filter(c => !c.startsWith('area-for-codemirror')) + const $wrapper = $(cm.getWrapperElement()) + classes.forEach(c => { + $wrapper.toggleClass(c) + }); + } + function jsonHints() { const cur = cm.getCursor() const token = cm.getTokenAt(cur) @@ -130,7 +152,7 @@ if (str.length > 0) { let validHints = [] let hint - for (let i = 0; hints.length < cmOptions.hintOptions.maxHints && i < validHints.length; i++) { + for (let i = 0; validHints.length < cmOptions.hintOptions.maxHints && i < hints.length; i++) { if (hints[i].text.startsWith(str)) { validHints.push(hints[i]) } diff --git a/webroot/css/CodeMirror/codemirror-additional.css b/webroot/css/CodeMirror/codemirror-additional.css index 4c0ae6e..0c62e18 100644 --- a/webroot/css/CodeMirror/codemirror-additional.css +++ b/webroot/css/CodeMirror/codemirror-additional.css @@ -6,3 +6,24 @@ .CodeMirror-gutters { border-radius: 0.25rem 0 0 0.25rem; } + +.CodeMirror.is-invalid { + border-color: #dc3545; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.CodeMirror.is-invalid .CodeMirror-gutters { + /* background-color: #dc3545 !important; */ +} + +.CodeMirror.is-invalid .CodeMirror-linenumber { + /* color: white !important; */ +} + +.CodeMirror-hints { + z-index: 1060 !important; /* Make sure hint is above modal */ +} \ No newline at end of file