From ab2ab4e8f8233e82e567f1d60697f4cbb5fd3518 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 22 Feb 2023 00:37:34 +0100 Subject: [PATCH 01/34] chg: [organisations] nationality field renamed to country - UI display only so far - want to maintain alignment with MISP, might change in the future - filtering still calls it nationality - API still calls it nationality --- templates/Open/Organisations/index.php | 2 +- templates/Organisations/add.php | 1 + templates/Organisations/index.php | 2 +- templates/Organisations/view.php | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/Open/Organisations/index.php b/templates/Open/Organisations/index.php index fa6fcab..18be544 100644 --- a/templates/Open/Organisations/index.php +++ b/templates/Open/Organisations/index.php @@ -57,7 +57,7 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data_path' => 'url', ], [ - 'name' => __('Nationality'), + 'name' => __('Country'), 'data_path' => 'nationality', ], [ diff --git a/templates/Organisations/add.php b/templates/Organisations/add.php index 72b9a9e..787f9a4 100644 --- a/templates/Organisations/add.php +++ b/templates/Organisations/add.php @@ -16,6 +16,7 @@ 'field' => 'url' ), array( + 'label' => __('Country'), 'field' => 'nationality' ), array( diff --git a/templates/Organisations/index.php b/templates/Organisations/index.php index 34e4a10..2347b04 100644 --- a/templates/Organisations/index.php +++ b/templates/Organisations/index.php @@ -66,7 +66,7 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data_path' => 'url', ], [ - 'name' => __('Nationality'), + 'name' => __('Country'), 'data_path' => 'nationality', 'sort' => 'nationality', ], diff --git a/templates/Organisations/view.php b/templates/Organisations/view.php index 8ffbef8..e393c6f 100644 --- a/templates/Organisations/view.php +++ b/templates/Organisations/view.php @@ -22,7 +22,7 @@ echo $this->element( 'path' => 'url' ], [ - 'key' => __('Nationality'), + 'key' => __('Country'), 'path' => 'nationality' ], [ From fdd876b1b2e7917da260f5a9990a081500685876 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 11:40:07 +0100 Subject: [PATCH 02/34] new: [component:CRUD] Added support of IN condition when filtering index --- src/Controller/Component/CRUDComponent.php | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 39554d4..8f19c23 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -10,6 +10,7 @@ use Cake\Utility\Text; use Cake\View\ViewBuilder; use Cake\ORM\TableRegistry; use Cake\ORM\Query; +use Cake\Database\Expression\QueryExpression; use Cake\Routing\Router; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\NotFoundException; @@ -1225,7 +1226,7 @@ class CRUDComponent extends Component } $activeFilters[$filter] = $filterValue; if (is_array($filterValue)) { - $query->where([($filter . ' IN') => $filterValue]); + $query = $this->setInCondition($query, $filter, $filterValue); } else { $query = $this->setValueCondition($query, $filter, $filterValue); } @@ -1311,6 +1312,27 @@ class CRUDComponent extends Component } } + protected function setInCondition($query, $fieldName, $values) + { + $split = explode(' ', $fieldName); + if (count($split) == 1) { + $field = $fieldName; + $operator = '='; + } else { + $field = $split[0]; + $operator = $split[1]; + } + if ($operator == '=') { + return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) { + return $exp->in($field, $values); + }); + } else if ($operator == '!=') { + return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) { + return $exp->notIn($field, $values); + }); + } + } + protected function setFilteringContext($contextFilters, $params) { $filteringContexts = []; From 4d4642770f97c908ea2d8e5d82d3814eb2681c06 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 12:55:18 +0100 Subject: [PATCH 03/34] new: [crud:filter] Added support of IN searches using dropdown --- src/Controller/Component/CRUDComponent.php | 31 +++++++-- .../Form/Fields/dropdownField.php | 12 ++-- templates/genericTemplates/filters.php | 66 +++++++++++++------ 3 files changed, 79 insertions(+), 30 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 8f19c23..5eca579 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -226,22 +226,41 @@ class CRUDComponent extends Component } else { $this->Controller->set('metaFieldsEnabled', false); } - $filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; + $filtersConfigRaw= !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; + $filtersConfig = []; + foreach ($filtersConfigRaw as $fieldConfig) { + if (is_array($fieldConfig)) { + $filtersConfig[$fieldConfig['name']] = $fieldConfig; + } else { + $filtersConfig[$fieldConfig] = ['name' => $fieldConfig]; + } + } + $filtersName = $this->getFilterFieldsName(); $typeMap = $this->Table->getSchema()->typeMap(); - $associatedtypeMap = !empty($this->Controller->filterFields) ? $this->_getAssociatedTypeMap() : []; + $associatedtypeMap = !empty($filtersName) ? $this->_getAssociatedTypeMap() : []; $typeMap = array_merge( $this->Table->getSchema()->typeMap(), $associatedtypeMap ); - $typeMap = array_filter($typeMap, function ($field) use ($filters) { - return in_array($field, $filters); + $typeMap = array_filter($typeMap, function ($field) use ($filtersName) { + return in_array($field, $filtersName); }, ARRAY_FILTER_USE_KEY); $this->Controller->set('typeMap', $typeMap); - $this->Controller->set('filters', $filters); + $this->Controller->set('filters', $filtersName); + $this->Controller->set('filtersConfig', $filtersConfig); $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/filters'); } + public function getFilterFieldsName(): array + { + $filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; + $filters = array_map(function($item) { + return is_array($item) ? $item['name'] : $item; + }, $filters); + return $filters; + } + /** * getResponsePayload Returns the adaquate response payload based on the request context * @@ -1613,7 +1632,7 @@ class CRUDComponent extends Component protected function _getAssociatedTypeMap(): array { $typeMap = []; - foreach ($this->Controller->filterFields as $filter) { + foreach ($this->getFilterFieldsName() as $filter) { $exploded = explode('.', $filter); if (count($exploded) > 1) { $model = $exploded[0]; diff --git a/templates/element/genericElements/Form/Fields/dropdownField.php b/templates/element/genericElements/Form/Fields/dropdownField.php index 4db9707..50be756 100644 --- a/templates/element/genericElements/Form/Fields/dropdownField.php +++ b/templates/element/genericElements/Form/Fields/dropdownField.php @@ -2,7 +2,7 @@ $seed = 's-' . mt_rand(); $controlParams = [ 'type' => 'select', - 'options' => $fieldData['options'], + 'options' => $fieldData['options'] ?? [], 'empty' => $fieldData['empty'] ?? false, 'value' => $fieldData['value'] ?? null, 'multiple' => $fieldData['multiple'] ?? false, @@ -20,8 +20,10 @@ if ($controlParams['options'] instanceof \Cake\ORM\Query) { $controlParams['options'] = $controlParams['options']->all()->toList(); } if (!empty($fieldData['select2'])) { + $fieldData['select2'] = $fieldData['select2'] === true ? [] : $fieldData['select2']; $controlParams['class'] .= ' select2-input'; } +$controlParams['class'] .= ' dropdown-custom-value' . "-$seed"; if (in_array('_custom', array_keys($controlParams['options']))) { $customInputValue = $this->Form->getSourceValue($fieldData['field']); if (!in_array($customInputValue, $controlParams['options'])) { @@ -34,7 +36,6 @@ if (in_array('_custom', array_keys($controlParams['options']))) { } else { $customInputValue = ''; } - $controlParams['class'] .= ' dropdown-custom-value' . "-$seed"; $adaptedField = $fieldData['field'] . '_custom'; $controlParams['templates']['formGroup'] = sprintf( '
{{input}}{{error}}%s
', @@ -57,9 +58,12 @@ echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $f if ($container.length == 0) { $container = $(document.body) } - $select.select2({ + const defaultSelect2Options = { dropdownParent: $container, - }) + } + const passedSelect2Options = ; + const select2Options = Object.assign({}, passedSelect2Options, defaultSelect2Options) + $select.select2(select2Options) }) diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index 858d5c3..4cb1df0 100644 --- a/templates/genericTemplates/filters.php +++ b/templates/genericTemplates/filters.php @@ -47,19 +47,28 @@ $filteringForm = $this->Bootstrap->table( __('Value'), sprintf('', __('Supports strict matches and LIKE matches with the `%` character. Example: `%.com`')) ), - 'formatter' => function ($field, $row) use ($typeMap, $formTypeMap) { + 'formatter' => function ($field, $row) use ($typeMap, $formTypeMap, $filtersConfig) { $fieldName = $row['fieldname']; $formType = $formTypeMap[$typeMap[$fieldName]] ?? 'text'; + $fieldData = [ + 'field' => $fieldName, + 'type' => $formType, + 'label' => '', + 'class' => 'fieldValue form-control-sm' + ]; + if (!empty($filtersConfig[$fieldName]['multiple'])) { + $fieldData['type'] = 'dropdown'; + $fieldData['multiple'] = true; + $fieldData['select2'] = [ + 'tags' => true, + 'tokenSeparators' => [',', ' '], + ]; + } $this->Form->setTemplates([ 'formGroup' => '
{{input}}
', ]); return $this->element('genericElements/Form/fieldScaffold', [ - 'fieldData' => [ - 'field' => $fieldName, - 'type' => $formType, - 'label' => '', - 'class' => 'fieldValue form-control-sm' - ], + 'fieldData' => $fieldData, 'params' => [] ]); } @@ -169,6 +178,36 @@ echo $this->Bootstrap->modal([ } setFilteringValues($filteringTable, field, value, operator) } + if (tags.length > 0) { + setFilteringTags($filteringTable, tags) + } + } + + function setFilteringValues($filteringTable, field, value, operator) { + $row = $filteringTable.find('td > span.fieldName').filter(function() { + return $(this).data('fieldname') == field + }).closest('tr') + $row.find('.fieldOperator').val(operator) + const $formElement = $row.find('.fieldValue'); + if ($formElement.attr('type') === 'datetime-local') { + $formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss')) + } else if ($formElement.is('select') && Array.isArray(value)) { + let newOptions = []; + value.forEach(aValue => { + const existingOption = $formElement.find('option').filter(function() { + return $(this).val() === aValue + }) + if (existingOption.length == 0) { + newOptions.push(new Option(aValue, aValue, true, true)) + } + }) + $formElement.append(newOptions).trigger('change'); + } else { + $formElement.val(value) + } + } + + function setFilteringTags($filteringTable, tags) { $select = $filteringTable.closest('.modal-body').find('select.select2-input') let passedTags = [] tags.forEach(tagname => { @@ -185,19 +224,6 @@ echo $this->Bootstrap->modal([ .trigger('change') } - function setFilteringValues($filteringTable, field, value, operator) { - $row = $filteringTable.find('td > span.fieldName').filter(function() { - return $(this).data('fieldname') == field - }).closest('tr') - $row.find('.fieldOperator').val(operator) - const $formElement = $row.find('.fieldValue'); - if ($formElement.attr('type') === 'datetime-local') { - $formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss')) - } else { - $formElement.val(value) - } - } - function getDataFromRow($row) { const rowData = {}; rowData['name'] = $row.find('td > span.fieldName').data('fieldname') From e9056a7b4ce5c54cab112b8bd5bf274b56e522bf Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 12:56:10 +0100 Subject: [PATCH 04/34] chg: [audit:filter] Made request_action a multiple search --- src/Controller/AuditLogsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php index e3e4e0c..8836d49 100644 --- a/src/Controller/AuditLogsController.php +++ b/src/Controller/AuditLogsController.php @@ -11,7 +11,7 @@ use Cake\Core\Configure; class AuditLogsController extends AppController { - public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title', 'AuditLogs.created']; + public $filterFields = ['model_id', 'model', ['name' => 'request_action', 'multiple' => true, ], 'user_id', 'model_title', 'AuditLogs.created']; public $quickFilterFields = ['model', 'request_action', 'model_title']; public $containFields = ['Users']; @@ -20,7 +20,7 @@ class AuditLogsController extends AppController $this->CRUD->index([ 'contain' => $this->containFields, 'order' => ['AuditLogs.id' => 'DESC'], - 'filters' => $this->filterFields, + 'filters' => $this->CRUD->getFilterFieldsName($this->filterFields), 'quickFilters' => $this->quickFilterFields, 'afterFind' => function($data) { $request_ip = is_resource($data['request_ip']) ? stream_get_contents($data['request_ip']) : $data['request_ip']; From f18cde8b0ff20cc47528a52fa57f4b0f7b402eb9 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 14:22:00 +0100 Subject: [PATCH 05/34] fix: [authkey:add] Forced `expiration` field to use datetime UI component Fix #145 --- templates/AuthKeys/add.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/AuthKeys/add.php b/templates/AuthKeys/add.php index c9f0219..4c829ec 100644 --- a/templates/AuthKeys/add.php +++ b/templates/AuthKeys/add.php @@ -14,7 +14,8 @@ echo $this->element('genericElements/Form/genericForm', [ ], [ 'field' => 'expiration', - 'label' => 'Expiration' + 'label' => __('Expiration'), + 'type' => 'datetime', ] ], 'submit' => [ From 7ccf9252470a23acc38ad6ed13eecf523e368b48 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 14:57:42 +0100 Subject: [PATCH 06/34] security: [authkey:add] Restrict creation of API keys for users in the same org and for other org_admins --- src/Controller/AuthKeysController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Controller/AuthKeysController.php b/src/Controller/AuthKeysController.php index 978810c..bfca38d 100644 --- a/src/Controller/AuthKeysController.php +++ b/src/Controller/AuthKeysController.php @@ -71,8 +71,12 @@ class AuthKeysController extends AppController if (empty($currentUser['role']['perm_org_admin'])) { $userConditions['id'] = $currentUser['id']; } else { - $role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0])->all()->extract('id')->toList(); - $userConditions['role_id IN'] = $role_ids; + $role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->extract('id')->toList(); + $userConditions['organisation_id'] = $currentUser['organisation_id']; + $userConditions['OR'] = [ + ['role_id IN' => $role_ids], + ['id' => $currentUser['id']], + ]; } } $users = $this->Users->find('list'); From 487670e522b5fbbe7b631305181ca64d75e32dfb Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 14:57:58 +0100 Subject: [PATCH 07/34] chg: [authkeys:add] Select logged-in user by default --- src/Controller/AuthKeysController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/AuthKeysController.php b/src/Controller/AuthKeysController.php index bfca38d..37aa356 100644 --- a/src/Controller/AuthKeysController.php +++ b/src/Controller/AuthKeysController.php @@ -103,6 +103,7 @@ class AuthKeysController extends AppController $dropdownData = [ 'user' => $users ]; + $this->entity->user_id = $currentUser['id']; $this->set(compact('dropdownData')); } } From a67c6b70d538adb0d12723a3f48ccd6cc7bbe2b2 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 23 Feb 2023 14:58:54 +0100 Subject: [PATCH 08/34] chg: [roles:index] Only show `add role` button for users having ACL access --- templates/Roles/index.php | 43 +++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/templates/Roles/index.php b/templates/Roles/index.php index d846934..02299e6 100644 --- a/templates/Roles/index.php +++ b/templates/Roles/index.php @@ -1,28 +1,31 @@ role->perm_admin)) { + $topbarChildren[] = [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Add role'), + 'class' => 'btn btn-primary', + 'popover_url' => '/roles/add' + ] + ] + ]; +} +$topbarChildren[] = [ + 'type' => 'search', + 'button' => __('Search'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' +]; + echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, 'top_bar' => [ - 'children' => [ - [ - 'type' => 'simple', - 'children' => [ - 'data' => [ - 'type' => 'simple', - 'text' => __('Add role'), - 'class' => 'btn btn-primary', - 'popover_url' => '/roles/add' - ] - ] - ], - [ - 'type' => 'search', - 'button' => __('Search'), - 'placeholder' => __('Enter value to search'), - 'data' => '', - 'searchKey' => 'value' - ] - ] + 'children' => $topbarChildren, ], 'fields' => [ [ From 6eb5106153773cf26ecbeb7115ef42e6fb801ca9 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 10:03:45 +0100 Subject: [PATCH 09/34] new: [ui:formInfo] Rafactored formInfo and added support of field description Can be done by using the `tooltip` key on the field configuration --- src/View/Helper/FormFieldMassageHelper.php | 9 +++ .../genericElements/Form/Fields/uuidField.php | 76 +++++++++---------- .../genericElements/Form/fieldScaffold.php | 7 +- .../element/genericElements/Form/formInfo.php | 53 ++++++++----- .../genericElements/Form/genericForm.php | 2 +- 5 files changed, 86 insertions(+), 61 deletions(-) diff --git a/src/View/Helper/FormFieldMassageHelper.php b/src/View/Helper/FormFieldMassageHelper.php index 183a9ed..b0db4ab 100644 --- a/src/View/Helper/FormFieldMassageHelper.php +++ b/src/View/Helper/FormFieldMassageHelper.php @@ -8,6 +8,15 @@ class FormFieldMassageHelper extends Helper { public function prepareFormElement(\Cake\View\Helper\FormHelper $form, array $controlParams, array $fieldData): string { + if ($fieldData['tooltip']) { + $form->setTemplates([ + 'label' => '{{text}}{{tooltip}}', + ]); + $controlParams['templateVars'] = array_merge( + $controlParams['templateVars'] ?? [], + ['tooltip' => $fieldData['tooltip'], ] + ); + } if (!empty($fieldData['stateDependence'])) { $controlParams['data-dependence-source'] = h($fieldData['stateDependence']['source']); $controlParams['data-dependence-option'] = h($fieldData['stateDependence']['option']); diff --git a/templates/element/genericElements/Form/Fields/uuidField.php b/templates/element/genericElements/Form/Fields/uuidField.php index b065e08..b63e551 100644 --- a/templates/element/genericElements/Form/Fields/uuidField.php +++ b/templates/element/genericElements/Form/Fields/uuidField.php @@ -1,43 +1,43 @@ Form->setTemplates([ - 'inputContainer' => '{{content}}', - 'inputContainerError' => '{{content}}', - 'formGroup' => '{{input}}', - ]); - $label = $fieldData['label']; - $formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData); - $temp = sprintf( - '
-
%s
-
-
- %s%s -
-
-
', - h($label), - $formElement, - sprintf( - '%s', - $random, - __('Generate') - ) - ); - echo $temp; +$random = Cake\Utility\Security::randomString(8); +$params['div'] = false; + +$genUUIDButton = $this->Bootstrap->button([ + 'id' => "uuid-gen-{$random}", + 'variant' => 'secondary', + 'text' => __('Generate'), +]); + +$this->Form->setTemplates([ + 'input' => sprintf('
%s{{genUUIDButton}}
', $this->Form->getTemplates('input')), +]); +$params['templateVars'] = [ + 'genUUIDButton' => $genUUIDButton, +]; + +$formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData); +echo $formElement; ?> + \ No newline at end of file diff --git a/templates/element/genericElements/Form/fieldScaffold.php b/templates/element/genericElements/Form/fieldScaffold.php index 18e6000..f95bdde 100644 --- a/templates/element/genericElements/Form/fieldScaffold.php +++ b/templates/element/genericElements/Form/fieldScaffold.php @@ -11,11 +11,12 @@ $fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']); } } - if (!empty($fieldDesc[$fieldData['field']])) { - $fieldData['label'] .= $this->element( + $fieldDescription = $fieldData['tooltip'] ?? ($fieldDesc[$fieldData['field']] ?? false); + if (!empty($fieldDescription)) { + $fieldData['tooltip'] = $this->element( 'genericElements/Form/formInfo', array( 'field' => $fieldData, - 'fieldDesc' => $fieldDesc[$fieldData['field']], + 'fieldDesc' => $fieldDescription, 'modelForForm' => $modelForForm ) ); diff --git a/templates/element/genericElements/Form/formInfo.php b/templates/element/genericElements/Form/formInfo.php index 89dd0b7..d569142 100644 --- a/templates/element/genericElements/Form/formInfo.php +++ b/templates/element/genericElements/Form/formInfo.php @@ -1,4 +1,5 @@ $fieldDesc); $default = 'info'; @@ -16,32 +17,46 @@ $default = 'info'; } } - echo sprintf( - '', - h($field['field']) - ); + $popoverID = sprintf("%sInfoPopover%s", h($field['field']), $seed); + echo $this->Bootstrap->icon('info-circle', [ + 'id' => $popoverID, + 'class' => ['ms-1'], + 'attrs' => [ + 'data-bs-toggle' => 'popover', + 'data-bs-trigger' => 'hover', + ] + ]); ?> diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index 7dafa18..5a91e78 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -19,7 +19,7 @@ $entity = isset($entity) ? $entity : null; $fieldsString = ''; $simpleFieldWhitelist = [ - 'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked' + 'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked', ]; if (empty($data['url'])) { $data['url'] = ["controller" => $this->request->getParam('controller'), "action" => $this->request->getParam('url')]; From 73c4baac3140f748f774ea608340f387149ac77a Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 10:04:18 +0100 Subject: [PATCH 10/34] chg: [helper:bootstrap] Added support of ID option --- src/View/Helper/BootstrapElements/BootstrapIcon.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/View/Helper/BootstrapElements/BootstrapIcon.php b/src/View/Helper/BootstrapElements/BootstrapIcon.php index 71c0c8a..837095d 100644 --- a/src/View/Helper/BootstrapElements/BootstrapIcon.php +++ b/src/View/Helper/BootstrapElements/BootstrapIcon.php @@ -21,6 +21,7 @@ class BootstrapIcon extends BootstrapGeneric { private $icon = ''; private $defaultOptions = [ + 'id' => '', 'class' => [], 'title' => '', 'attrs' => [], @@ -48,6 +49,7 @@ class BootstrapIcon extends BootstrapGeneric { $html = $this->node('span', array_merge( [ + 'id' => $this->options['id'] ?? '', 'class' => array_merge( $this->options['class'], ["fa fa-{$this->icon}"] From 22c460575eea8bc9d93a549ed20763b48138720d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 10:05:58 +0100 Subject: [PATCH 11/34] chg: [organisations:add] Added notice about UUID reuse --- templates/Organisations/add.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/Organisations/add.php b/templates/Organisations/add.php index 787f9a4..e430cc6 100644 --- a/templates/Organisations/add.php +++ b/templates/Organisations/add.php @@ -10,7 +10,8 @@ array( 'field' => 'uuid', 'label' => 'UUID', - 'type' => 'uuid' + 'type' => 'uuid', + 'tooltip' => __('If the Organisation already has a known UUID in another application such as MISP or another Cerebrate, please re-use this one.'), ), array( 'field' => 'url' From 1620fd3e596973a545c4b63a683647208d78db57 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 10:31:33 +0100 Subject: [PATCH 12/34] chg: [encryptionKey] Made key searchable with substring strategy --- src/Controller/EncryptionKeysController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index 39453b2..fb524a5 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -15,7 +15,7 @@ use Cake\Error\Debugger; class EncryptionKeysController extends AppController { public $filterFields = ['owner_model', 'owner_id', 'encryption_key']; - public $quickFilterFields = ['encryption_key']; + public $quickFilterFields = [['encryption_key' => true]]; public $containFields = ['Individuals', 'Organisations']; public $statisticsFields = ['type']; From c148b0993a7329bb2b6cdc68ad3413113960c069 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 10:35:25 +0100 Subject: [PATCH 13/34] chg: [encryptionKeys:beforeSave] Updated ACL to disable management of keys for regular orgs --- src/Controller/EncryptionKeysController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index fb524a5..a01adf7 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -83,6 +83,9 @@ class EncryptionKeysController extends AppController $individualConditions = [ 'id' => $currentUser['individual_id'] ]; + $orgConditions = [ + 'id' => -1, // Only org_admins are allowed to manage their org's encryption keys + ]; } else { $this->loadModel('Alignments'); $individualConditions = ['id IN' => $this->Alignments->find('list', [ @@ -122,6 +125,11 @@ class EncryptionKeysController extends AppController 'organisation' => $this->Organisations->find('list')->order(['name' => 'asc'])->where($orgConditions)->all()->toArray(), 'individual' => $this->Individuals->find('list')->order(['email' => 'asc'])->where($individualConditions)->all()->toArray() ]; + foreach ($dropdownData as $modelName => $list) { + if (empty($list)) { + unset($dropdownData[$modelName]); + } + } return $params; } From cbdf64a78488e40ec519b78cf7d9d0cbd4b0c12a Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 11:12:48 +0100 Subject: [PATCH 14/34] new: [element:tagsField] Added support of editable based on passed configuration --- .../element/genericElements/SingleViews/Fields/tagsField.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/element/genericElements/SingleViews/Fields/tagsField.php b/templates/element/genericElements/SingleViews/Fields/tagsField.php index 414da50..6a56530 100644 --- a/templates/element/genericElements/SingleViews/Fields/tagsField.php +++ b/templates/element/genericElements/SingleViews/Fields/tagsField.php @@ -3,6 +3,6 @@ $tags = Cake\Utility\Hash::get($data, 'tags'); echo $this->Tag->tags($tags, [ 'allTags' => $allTags, - 'picker' => true, - 'editable' => true, + 'picker' => !empty($field['editable']), + 'editable' => !empty($field['editable']), ]); From af8f1e9e74b5c12cde5b7a44451e6232487df7a5 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 11:17:55 +0100 Subject: [PATCH 15/34] chg: [tags:org/individual] Relaxed ACL on tagging - Before only `site_admin` could add tags. - Now `org_admins` can add tags for their orgs and individuals - Regular users can self manage their own individual tag --- src/Controller/Component/ACLComponent.php | 8 ++--- src/Controller/IndividualsController.php | 35 ++++++++++++++-------- src/Controller/OrganisationsController.php | 27 ++++++++++++----- templates/Individuals/view.php | 1 + templates/Organisations/view.php | 1 + 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 9c3dd4c..4d37067 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -90,8 +90,8 @@ class ACLComponent extends Component 'edit' => ['perm_admin', 'perm_org_admin'], 'filtering' => ['*'], 'index' => ['*'], - 'tag' => ['perm_tagger'], - 'untag' => ['perm_tagger'], + 'tag' => ['*'], + 'untag' => ['*'], 'view' => ['*'], 'viewTags' => ['*'] ], @@ -152,8 +152,8 @@ class ACLComponent extends Component 'edit' => ['perm_admin'], 'filtering' => ['*'], 'index' => ['*'], - 'tag' => ['perm_tagger'], - 'untag' => ['perm_tagger'], + 'tag' => ['perm_org_admin'], + 'untag' => ['perm_org_admin'], 'view' => ['*'], 'viewTags' => ['*'] ], diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index bd3c852..236dff6 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -68,25 +68,15 @@ class IndividualsController extends AppController if (!empty($responsePayload)) { return $responsePayload; } + $this->set('canEdit', $this->canEdit($id)); } public function edit($id) { - $currentUser = $this->ACL->getUser(); - if (!$currentUser['role']['perm_admin']) { - $validIndividuals = $this->Individuals->getValidIndividualsToEdit($currentUser); - if (!in_array($id, $validIndividuals)) { - throw new MethodNotAllowedException(__('You cannot modify that individual.')); - } + if (!$this->canEdit($id)) { + throw new MethodNotAllowedException(__('You cannot modify that individual.')); } $currentUser = $this->ACL->getUser(); - $validIndividualIds = []; - if (!$currentUser['role']['perm_admin']) { - $validIndividualIds = $this->Individuals->getValidIndividualsToEdit($currentUser); - if (!in_array($id, $validIndividualIds)) { - throw new NotFoundException(__('Invalid individual.')); - } - } $this->CRUD->edit($id, [ 'beforeSave' => function($data) use ($currentUser) { if ($currentUser['role']['perm_admin'] && isset($data['uuid'])) { @@ -113,6 +103,9 @@ class IndividualsController extends AppController public function tag($id) { + if (!$this->canEdit($id)) { + throw new MethodNotAllowedException(__('You cannot tag that individual.')); + } $this->CRUD->tag($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -122,6 +115,9 @@ class IndividualsController extends AppController public function untag($id) { + if (!$this->canEdit($id)) { + throw new MethodNotAllowedException(__('You cannot untag that individual.')); + } $this->CRUD->untag($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -137,4 +133,17 @@ class IndividualsController extends AppController return $responsePayload; } } + + public function canEdit($indId): bool + { + $currentUser = $this->ACL->getUser(); + if ($currentUser['role']['perm_admin']) { + return true; + } + $validIndividuals = $this->Individuals->getValidIndividualsToEdit($currentUser); + if (in_array($indId, $validIndividuals)) { + return true; + } + return false; + } } diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php index 367df4f..5de8063 100644 --- a/src/Controller/OrganisationsController.php +++ b/src/Controller/OrganisationsController.php @@ -101,7 +101,6 @@ class OrganisationsController extends AppController if (!empty($responsePayload)) { return $responsePayload; } - $this->set('metaGroup', 'ContactDB'); } public function view($id) @@ -111,16 +110,12 @@ class OrganisationsController extends AppController if (!empty($responsePayload)) { return $responsePayload; } - $this->set('metaGroup', 'ContactDB'); + $this->set('canEdit', $this->canEdit($id)); } public function edit($id) { - $currentUser = $this->ACL->getUser(); - if ( - !($currentUser['organisation']['id'] == $id && $currentUser['role']['perm_org_admin']) && - !$currentUser['role']['perm_admin'] - ) { + if (!$this->canEdit($id)) { throw new MethodNotAllowedException(__('You cannot modify that organisation.')); } $this->CRUD->edit($id); @@ -144,6 +139,9 @@ class OrganisationsController extends AppController public function tag($id) { + if (!$this->canEdit($id)) { + throw new MethodNotAllowedException(__('You cannot tag that organisation.')); + } $this->CRUD->tag($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -153,6 +151,9 @@ class OrganisationsController extends AppController public function untag($id) { + if (!$this->canEdit($id)) { + throw new MethodNotAllowedException(__('You cannot untag that organisation.')); + } $this->CRUD->untag($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -168,4 +169,16 @@ class OrganisationsController extends AppController return $responsePayload; } } + + public function canEdit($orgId): bool + { + $currentUser = $this->ACL->getUser(); + if ($currentUser['role']['perm_admin']) { + return true; + } + if ($currentUser['role']['perm_org_admin'] && $currentUser['organisation']['id'] == $orgId) { + return true; + } + return false; + } } diff --git a/templates/Individuals/view.php b/templates/Individuals/view.php index 3eeddbc..423c7e6 100644 --- a/templates/Individuals/view.php +++ b/templates/Individuals/view.php @@ -31,6 +31,7 @@ echo $this->element( [ 'key' => __('Tags'), 'type' => 'tags', + 'editable' => $canEdit, ], [ 'key' => __('Alignments'), diff --git a/templates/Organisations/view.php b/templates/Organisations/view.php index e393c6f..0d3d2cd 100644 --- a/templates/Organisations/view.php +++ b/templates/Organisations/view.php @@ -40,6 +40,7 @@ echo $this->element( [ 'key' => __('Tags'), 'type' => 'tags', + 'editable' => $canEdit, ], [ 'key' => __('Alignments'), From 0833a8c0e4808d81c117e82885418cc8dbb1382e Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 11:18:48 +0100 Subject: [PATCH 16/34] fix: [individual:getValidToEdit] Restricted ACL to prevent one org_admin to edit another from the same org --- src/Model/Table/IndividualsTable.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 95828ff..277b2f7 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -124,12 +124,15 @@ class IndividualsTable extends AppTable public function getValidIndividualsToEdit(object $currentUser): array { - $adminRoles = $this->Users->Roles->find('list')->select(['id'])->where(['perm_admin' => 1])->all()->toArray(); + $validRoles = $this->Users->Roles->find('list')->select(['id'])->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->toArray(); $validIndividualIds = $this->Users->find('list')->select(['individual_id'])->where( [ 'organisation_id' => $currentUser['organisation_id'], 'disabled' => 0, - 'role_id NOT IN' => array_keys($adminRoles) + 'OR' => [ + ['role_id IN' => array_keys($validRoles)], + ['id' => $currentUser['id']], + ] ] )->all()->toArray(); return array_keys($validIndividualIds); From fda8aa5866645a08e0287ee5ea79aa4e356757aa Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 14:57:40 +0100 Subject: [PATCH 17/34] chg: [component:CRUD] Include meta-template before calling `afterFind` --- src/Controller/Component/CRUDComponent.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 5eca579..af8e193 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -568,16 +568,16 @@ class CRUDComponent extends Component $query->where($params['conditions']); } $data = $query->first(); + if ($this->metaFieldsSupported()) { + $metaTemplates = $this->getMetaTemplates(); + $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); + } if (isset($params['afterFind'])) { $data = $params['afterFind']($data, $params); } if (empty($data)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } - if ($this->metaFieldsSupported()) { - $metaTemplates = $this->getMetaTemplates(); - $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); - } if ($this->request->is(['post', 'put'])) { $patchEntityParams = [ 'associated' => [] From a4276863880f5ccd49a7b36a8b2fc89b3852f378 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 15:02:08 +0100 Subject: [PATCH 18/34] chg: [user:permissionRestriction] Move check from beforeSave to ApplicationRule --- src/Model/Table/UsersTable.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index aa52040..86160d5 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -73,13 +73,6 @@ class UsersTable extends AppTable if (!$entity->isNew()) { $success = $this->handleUserUpdateRouter($entity); } - $permissionRestrictionCheck = $this->checkPermissionRestrictions($entity); - if ($permissionRestrictionCheck !== true) { - $entity->setErrors($permissionRestrictionCheck); - $event->stopPropagation(); - $event->setResult(false); - return false; - } return $success; } @@ -187,10 +180,24 @@ class UsersTable extends AppTable public function buildRules(RulesChecker $rules): RulesChecker { $rules->add($rules->isUnique(['username'])); - $allowDuplicateIndividuals = false; if (empty(Configure::read('user.multiple-users-per-individual')) || !empty(Configure::read('keycloak.enabled'))) { $rules->add($rules->isUnique(['individual_id'])); } + + $rules->add(function($entity, $options) { + $permissionRestrictionCheck = $this->checkPermissionRestrictions($entity); + if ($permissionRestrictionCheck !== true) { + foreach ($permissionRestrictionCheck as $permission_name => $errors) { + foreach ($entity->meta_fields as $i => $metaField) { + if ($metaField['field'] === $permission_name) { + $entity->meta_fields[$i]->setErrors(['value' => $errors]); + } + } + } + return false; + } + return true; + }, 'permissionLimitations'); return $rules; } From 14b46c0e9d8da7f408e1b0371a564a369757fbe0 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 15:03:06 +0100 Subject: [PATCH 19/34] fix: [element:metafields_panel] Correct usage of notices for bootstrap/listTable --- .../genericElements/SingleViews/metafields_panel.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/element/genericElements/SingleViews/metafields_panel.php b/templates/element/genericElements/SingleViews/metafields_panel.php index 82b24be..d4b474f 100644 --- a/templates/element/genericElements/SingleViews/metafields_panel.php +++ b/templates/element/genericElements/SingleViews/metafields_panel.php @@ -28,9 +28,9 @@ foreach($data['MetaTemplates'] as $metaTemplate) { ] ]), 'rawNoEscaping' => true, - 'warning' => $metaField->warning ?? null, - 'info' => $metaField->info ?? null, - 'danger' => $metaField->danger ?? null + 'notice_warning' => $metaTemplateField->warning ?? null, + 'notice_info' => $metaTemplateField->info ?? null, + 'notice_danger' => $metaTemplateField->danger ?? null ]; $labelPrintedOnce = true; } From 57835b8e52c16c703772f257642a34ce926f3a0d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 15:05:19 +0100 Subject: [PATCH 20/34] fix: [elements:metaTemplateForm] Restored error container in the form --- templates/element/genericElements/Form/metaTemplateForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/element/genericElements/Form/metaTemplateForm.php b/templates/element/genericElements/Form/metaTemplateForm.php index 5331cf9..fb12f9d 100644 --- a/templates/element/genericElements/Form/metaTemplateForm.php +++ b/templates/element/genericElements/Form/metaTemplateForm.php @@ -4,7 +4,7 @@ use Cake\Utility\Inflector; $default_template = [ 'inputContainer' => '
{{content}}
', - 'inputContainerError' => '
{{content}}
', + 'inputContainerError' => '
{{content}}{{error}}
', 'formGroup' => '
{{input}}{{error}}
', 'error' => '
{{content}}
', 'errorList' => '
    {{content}}
', From aead79a4c3ac28acca138ef356b2e13afaa0452c Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 15:20:29 +0100 Subject: [PATCH 21/34] chg: [component:CRUD] Added `afterFind` support in add --- src/Controller/Component/CRUDComponent.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index af8e193..d7cb5bf 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -309,6 +309,9 @@ class CRUDComponent extends Component if ($this->metaFieldsSupported()) { $metaTemplates = $this->getMetaTemplates(); $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } } if ($this->request->is('post')) { $patchEntityParams = [ From 59f8608d50f906b27c9bfdc25e8dc0178943f24b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 24 Feb 2023 15:22:18 +0100 Subject: [PATCH 22/34] new: [user:permissionLimitation] Added current permission status while in `add` or `edit` context Also moved the notification key from meta-fields to meta-template-fields --- src/Controller/UsersController.php | 14 ++ .../Table/PermissionLimitationsTable.php | 70 +++--- templates/Users/add.php | 232 +++++++++++------- 3 files changed, 192 insertions(+), 124 deletions(-) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 3bd715d..dad2bc7 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -120,6 +120,12 @@ class UsersController extends AppController if (Configure::read('keycloak.enabled')) { $this->Users->enrollUserRouter($data); } + }, + 'afterFind' => function ($user, &$params) use ($currentUser) { + if (!empty($user)) { // We don't have a 404 + $user = $this->fetchTable('PermissionLimitations')->attachLimitations($user); + } + return $user; } ]); $responsePayload = $this->CRUD->getResponsePayload(); @@ -227,6 +233,14 @@ class UsersController extends AppController if (!$this->ACL->canEditUser($currentUser, $user)) { throw new MethodNotAllowedException(__('You cannot edit the given user.')); } + $user = $this->fetchTable('PermissionLimitations')->attachLimitations($user); + } + return $user; + }; + } else { + $params['afterFind'] = function ($user, &$params) use ($currentUser) { + if (!empty($user)) { // We don't have a 404 + $user = $this->fetchTable('PermissionLimitations')->attachLimitations($user); } return $user; }; diff --git a/src/Model/Table/PermissionLimitationsTable.php b/src/Model/Table/PermissionLimitationsTable.php index e40cf96..206ea8e 100644 --- a/src/Model/Table/PermissionLimitationsTable.php +++ b/src/Model/Table/PermissionLimitationsTable.php @@ -30,13 +30,17 @@ class PermissionLimitationsTable extends AppTable public function getListOfLimitations(\App\Model\Entity\User $data) { $Users = TableRegistry::getTableLocator()->get('Users'); - $ownOrgUserIds = $Users->find('list', [ - 'keyField' => 'id', - 'valueField' => 'id', - 'conditions' => [ - 'organisation_id' => $data['organisation_id'] - ] - ])->all()->toList(); + $includeOrganisationPermissions = !empty($data['organisation_id']); + $ownOrgUserIds = []; + if ($includeOrganisationPermissions) { + $ownOrgUserIds = $Users->find('list', [ + 'keyField' => 'id', + 'valueField' => 'id', + 'conditions' => [ + 'organisation_id' => $data['organisation_id'] + ] + ])->all()->toList(); + } $MetaFields = TableRegistry::getTableLocator()->get('MetaFields'); $raw = $this->find()->select(['scope', 'permission', 'max_occurrence'])->disableHydration()->toArray(); $limitations = []; @@ -70,9 +74,12 @@ class PermissionLimitationsTable extends AppTable if (!empty($ownOrgUserIds)) { $conditions['parent_id IN'] = array_values($ownOrgUserIds); } - $limitations[$field]['organisation']['current'] = $MetaFields->find('all', [ - 'conditions' => $conditions, - ])->count(); + $limitations[$field]['organisation']['current'] = '?'; + if ($includeOrganisationPermissions) { + $limitations[$field]['organisation']['current'] = $MetaFields->find('all', [ + 'conditions' => $conditions, + ])->count(); + } } } return $limitations; @@ -89,34 +96,35 @@ class PermissionLimitationsTable extends AppTable if (!empty($data['MetaTemplates'])) { foreach ($data['MetaTemplates'] as &$metaTemplate) { foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) { - $boolean = $meta_template_field['type'] === 'boolean'; - foreach ($meta_template_field['metaFields'] as &$metaField) { - if (isset($permissionLimitations[$metaField['field']])) { - foreach ($permissionLimitations[$metaField['field']] as $scope => $value) { - $messageType = 'warning'; + if (isset($permissionLimitations[$meta_template_field['field']])) { + foreach ($permissionLimitations[$meta_template_field['field']] as $scope => $value) { + $messageType = 'warning'; + if ($value['current'] == '?') { + $messageType = 'info'; + } else { if ($value['limit'] > $value['current']) { $messageType = 'info'; } if ($value['limit'] < $value['current']) { $messageType = 'danger'; } - if (empty($metaField[$messageType])) { - $metaField[$messageType] = ''; - } - $altText = __( - 'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).', - $scope === 'global' ? __('instance wide') : __('for your organisation'), - $value['current'], - $value['limit'] - ); - $metaField[$messageType] .= sprintf( - ' : %s/%s', - $altText, - $icons[$scope], - $value['current'], - $value['limit'] - ); } + if (empty($metaField[$messageType])) { + $metaField[$messageType] = ''; + } + $altText = __( + 'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).', + $scope === 'global' ? __('instance wide') : __('for your organisation'), + $value['current'], + $value['limit'] + ); + $meta_template_field[$messageType] .= sprintf( + ' : %s/%s', + $altText, + $icons[$scope], + $value['current'], + $value['limit'] + ); } } } diff --git a/templates/Users/add.php b/templates/Users/add.php index 93a8d56..9aff9d3 100644 --- a/templates/Users/add.php +++ b/templates/Users/add.php @@ -1,99 +1,145 @@ request->getParam('action') === 'add') { - $dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual']; - if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) { - $passwordRequired = 'required'; - } - } + +use Cake\Core\Configure; + +$passwordRequired = false; +$showPasswordField = false; +if ($this->request->getParam('action') === 'add') { + $dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual']; if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) { - $showPasswordField = true; + $passwordRequired = 'required'; } - echo $this->element('genericElements/Form/genericForm', [ - 'data' => [ - 'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'), - 'model' => 'Roles', - 'fields' => [ - [ - 'field' => 'individual_id', - 'type' => 'dropdown', - 'label' => __('Associated individual'), - 'options' => isset($dropdownData['individual']) ? $dropdownData['individual'] : [], - 'conditions' => $this->request->getParam('action') === 'add' - ], - [ - 'field' => 'individual.email', - 'stateDependence' => [ - 'source' => '#individual_id-field', - 'option' => 'new' - ], - 'required' => false - ], - [ - 'field' => 'individual.first_name', - 'label' => 'First name', - 'stateDependence' => [ - 'source' => '#individual_id-field', - 'option' => 'new' - ], - 'required' => false - ], - [ - 'field' => 'individual.last_name', - 'label' => 'Last name', - 'stateDependence' => [ - 'source' => '#individual_id-field', - 'option' => 'new' - ], - 'required' => false - ], - [ - 'field' => 'username', - 'autocomplete' => 'off' - ], - [ - 'field' => 'organisation_id', - 'type' => 'dropdown', - 'label' => __('Associated organisation'), - 'options' => $dropdownData['organisation'], - 'default' => $loggedUser['organisation_id'] - ], - [ - 'field' => 'password', - 'label' => __('Password'), - 'type' => 'password', - 'required' => $passwordRequired, - 'autocomplete' => 'new-password', - 'value' => '', - 'requirements' => $showPasswordField, - ], - [ - 'field' => 'confirm_password', - 'label' => __('Confirm Password'), - 'type' => 'password', - 'required' => $passwordRequired, - 'autocomplete' => 'off', - 'requirements' => $showPasswordField, - ], - [ - 'field' => 'role_id', - 'type' => 'dropdown', - 'label' => __('Role'), - 'options' => $dropdownData['role'], - 'default' => $defaultRole ?? null - ], - [ - 'field' => 'disabled', - 'type' => 'checkbox', - 'label' => 'Disable' - ], +} +if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) { + $showPasswordField = true; +} +echo $this->element('genericElements/Form/genericForm', [ + 'data' => [ + 'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'), + 'model' => 'Roles', + 'fields' => [ + [ + 'field' => 'individual_id', + 'type' => 'dropdown', + 'label' => __('Associated individual'), + 'options' => isset($dropdownData['individual']) ? $dropdownData['individual'] : [], + 'conditions' => $this->request->getParam('action') === 'add' ], - 'submit' => [ - 'action' => $this->request->getParam('action') - ] + [ + 'field' => 'individual.email', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], + [ + 'field' => 'individual.first_name', + 'label' => 'First name', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], + [ + 'field' => 'individual.last_name', + 'label' => 'Last name', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], + [ + 'field' => 'username', + 'autocomplete' => 'off' + ], + [ + 'field' => 'organisation_id', + 'type' => 'dropdown', + 'label' => __('Associated organisation'), + 'options' => $dropdownData['organisation'], + 'default' => $loggedUser['organisation_id'] + ], + [ + 'field' => 'password', + 'label' => __('Password'), + 'type' => 'password', + 'required' => $passwordRequired, + 'autocomplete' => 'new-password', + 'value' => '', + 'requirements' => $showPasswordField, + ], + [ + 'field' => 'confirm_password', + 'label' => __('Confirm Password'), + 'type' => 'password', + 'required' => $passwordRequired, + 'autocomplete' => 'off', + 'requirements' => $showPasswordField, + ], + [ + 'field' => 'role_id', + 'type' => 'dropdown', + 'label' => __('Role'), + 'options' => $dropdownData['role'], + 'default' => $defaultRole ?? null + ], + [ + 'field' => 'disabled', + 'type' => 'checkbox', + 'label' => 'Disable' + ], + ], + 'submit' => [ + 'action' => $this->request->getParam('action') ] - ]); + ] +]); ?> - + + \ No newline at end of file From 480e4a65fe72e6de99ca8ee4d426fe8bf58901b3 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 10:43:59 +0100 Subject: [PATCH 23/34] fix: [elements:bootstrapTabs] Removed unused options --- src/View/Helper/BootstrapElements/BootstrapTabs.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/View/Helper/BootstrapElements/BootstrapTabs.php b/src/View/Helper/BootstrapElements/BootstrapTabs.php index e0ac5af..60beae5 100644 --- a/src/View/Helper/BootstrapElements/BootstrapTabs.php +++ b/src/View/Helper/BootstrapElements/BootstrapTabs.php @@ -215,8 +215,6 @@ class BootstrapTabs extends BootstrapGeneric ], [ "bg-{$this->options['header-variant']}", - "text-{$this->options['header-text-variant']}", - "border-{$this->options['header-border-variant']}" ] )], $this->genNav()); $content = $this->node('div', ['class' => array_merge( @@ -226,7 +224,6 @@ class BootstrapTabs extends BootstrapGeneric ], [ "bg-{$this->options['body-variant']}", - "text-{$this->options['body-text-variant']}" ] )], $this->genContent()); @@ -238,7 +235,6 @@ class BootstrapTabs extends BootstrapGeneric ($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '') ], [ - "border-{$this->options['header-border-variant']}" ] )], $containerContent); return $container; From 26c038b25b8cf1a4ec1d1ee31276e0caaaef6602 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 11:12:54 +0100 Subject: [PATCH 24/34] chg: [settings:cerebrate] Improved check before saving debug level --- src/Model/Table/SettingProviders/CerebrateSettingsProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index 3f9bb65..1876c7b 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -321,7 +321,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider 2 => __('Debug On + SQL Dump'), ], 'test' => function ($value, $setting, $validator) { - $validator->range('value', [0, 3]); + $validator->range('value', [0, 2]); return testValidator($value, $validator); }, ], From c8e5823393ae31016bbdbbb0244b1371eafe179b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 11:13:40 +0100 Subject: [PATCH 25/34] chg: [helper:bootstrap] Make sure to output the value even if it's a `0` --- src/View/Helper/BootstrapHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index f814116..122ca7c 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -450,7 +450,7 @@ class BootstrapGeneric return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value)); } return ''; - } else if (empty($value)) { + } else if (!isset($value)) { return ''; } return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value)); From ce8a7ba1beeab3bd19620bb2e53bc14be5a09202 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 12:14:13 +0100 Subject: [PATCH 26/34] fix: [individuals:canEdit] Changed function from public to private --- src/Controller/IndividualsController.php | 2 +- src/Controller/OrganisationsController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 236dff6..7a46fd2 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -134,7 +134,7 @@ class IndividualsController extends AppController } } - public function canEdit($indId): bool + private function canEdit($indId): bool { $currentUser = $this->ACL->getUser(); if ($currentUser['role']['perm_admin']) { diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php index 5de8063..9739d04 100644 --- a/src/Controller/OrganisationsController.php +++ b/src/Controller/OrganisationsController.php @@ -170,7 +170,7 @@ class OrganisationsController extends AppController } } - public function canEdit($orgId): bool + private function canEdit($orgId): bool { $currentUser = $this->ACL->getUser(); if ($currentUser['role']['perm_admin']) { From 6fc568e80e1440f60e3af962c1e2766177ef620c Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 12:16:36 +0100 Subject: [PATCH 27/34] new: [metaTemplateNameDirectory] Added index to see the known template and their associated saved meta-templates --- src/Controller/Component/ACLComponent.php | 3 + .../MetaTemplateNameDirectoryController.php | 35 +++++++++++ .../Table/MetaTemplateNameDirectoryTable.php | 12 +++- templates/MetaTemplateNameDirectory/index.php | 60 +++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/Controller/MetaTemplateNameDirectoryController.php create mode 100644 templates/MetaTemplateNameDirectory/index.php diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 4d37067..f7468e9 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -146,6 +146,9 @@ class ACLComponent extends Component 'toggle' => ['perm_admin'], 'view' => ['perm_admin'], ], + 'MetaTemplateNameDirectory' => [ + 'index' => ['perm_admin'], + ], 'Organisations' => [ 'add' => ['perm_admin'], 'delete' => ['perm_admin'], diff --git a/src/Controller/MetaTemplateNameDirectoryController.php b/src/Controller/MetaTemplateNameDirectoryController.php new file mode 100644 index 0000000..98c6884 --- /dev/null +++ b/src/Controller/MetaTemplateNameDirectoryController.php @@ -0,0 +1,35 @@ + true], 'uuid', 'version']; + public $filterFields = ['name', 'uuid', 'version']; + public $containFields = ['MetaTemplates']; + + + public function index() + { + $this->CRUD->index([ + 'filters' => $this->filterFields, + 'quickFilters' => $this->quickFilterFields, + 'contain' => $this->containFields, + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } +} diff --git a/src/Model/Table/MetaTemplateNameDirectoryTable.php b/src/Model/Table/MetaTemplateNameDirectoryTable.php index 32e0869..df39103 100644 --- a/src/Model/Table/MetaTemplateNameDirectoryTable.php +++ b/src/Model/Table/MetaTemplateNameDirectoryTable.php @@ -7,6 +7,7 @@ use App\Model\Entity\MetaTemplateNameDirectory; use App\Model\Table\AppTable; use Cake\ORM\RulesChecker; use Cake\Validation\Validator; +use Cake\Log\Log; class MetaTemplateNameDirectoryTable extends AppTable { @@ -20,6 +21,9 @@ class MetaTemplateNameDirectoryTable extends AppTable 'foreignKey' => 'meta_template_directory_id', ] ); + $this->hasOne('MetaTemplates', [ + 'foreignKey' => 'meta_template_directory_id', + ]); $this->setDisplayField('name'); } @@ -59,7 +63,11 @@ class MetaTemplateNameDirectoryTable extends AppTable if (!empty($existingTemplate)) { return $existingTemplate; } - $this->save($metaTemplateDirectory); - return $metaTemplateDirectory; + $savedEntity = $this->save($metaTemplateDirectory); + if ($savedEntity) { + return $metaTemplateDirectory; + } + Log::error(__('Could not save meta_template_directory. Reasons: {0}', json_encode($metaTemplateDirectory->getErrors()))); + return false; } } diff --git a/templates/MetaTemplateNameDirectory/index.php b/templates/MetaTemplateNameDirectory/index.php new file mode 100644 index 0000000..0b57d8c --- /dev/null +++ b/templates/MetaTemplateNameDirectory/index.php @@ -0,0 +1,60 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'search', + 'button' => __('Search'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value' + ] + ] + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'id', + ], + [ + 'name' => __('Name'), + 'sort' => 'name', + 'data_path' => 'name', + ], + [ + 'name' => __('Namespace'), + 'sort' => 'namespace', + 'data_path' => 'namespace', + ], + [ + 'name' => __('UUID'), + 'sort' => 'uuid', + 'data_path' => 'uuid' + ], + [ + 'name' => __('Version'), + 'sort' => 'version', + 'data_path' => 'version', + ], + [ + 'name' => __('Associated Meta-Template'), + 'sort' => 'meta_template.id', + 'data_path' => 'meta_template.id', + 'element' => 'function', + 'function' => function($row, $viewContext) { + return $viewContext->Bootstrap::node('a', [ + 'href' => h($baseurl . '/metaTemplates/view/' . $row->meta_template->id ?? ''), + ], !empty($row->meta_template->name) ? (sprintf('%s (v%s)', h($row->meta_template->name), h($row->meta_template->version))) :''); + } + ], + ], + 'title' => __('Meta Template Name Directory'), + 'description' => __('The directory of all meta templates known by the system.'), + 'actions' => [] + ] +]); From 3ca6b68429eabf34018631c7a3372747ff6fb602 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 27 Feb 2023 12:17:04 +0100 Subject: [PATCH 28/34] fix: [acl:metaTemplate] Added missing entry --- src/Controller/Component/ACLComponent.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index f7468e9..cbd1cb8 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -140,6 +140,7 @@ class ACLComponent extends Component 'enable' => ['perm_admin'], 'getMetaFieldsToUpdate' => ['perm_admin'], 'index' => ['perm_admin'], + 'migrateMetafieldsToNewestTemplate' => ['perm_admin'], 'migrateOldMetaTemplateToNewestVersionForEntity' => ['perm_admin'], 'update' => ['perm_admin'], 'updateAllTemplates' => ['perm_admin'], From acb66ac4a03ef81b4618d27c8d1f3a227d3ff95b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 08:05:32 +0100 Subject: [PATCH 29/34] fix: [individuals:delete] Gracefully catches deletion of individuals associated to a user --- src/Controller/Component/CRUDComponent.php | 6 ++++++ src/Controller/IndividualsController.php | 11 ++++++++++- templates/Individuals/index.php | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index d7cb5bf..d56609c 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -851,6 +851,9 @@ class CRUDComponent extends Component $query->contain($params['contain']); } $data = $query->first(); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } if (empty($data)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } @@ -873,6 +876,9 @@ class CRUDComponent extends Component $query->contain($params['contain']); } $data = $query->first(); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } if (isset($params['beforeSave'])) { try { $data = $params['beforeSave']($data); diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 7a46fd2..46e52e8 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -94,7 +94,16 @@ class IndividualsController extends AppController public function delete($id) { - $this->CRUD->delete($id); + $params = [ + 'contain' => ['Users'], + 'afterFind' => function($data, $params) { + if (!empty($data['user'])) { + throw new ForbiddenException(__('Individual associated to a user cannot be deleted.')); + } + return $data; + } + ]; + $this->CRUD->delete($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; diff --git a/templates/Individuals/index.php b/templates/Individuals/index.php index 4a662aa..d7d228f 100644 --- a/templates/Individuals/index.php +++ b/templates/Individuals/index.php @@ -103,6 +103,9 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'icon' => 'trash', 'complex_requirement' => [ 'function' => function ($row, $options) use ($loggedUser) { + if (!empty($row['user'])) { // cannot delete individuals with associated user(s) + return false; + } return (bool)$loggedUser['role']['perm_admin']; } ] From db19afd9ac9b6f86e3b2eb0e70c6bc4476b284b8 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 09:59:01 +0100 Subject: [PATCH 30/34] chg: [ui:select2] Added CSS file relying on BS variables instead of default theme hardcoded values --- templates/layout/default.php | 2 +- webroot/css/select2-bootstrap5-vars.css | 516 ++++++++++++++++++++++++ 2 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 webroot/css/select2-bootstrap5-vars.css diff --git a/templates/layout/default.php b/templates/layout/default.php index e810136..b6f807e 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -58,7 +58,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa Html->css('CodeMirror/addon/hint/show-hint') ?> Html->css('CodeMirror/addon/lint/lint') ?> Html->css('select2.min') ?> - Html->css('select2-bootstrap5') ?> + Html->css('select2-bootstrap5-vars') ?> Html->script('apexcharts.min') ?> Html->script('moment-with-locales.min') ?> Html->css('apexcharts') ?> diff --git a/webroot/css/select2-bootstrap5-vars.css b/webroot/css/select2-bootstrap5-vars.css new file mode 100644 index 0000000..c628064 --- /dev/null +++ b/webroot/css/select2-bootstrap5-vars.css @@ -0,0 +1,516 @@ +/*! + * Select2 v4 Bootstrap 5 theme v1.1.1 +*/ +.select2-container--bootstrap-5 { + display: block; + } + .select2-container--bootstrap-5 *:focus { + outline: 0; + } + .select2-container--bootstrap-5 .select2-selection { + width: 100%; + min-height: calc(1.5em + (0.75rem + 2px)); + padding: 0.375rem 0.75rem; + font-family: inherit; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-gray-400); + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + @media (prefers-reduced-motion: reduce) { + .select2-container--bootstrap-5 .select2-selection { + transition: none; + } + } + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .select2-container--bootstrap-5.select2-container--open .select2-selection { + border-color: rgba(var(--bs-primary-rgb), 0.5); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); + } + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection { + border-bottom: 1px solid transparent; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection { + border-top: 1px solid transparent; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .select2-container--bootstrap-5 .select2-search { + width: 100%; + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear, + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + position: absolute; + top: 50%; + right: 2.25rem; + width: 0.75rem; + height: 0.75rem; + padding: 0.25em 0.25em; + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat; + transform: translateY(-50%); + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover, + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear > span, + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear > span { + display: none; + } + + .select2-container--bootstrap-5 .select2-dropdown { + border-color: var(--bs-gray-400); + border-radius: 0.25rem; + } + .select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below { + border-top: 0 solid transparent; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + .select2-container--bootstrap-5 .select2-dropdown .select2-search { + padding: 0.375rem 0.75rem; + } + .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-family: inherit; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + background-clip: padding-box; + border: 1px solid var(--bs-gray-400); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + } + @media (prefers-reduced-motion: reduce) { + .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field { + transition: none; + } + } + .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field:focus { + border-color: rgba(var(--bs-primary-rgb), 0.5); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options:not(.select2-results__options--nested) { + max-height: 15rem; + overflow-y: auto; + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option { + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__message { + color: var(--bs-secondary); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--highlighted { + color: #000; + background-color: var(--bs-light); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--selected, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-selected=true] { + color: var(--bs-body-color); + background-color: var(--bs-primary); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--disabled, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-disabled=true] { + color: var(--bs-gray); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] { + padding: 0; + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group { + padding: 0.375rem 0.375rem; + font-weight: 500; + line-height: 1.5; + color: var(--bs-gray); + } + .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option { + padding: 0.375rem 0.75rem; + } + + .select2-container--bootstrap-5 .select2-selection--single { + padding: 0.375rem 2.25rem 0.375rem 0.75rem; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered { + padding: 0; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder { + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + } + .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__arrow { + display: none; + } + + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding-left: 0; + margin: 0; + list-style: none; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.35em 0.65em; + margin-right: 0.375rem; + margin-bottom: 0.375rem; + font-size: 1rem; + color: var(--bs-body-color); + cursor: auto; + border: 1px solid var(--bs-gray-400); + border-radius: 0.25rem; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove { + width: 0.75rem; + height: 0.75rem; + padding: 0.25em 0.25em; + margin-right: 0.25rem; + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat; + border: 0; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove > span { + display: none; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-search { + display: block; + width: 100%; + height: 1.5rem; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field { + width: 100%; + height: 1.5rem; + margin-top: 0; + margin-left: 0; + font-family: inherit; + line-height: 1.5; + background-color: transparent; + } + .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + right: 0.75rem; + } + + .select2-container--bootstrap-5.select2-container--disabled .select2-selection, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection { + color: var(--bs-secondary); + cursor: not-allowed; + background-color: var(--bs-gray-200); + border-color: var(--bs-gray-400); + box-shadow: none; + } + .select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__clear, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__clear { + display: none; + } + .select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice { + cursor: not-allowed; + } + .select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove { + display: none; + } + .select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty), .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) { + padding-bottom: 0; + } + .select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search { + display: none; + } + + .input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .input-group > .input-group-text ~ .select2-container--bootstrap-5 .select2-selection, + .input-group > .btn ~ .select2-container--bootstrap-5 .select2-selection, + .input-group > .dropdown-menu ~ .select2-container--bootstrap-5 .select2-selection { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .input-group .select2-container--bootstrap-5 { + flex-grow: 1; + } + .input-group .select2-container--bootstrap-5 .select2-selection { + height: 100%; + } + + .is-valid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5 .select2-selection { + border-color: var(--bs-success); + } + .is-valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-valid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open .select2-selection { + border-color: var(--bs-success); + box-shadow: 0 0 0 0.25rem rgba( var(--bs-success-rgb), 0.25); + } + .is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection { + border-bottom: 1px solid transparent; + } + .is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection { + border-top: 1px solid transparent; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .is-invalid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5 .select2-selection { + border-color: var(--bs-danger); + } + .is-invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection { + border-color: var(--bs-danger); + box-shadow: 0 0 0 0.25rem rgba( var(--bs-danger-rgb), 0.25); + } + .is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection { + border-bottom: 1px solid transparent; + } + .is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection { + border-top: 1px solid transparent; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .select2-container--bootstrap-5 .select2--small ~ .select2-selection { + min-height: calc(1.5em + (0.5rem + 2px)); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear, + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear { + width: 0.5rem; + height: 0.5rem; + padding: 0.125rem 0.125rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear:hover, + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-search .select2-search__field { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group { + padding: 0.25rem 0.25rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option { + padding: 0.25rem 0.5rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--single { + padding: 0.25rem 2.25rem 0.25rem 0.5rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) { + padding-bottom: 0.25rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice { + padding: 0.35em 0.65em; + font-size: 0.875rem; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove { + width: 0.5rem; + height: 0.5rem; + padding: 0.125rem 0.125rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear { + right: 0.5rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection { + min-height: calc(1.5em + (1rem + 2px)); + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear, + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear { + width: 1rem; + height: 1rem; + padding: 0.5rem 0.5rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear:hover, + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-search .select2-search__field { + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option { + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group { + padding: 0.5rem 0.5rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option { + padding: 0.5rem 1rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--single { + padding: 0.5rem 2.25rem 0.5rem 1rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) { + padding-bottom: 0.5rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice { + padding: 0.35em 0.65em; + font-size: 1.25rem; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove { + width: 1rem; + height: 1rem; + padding: 0.5rem 0.5rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear { + right: 1rem; + } + + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection { + min-height: calc(1.5em + (0.5rem + 2px)); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear, + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + width: 0.5rem; + height: 0.5rem; + padding: 0.125rem 0.125rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover, + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group { + padding: 0.25rem 0.25rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option { + padding: 0.25rem 0.5rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single { + padding: 0.25rem 2.25rem 0.25rem 0.5rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) { + padding-bottom: 0.25rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice { + padding: 0.35em 0.65em; + font-size: 0.875rem; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove { + width: 0.5rem; + height: 0.5rem; + padding: 0.125rem 0.125rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat; + } + .form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + right: 0.5rem; + } + + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection { + min-height: calc(1.5em + (1rem + 2px)); + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear, + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + width: 1rem; + height: 1rem; + padding: 0.5rem 0.5rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover, + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field { + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option { + padding: 0.5rem 1rem; + font-size: 1.25rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group { + padding: 0.5rem 0.5rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option { + padding: 0.5rem 1rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single { + padding: 0.5rem 2.25rem 0.5rem 1rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) { + padding-bottom: 0.5rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice { + padding: 0.35em 0.65em; + font-size: 1.25rem; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove { + width: 1rem; + height: 1rem; + padding: 0.5rem 0.5rem; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover { + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat; + } + .form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear { + right: 1rem; + } \ No newline at end of file From 5b07c57037f6840079a9c74cef6517b1aad15b37 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 10:02:33 +0100 Subject: [PATCH 31/34] fix: [elements:dropdownField] Always attach select2 to the body --- templates/MailingLists/add_individual.php | 1 - .../genericElements/Form/Fields/dropdownField.php | 9 ++++++--- templates/element/genericElements/Form/genericForm.php | 9 --------- webroot/css/main.css | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/templates/MailingLists/add_individual.php b/templates/MailingLists/add_individual.php index 0455d22..beb19da 100644 --- a/templates/MailingLists/add_individual.php +++ b/templates/MailingLists/add_individual.php @@ -10,7 +10,6 @@ echo $this->element('genericElements/Form/genericForm', [ 'multiple' => true, 'select2' => true, 'label' => __('Members'), - 'class' => 'select2-input', 'options' => $dropdownData['individuals'] ], [ diff --git a/templates/element/genericElements/Form/Fields/dropdownField.php b/templates/element/genericElements/Form/Fields/dropdownField.php index 50be756..5f84e5c 100644 --- a/templates/element/genericElements/Form/Fields/dropdownField.php +++ b/templates/element/genericElements/Form/Fields/dropdownField.php @@ -19,7 +19,9 @@ if (!empty($fieldData['label'])) { if ($controlParams['options'] instanceof \Cake\ORM\Query) { $controlParams['options'] = $controlParams['options']->all()->toList(); } -if (!empty($fieldData['select2'])) { +$initSelect2 = false; +if (isset($fieldData['select2']) && $fieldData['select2'] == true) { + $initSelect2 = true; $fieldData['select2'] = $fieldData['select2'] === true ? [] : $fieldData['select2']; $controlParams['class'] .= ' select2-input'; } @@ -53,8 +55,9 @@ echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $f $select.attr('onclick', 'toggleFreetextSelectField(this)') $select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)') updateAssociatedSelect($select.parent().find('input.custom-value')[0]) - - let $container = $select.closest('.modal-dialog') + + // let $container = $select.closest('.modal-dialog .modal-body') + let $container = [] if ($container.length == 0) { $container = $(document.body) } diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index 5a91e78..c0164a7 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -155,14 +155,5 @@ $('.formDropdown').on('change', function() { executeStateDependencyChecks('#' + this.id); }) - - - $('select.select2-input').select2({ - dropdownParent: , - width: '100%', - }) - }); \ No newline at end of file diff --git a/webroot/css/main.css b/webroot/css/main.css index 562e523..391019d 100644 --- a/webroot/css/main.css +++ b/webroot/css/main.css @@ -94,7 +94,7 @@ input[type="checkbox"]:disabled.change-cursor { } .select2-container { - z-index: 900; + z-index: 1060; } .select2-container--bootstrap-5 { From 47bebe5b6807c644ef3bfc79a3882c3bf2d420d6 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 11:37:58 +0100 Subject: [PATCH 32/34] chg: [metaTemplate:update] Gracefully handle case when template on disk is not readable --- src/Controller/MetaTemplatesController.php | 12 +++++++++++- src/Model/Table/MetaTemplatesTable.php | 11 +++++++++++ templates/MetaTemplates/update.php | 8 ++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index 0c59120..ac8cd2e 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -404,8 +404,18 @@ class MetaTemplatesController extends AppController $metaTemplate = $this->MetaTemplates->get($template_id, [ 'contain' => ['MetaTemplateFields'] ]); - $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid); + $error = ''; + $errorMessage = ''; + $templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid, $error); + if (is_null($templateOnDisk)) { + $errorMessage = __('Could not retreive template\'s status. Reason: {0}', $error); + $this->Flash->error($errorMessage); + $templateOnDisk = []; + } $templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate); + if (!empty($errorMessage)) { + $templateStatus['error'] = $errorMessage; + } $this->set('templateOnDisk', $templateOnDisk); $this->set('templateStatus', $templateStatus); return [ diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php index 57925a6..d42c97d 100644 --- a/src/Model/Table/MetaTemplatesTable.php +++ b/src/Model/Table/MetaTemplatesTable.php @@ -555,6 +555,10 @@ class MetaTemplatesTable extends AppTable if (substr($file, -5) === '.json') { $errorMessage = ''; $template = $this->decodeTemplateFromDisk($path . $file, $errorMessage); + if (!empty($errorMessage)) { + $error = $errorMessage; + return null; + } if (!empty($template) && $template['uuid'] == $uuid) { return $template; } @@ -1318,6 +1322,13 @@ class MetaTemplatesTable extends AppTable $updateStatus['next_version'] = $template['version']; $updateStatus['new'] = false; $updateStatus['automatically-updateable'] = false; + $updateStatus['conflicts'] = []; + if (empty($template)) { + $updateStatus['up-to-date'] = false; + $updateStatus['automatically-updateable'] = false; + $updateStatus['can-be-removed'] = false; + return $updateStatus; + } if (intval($metaTemplate->version) >= intval($template['version'])) { $updateStatus['up-to-date'] = true; $updateStatus['conflicts'][] = __('Could not update the template. Local version is equal or newer.'); diff --git a/templates/MetaTemplates/update.php b/templates/MetaTemplates/update.php index a1c1821..c01b620 100644 --- a/templates/MetaTemplates/update.php +++ b/templates/MetaTemplates/update.php @@ -11,6 +11,14 @@ if ($updateStatus['up-to-date']) { 'dismissible' => false, ]); $modalType = 'ok-only'; +} else if (empty($updateStatus['templateOnDisk'])) { + $diskTemplateError = $templateStatus['error'] ?? __('Unknown'); + $bodyHtml .= $this->Bootstrap->alert([ + 'variant' => 'danger', + 'html' => sprintf('%s %s

%s

', __('Could not get template on disk.'), __('Reason:'), h($diskTemplateError)), + 'dismissible' => false, + ]); + $modalType = 'ok-only'; } else { if ($updateStatus['automatically-updateable']) { $bodyHtml .= $this->Bootstrap->alert([ From c2e9fd3b754087e1551a9bc57c165dbcf7e5f347 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 11:38:32 +0100 Subject: [PATCH 33/34] chg: [meta-template:index] Added link to metaTemplateNameDirectory --- src/Controller/Component/Navigation/MetaTemplates.php | 11 ++++++++++- .../element/layouts/header/header-breadcrumb.php | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php index c9c6707..e4cec76 100644 --- a/src/Controller/Component/Navigation/MetaTemplates.php +++ b/src/Controller/Component/Navigation/MetaTemplates.php @@ -56,6 +56,12 @@ class MetaTemplatesNavigation extends BaseNavigation 'url' => '/metaTemplates/prune_outdated_template', 'isPOST' => true, ]); + $this->bcf->addRoute('MetaTemplates', 'view_template_directory', [ + 'label' => __('View all known templates'), + 'icon' => 'list', + 'url' => '/metaTemplateNameDirectory/index', + 'isRedirect' => true, + ]); } public function addParents() @@ -78,7 +84,7 @@ class MetaTemplatesNavigation extends BaseNavigation $totalUpdateCount = $udpateCount + $newCount; } $updateAllActionConfig = [ - 'label' => __('Update all template'), + 'label' => __('Update all templates'), 'url' => '/metaTemplates/updateAllTemplates', 'url_vars' => ['id' => 'id'], ]; @@ -94,6 +100,9 @@ class MetaTemplatesNavigation extends BaseNavigation 'label' => __('Prune outdated templates'), 'url' => '/metaTemplates/prune_outdated_template', ]); + $this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'view_template_directory', [ + 'isRedirect' => true, + ]); if (empty($this->viewVars['templateStatus']['up-to-date'])) { $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [ diff --git a/templates/element/layouts/header/header-breadcrumb.php b/templates/element/layouts/header/header-breadcrumb.php index bdf5a1b..5c1f05c 100644 --- a/templates/element/layouts/header/header-breadcrumb.php +++ b/templates/element/layouts/header/header-breadcrumb.php @@ -81,6 +81,8 @@ if (!empty($breadcrumb)) { } if (!empty($actionEntry['isPOST'])) { $onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url']))); + } else if (!empty($actionEntry['isRedirect'])) { + $onclickFunction = sprintf('window.location.replace(\'%s\');', h(Router::url($actionEntry['url']))); } else { $onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.modalFromUrl(\'%s\'))', h(Router::url($actionEntry['url']))); } From dbf18da087cef6660c678b62da47f3a982cbf940 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 13 Mar 2023 11:40:47 +0100 Subject: [PATCH 34/34] fix: [meta-template:update] Typo in variable name --- templates/MetaTemplates/update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/MetaTemplates/update.php b/templates/MetaTemplates/update.php index c01b620..f2178bd 100644 --- a/templates/MetaTemplates/update.php +++ b/templates/MetaTemplates/update.php @@ -11,7 +11,7 @@ if ($updateStatus['up-to-date']) { 'dismissible' => false, ]); $modalType = 'ok-only'; -} else if (empty($updateStatus['templateOnDisk'])) { +} else if (empty($templateOnDisk)) { $diskTemplateError = $templateStatus['error'] ?? __('Unknown'); $bodyHtml .= $this->Bootstrap->alert([ 'variant' => 'danger',