Merge branch 'feature-metafield-dropdown' into develop-unstable

pull/121/head
Sami Mokaddem 2022-11-15 11:29:07 +01:00
commit 410c27aa35
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
21 changed files with 342 additions and 53 deletions

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
use Phinx\Db\Adapter\MysqlAdapter;
class MetaFieldSaneDefault extends AbstractMigration
{
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change()
{
$exists = $this->table('meta_template_fields')->hasColumn('sane_default');
if (!$exists) {
$this->table('meta_template_fields')
->addColumn('sane_default', 'text', [
'default' => null,
'null' => true,
'limit' => MysqlAdapter::TEXT_LONG,
'comment' => 'List of sane default values to be proposed',
])
->addColumn('values_list', 'text', [
'default' => null,
'null' => true,
'limit' => MysqlAdapter::TEXT_LONG,
'comment' => 'List of values that have to be used',
])
->update();
}
}
}

View File

@ -179,6 +179,7 @@ class IncomingConnectionRequestProcessor extends LocalToolInboxProcessor impleme
public function __construct() {
parent::__construct();
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase I of inter-connection when another cerebrate instance performs the request.');
}
@ -291,6 +292,7 @@ class AcceptedRequestProcessor extends LocalToolInboxProcessor implements Generi
public function __construct() {
parent::__construct();
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase II of inter-connection when initial request has been accepted by the remote cerebrate.');
}
@ -367,6 +369,7 @@ class DeclinedRequestProcessor extends LocalToolInboxProcessor implements Generi
public function __construct() {
parent::__construct();
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase II of MISP inter-connection when initial request has been declined by the remote cerebrate.');
}

View File

@ -28,6 +28,7 @@ class DataExchangeProcessor extends SynchronisationInboxProcessor implements Gen
public function __construct() {
parent::__construct();
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle exchange of data between two cerebrate instances');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}

View File

@ -31,6 +31,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
public function __construct() {
parent::__construct();
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle user account for this cerebrate instance');
}

View File

@ -70,6 +70,7 @@ class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements Gene
public function __construct() {
parent::__construct();
$this->description = __('Handle re-sending messages that failed to be received from other cerebrate instances.');
$this->severity = $this->Inbox::SEVERITY_WARNING;
$this->Broods = TableRegistry::getTableLocator()->get('Broods');
$this->LocalTools = \Cake\ORM\TableRegistry::getTableLocator()->get('LocalTools');
}

View File

@ -20,9 +20,14 @@ class GenericOutboxProcessor
protected $validator;
protected $processingTemplate = '/genericTemplates/confirm';
protected $processingTemplatesDirectory = ROOT . '/libraries/default/OutboxProcessors/templates';
protected $defaultSeverity;
protected $severity;
public function __construct($registerActions=false) {
$this->Outbox = TableRegistry::getTableLocator()->get('Outbox');
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
$this->defaultSeverity = $this->Inbox::SEVERITY_INFO;
if ($registerActions) {
$this->registerActionInProcessor();
}
@ -55,6 +60,10 @@ class GenericOutboxProcessor
{
return $this->description ?? '';
}
public function getSeverity()
{
return $this->severity ?? $this->defaultSeverity;
}
protected function getProcessingTemplatePath()
{
@ -77,8 +86,9 @@ class GenericOutboxProcessor
$builder = new ViewBuilder();
$builder->disableAutoLayout()
->setClassName('Monad')
->setTemplate($processingTemplate);
$view = $builder->build($viewVariables);
->setTemplate($processingTemplate)
->setVars($viewVariables);
$view = $builder->build();
$view->setRequest($serverRequest);
return $view->render();
}
@ -193,6 +203,7 @@ class GenericOutboxProcessor
$user_id = Router::getRequest()->getSession()->read('Auth.id');
$requestData['scope'] = $this->scope;
$requestData['action'] = $this->action;
$requestData['severity'] = $this->getSeverity();
$requestData['user_id'] = $user_id;
$request = $this->generateRequest($requestData);
$savedRequest = $this->Outbox->createEntry($request);

View File

@ -8,11 +8,10 @@ use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Core\Configure;
use PhpParser\Node\Stmt\Echo_;
class AuditLogsController extends AppController
{
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title'];
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title', 'AuditLogs.created'];
public $quickFilterFields = ['model', 'request_action', 'model_title'];
public $containFields = ['Users'];

View File

@ -115,7 +115,7 @@ class CRUDComponent extends Component
if ($this->metaFieldsSupported()) {
$query = $this->includeRequestedMetaFields($query);
}
$query = $this->setRequestedEntryAmount($query);
$this->setRequestedEntryAmount();
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
if (isset($options['afterFind'])) {
$function = $options['afterFind'];

View File

@ -16,7 +16,7 @@ use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
public $filterFields = ['scope', 'action', 'Inbox.created', 'title', 'origin', 'message', 'Users.id', 'Users.username',];
public $filterFields = ['scope', 'action', 'Inbox.created', 'severity', 'title', 'origin', 'message', 'Users.id', 'Users.username',];
public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin'];
public $containFields = ['Users'];
@ -43,9 +43,94 @@ class InboxController extends AppController
[
'default' => true,
'label' => __('My Notifications'),
'filterConditionFunction' => function ($query) {
return $query->where(function(QueryExpression $exp) {
return $exp->or(['user_id' => $this->ACL->getUser()['id']])
->isNull('user_id');
});
}
],
[
'label' => __('User Registration'),
'filterConditionFunction' => function ($query) {
return $query->where([
'user_id' => $this->ACL->getUser()['id'],
'scope' => 'User',
'action' => 'Registration',
]);
}
],
[
'label' => __('Inter-connection Requests'),
'filterConditionFunction' => function ($query) {
return $query->where([
'scope' => 'LocalTool',
'action IN' => ['IncomingConnectionRequest', 'AcceptedRequest', 'DeclinedRequest'],
]);
}
],
[
'label' => __('Data changed'),
'filterConditionFunction' => function ($query) {
return $query->where([
'user_id' => $this->ACL->getUser()['id'], // Each admin get a message about data changes
'scope' => 'Notification',
'action' => 'DataChange',
]);
}
],
[
'label' => 'severity:primary',
'viewElement' => 'bootstrapUI',
'viewElementParams' => [
'element' => 'badge',
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
],
'filterConditionFunction' => function ($query) {
return $query->where([
'severity' => $this->Inbox::SEVERITY_PRIMARY,
]);
}
],
[
'label' => 'severity:info',
'viewElement' => 'bootstrapUI',
'viewElementParams' => [
'element' => 'badge',
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
],
'filterConditionFunction' => function ($query) {
return $query->where([
'severity' => $this->Inbox::SEVERITY_INFO,
]);
}
],
[
'label' => 'severity:warning',
'viewElement' => 'bootstrapUI',
'viewElementParams' => [
'element' => 'badge',
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
],
'filterConditionFunction' => function ($query) {
return $query->where([
'severity' => $this->Inbox::SEVERITY_WARNING,
]);
}
],
[
'label' => 'severity:danger',
'viewElement' => 'bootstrapUI',
'viewElementParams' => [
'element' => 'badge',
'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
],
'filterConditionFunction' => function ($query) {
return $query->where([
'severity' => $this->Inbox::SEVERITY_DANGER,
]);
}
],

View File

@ -7,5 +7,32 @@ use Cake\ORM\Entity;
class MetaTemplateField extends AppModel
{
protected $_virtual = ['form_type', 'form_options', ];
protected function _getFormType()
{
$formType = 'text';
if (!empty($this->sane_default) || !empty($this->values_list)) {
$formType = 'dropdown';
} else if ($this->type === 'boolean') {
$formType = 'checkbox';
}
return $formType;
}
protected function _getFormOptions()
{
$formOptions = [];
if ($this->formType === 'dropdown') {
$selectOptions = !empty($this->sane_default) ? $this->sane_default : $this->values_list;
$selectOptions = array_combine($selectOptions, $selectOptions);
if (!empty($this->sane_default)) {
$selectOptions[] = ['value' => '_custom', 'text' => __('-- custom value --'), 'class' => 'custom-value'];
}
$selectOptions[''] = __('-- no value --');
$formOptions['options'] = $selectOptions;
}
return $formOptions;
}
}

View File

@ -27,6 +27,8 @@ class MetaTemplateFieldsTable extends AppTable
$this->hasMany('MetaFields');
$this->setDisplayField('field');
$this->getSchema()->setColumnType('sane_default', 'json');
$this->getSchema()->setColumnType('values_list', 'json');
$this->loadTypeHandlers();
}

View File

@ -28,6 +28,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'request_ip',
'data_path' => 'request_ip',
],
[
'name' => 'created',
'sort' => 'created',
'data_path' => 'created',
'element' => 'datetime'
],
[
'name' => __('Username'),
'sort' => 'user.username',

View File

@ -30,6 +30,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'multiple',
'field' => 'textarea'
],
[
'name' => __('Sane defaults'),
'data_path' => 'sane_default'
],
[
'name' => __('Values List'),
'data_path' => 'values_list'
],
[
'name' => __('Validation regex'),
'sort' => 'regex',

View File

@ -0,0 +1,7 @@
<?php
if (!empty($element)) {
echo $this->Bootstrap->{$element}([
'text' => $text,
'variant' => $variant,
]);
}

View File

@ -1,14 +1,80 @@
<?php
$controlParams = [
'options' => $fieldData['options'],
'empty' => $fieldData['empty'] ?? false,
'value' => $fieldData['value'] ?? null,
'multiple' => $fieldData['multiple'] ?? false,
'disabled' => $fieldData['disabled'] ?? false,
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select',
'default' => ($fieldData['default'] ?? null)
];
if (!empty($fieldData['label'])) {
$controlParams['label'] = $fieldData['label'];
$seed = 's-' . mt_rand();
$controlParams = [
'type' => 'select',
'options' => $fieldData['options'],
'empty' => $fieldData['empty'] ?? false,
'value' => $fieldData['value'] ?? null,
'multiple' => $fieldData['multiple'] ?? false,
'disabled' => $fieldData['disabled'] ?? false,
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select',
'default' => $fieldData['default'] ?? '',
];
if (!empty($fieldData['field'])) { // used for multi meta-field form
$controlParams['field'] = $fieldData['field'];
}
if (!empty($fieldData['label'])) {
$controlParams['label'] = $fieldData['label'];
}
if ($controlParams['options'] instanceof \Cake\ORM\Query) {
$controlParams['options'] = $controlParams['options']->all()->toList();
}
if (in_array('_custom', array_keys($controlParams['options']))) {
$customInputValue = $this->Form->getSourceValue($fieldData['field']);
if (!in_array($customInputValue, $controlParams['options'])) {
$controlParams['options'] = array_map(function ($option) {
if (is_array($option) && $option['value'] == '_custom') {
$option[] = 'selected';
}
return $option;
}, $controlParams['options']);
} else {
$customInputValue = '';
}
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
$controlParams['class'] .= ' dropdown-custom-value' . "-$seed";
$adaptedField = $fieldData['field'] . '_custom';
$controlParams['templates']['formGroup'] = sprintf(
'<label class="col-sm-2 col-form-label form-label" {{attrs}}>{{label}}</label><div class="col-sm-10 multi-metafield-input-container"><div class="d-flex form-dropdown-with-freetext input-group">{{input}}{{error}}%s</div></div>',
sprintf('<input type="text" class="form-control custom-value" field="%s" value="%s">', h($adaptedField), h($customInputValue))
);
}
echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $fieldData);
?>
<script>
(function() {
$(document).ready(function() {
const $select = $('select.dropdown-custom-value-<?= $seed ?>')
toggleFreetextSelectField($select[0]);
$select.attr('onclick', 'toggleFreetextSelectField(this)')
$select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)')
updateAssociatedSelect($select.parent().find('input.custom-value')[0])
})
})()
function toggleFreetextSelectField(selectEl) {
const $select = $(selectEl)
const show = $select.find('option:selected').hasClass('custom-value')
const $container = $(selectEl).parent()
let $freetextInput = $container.find('input.custom-value')
if (show) {
$freetextInput.removeClass('d-none')
} else {
$freetextInput.addClass('d-none')
}
}
function updateAssociatedSelect(input) {
const $input = $(input)
const $select = $input.parent().find('select')
const $customOption = $select.find('option.custom-value')
$customOption.val($input.val())
}
</script>
<style>
form div.form-dropdown-with-freetext input.custom-value {
flex-grow: 3;
}
</style>

View File

@ -31,15 +31,16 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
$metaField = reset($metaTemplateField->metaFields);
$fieldData = [
'label' => $metaTemplateField->label,
'type' => $metaTemplateField->formType,
];
if (!empty($metaTemplateField->formOptions)) {
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
if (isset($metaField->id)) {
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id);
} else {
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
}
if ($metaTemplateField->type === 'boolean') {
$fieldData['type'] = 'checkbox';
}
$this->Form->setTemplates($backupTemplates);
$fieldsHtml .= $this->element(
'genericElements/Form/fieldScaffold',
@ -66,9 +67,10 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
$fieldData = [
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
'label' => $metaTemplateField->label,
'type' => $metaTemplateField->formType,
];
if ($metaTemplateField->type === 'boolean') {
$fieldData['type'] = 'checkbox';
if (!empty($metaTemplateField->formOptions)) {
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
$fieldsHtml .= $this->element(
'genericElements/Form/fieldScaffold',
@ -80,4 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
}
}
}
echo $fieldsHtml;
$fieldContainer = $this->Bootstrap->genNode('div', [
'class' => [],
], $fieldsHtml);
echo $fieldContainer;

View File

@ -45,6 +45,8 @@ $seed = 'mfb-' . mt_rand();
$clonedContainer
.removeClass('has-error')
.find('.error-message ').remove()
$clonedContainer
.find('label.form-label').text('')
const $clonedInput = $clonedContainer.find('input, select')
if ($clonedInput.length > 0) {
const injectedTemplateId = $clicked.closest('.multi-metafields-container').find('.new-metafield').length
@ -54,18 +56,25 @@ $seed = 'mfb-' . mt_rand();
}
}
function adjustClonedInputAttr($input, injectedTemplateId) {
let explodedPath = $input.attr('field').split('.').splice(0, 5)
explodedPath.push('new', injectedTemplateId)
dottedPathStr = explodedPath.join('.')
brackettedPathStr = explodedPath.map((elem, i) => {
return i == 0 ? elem : `[${elem}]`
}).join('')
$input.attr('id', dottedPathStr)
.attr('field', dottedPathStr)
.attr('name', brackettedPathStr)
.val('')
.removeClass('is-invalid')
function adjustClonedInputAttr($inputs, injectedTemplateId) {
$inputs.each(function() {
const $input = $(this)
let explodedPath = $input.attr('field').split('.').splice(0, 5)
explodedPath.push('new', injectedTemplateId)
dottedPathStr = explodedPath.join('.')
brackettedPathStr = explodedPath.map((elem, i) => {
return i == 0 ? elem : `[${elem}]`
}).join('')
const attrs = ['id', 'field', 'name']
attrs.forEach((attr) => {
if ($input.attr(attr) !== undefined) {
$input.attr(attr, attr === 'name' ? brackettedPathStr : dottedPathStr)
}
})
$input
.val('')
.removeClass('is-invalid')
})
}
})()
</script>

View File

@ -22,6 +22,7 @@ if (!empty($metaFieldsEntities)) {
$metaFieldsEntity->meta_template_field_id,
$metaFieldsEntity->id
),
'type' => $metaTemplateField->formType,
];
if($metaFieldsEntity->isNew()) {
$fieldData['field'] = sprintf(
@ -33,7 +34,10 @@ if (!empty($metaFieldsEntities)) {
$fieldData['class'] = 'new-metafield';
}
if ($labelPrintedOnce) { // Only the first input can have a label
$fieldData['label'] = false;
$fieldData['label'] = ['text' => ''];
}
if ($metaTemplateField->formType === 'dropdown') {
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
$labelPrintedOnce = true;
$fieldsHtml .= $this->element(
@ -49,14 +53,19 @@ if (!empty($metaTemplateField) && !empty($multiple)) { // Add multiple field but
$metaTemplateField->label = Inflector::humanize($metaTemplateField->field);
$emptyMetaFieldInput = '';
if (empty($metaFieldsEntities)) { // Include editable field for meta-template not containing a meta-field
$fieldData = [
'label' => $metaTemplateField->label,
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
'class' => 'new-metafield',
'type' => $metaTemplateField->formType,
];
if ($metaTemplateField->formType === 'dropdown') {
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
$emptyMetaFieldInput = $this->element(
'genericElements/Form/fieldScaffold',
[
'fieldData' => [
'label' => $metaTemplateField->label,
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
'class' => 'new-metafield',
],
'fieldData' => $fieldData,
'form' => $form,
]
);

View File

@ -21,7 +21,7 @@
} else {
$currentFilteringContext = $filteringContext['filterCondition'];
}
$contextArray[] = [
$contextItem = [
'active' => (
(
$currentQuery == $currentFilteringContext && // query conditions match
@ -42,9 +42,17 @@
"#table-container-${tableRandomValue}",
"#table-container-${tableRandomValue} table.table",
],
'text' => $filteringContext['label'],
'class' => 'btn-sm'
];
if (!empty($filteringContext['viewElement'])) {
$contextItem['html'] = $this->element(
$filteringContext['viewElement'],
$filteringContext['viewElementParams'] ?? []
);
} else {
$contextItem['text'] = $filteringContext['label'];
}
$contextArray[] = $contextItem;
}
$dataGroup = [

View File

@ -202,7 +202,7 @@ echo $this->Bootstrap->modal([
rowData['operator'] = $row.find('select.fieldOperator').val()
const $formElement = $row.find('.fieldValue');
if ($formElement.attr('type') === 'datetime-local') {
rowData['value'] = moment($formElement.val()).toISOString()
rowData['value'] = $formElement.val().length > 0 ? moment($formElement.val()).toISOString() : $formElement.val()
} else {
rowData['value'] = $formElement.val()
}

View File

@ -67,14 +67,21 @@ function saveAndUpdateSetting(statusNode, $input, settingName, settingValue) {
settingValue = JSON.stringify(settingValue)
}
saveSetting(statusNode, settingName, settingValue).then((result) => {
window.settingsFlattened[settingName] = result.data
if ($input.attr('type') == 'checkbox') {
$input.prop('checked', result.data.value == true)
} else {
$input.val(result.data.value)
}
updateSettingValue($input, settingName, result.data)
}).catch((e) => {
updateSettingValue($input, settingName, window.settingsFlattened[settingName])
}).finally(() => {
handleSettingValueChange($input)
}).catch((e) => { })
})
}
function updateSettingValue($input, settingName, settingValue) {
window.settingsFlattened[settingName] = settingValue
if ($input.attr('type') == 'checkbox') {
$input.prop('checked', settingValue.value == true)
} else {
$input.val(settingValue.value)
}
}
function handleSettingValueChange($input) {