diff --git a/config/Migrations/20221025000000_inbox_severity.php b/config/Migrations/20221025000000_inbox_severity.php
new file mode 100644
index 0000000..d452856
--- /dev/null
+++ b/config/Migrations/20221025000000_inbox_severity.php
@@ -0,0 +1,45 @@
+table('inbox')->hasColumn('severity');
+ if (!$exists) {
+ $this->table('inbox')
+ ->addColumn('severity', 'integer', [
+ 'null' => false,
+ 'default' => 0,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->renameColumn('comment', 'message')
+ ->removeColumn('description')
+ ->update();
+ $this->table('outbox')
+ ->addColumn('severity', 'integer', [
+ 'null' => false,
+ 'default' => 0,
+ 'signed' => false,
+ 'length' => 10,
+ ])
+ ->renameColumn('comment', 'message')
+ ->removeColumn('description')
+ ->update();
+ }
+ }
+}
diff --git a/config/Migrations/20221028000000_MetaFieldSaneDefault.php b/config/Migrations/20221028000000_MetaFieldSaneDefault.php
new file mode 100644
index 0000000..b59149c
--- /dev/null
+++ b/config/Migrations/20221028000000_MetaFieldSaneDefault.php
@@ -0,0 +1,34 @@
+table('meta_template_fields')->hasColumn('sane_default');
+ if (!$exists) {
+ $this->table('meta_template_fields')
+ ->addColumn('sane_default', 'text', [
+ 'default' => null,
+ 'null' => true,
+ 'limit' => MysqlAdapter::TEXT_LONG,
+ 'comment' => 'List of sane default values to be proposed',
+ ])
+ ->addColumn('values_list', 'text', [
+ 'default' => null,
+ 'null' => true,
+ 'limit' => MysqlAdapter::TEXT_LONG,
+ 'comment' => 'List of values that have to be used',
+ ])
+ ->update();
+ }
+ }
+}
diff --git a/libraries/default/InboxProcessors/GenericInboxProcessor.php b/libraries/default/InboxProcessors/GenericInboxProcessor.php
index 1ca84ea..fd2e09d 100644
--- a/libraries/default/InboxProcessors/GenericInboxProcessor.php
+++ b/libraries/default/InboxProcessors/GenericInboxProcessor.php
@@ -19,9 +19,12 @@ class GenericInboxProcessor
protected $validator;
protected $processingTemplate = '/genericTemplates/confirm';
protected $processingTemplatesDirectory = ROOT . '/libraries/default/InboxProcessors/templates';
+ protected $defaultSeverity;
+ protected $severity;
public function __construct($registerActions=false) {
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
+ $this->defaultSeverity = $this->Inbox::SEVERITY_INFO;
if ($registerActions) {
$this->registerActionInProcessor();
}
@@ -54,6 +57,10 @@ class GenericInboxProcessor
{
return $this->description ?? '';
}
+ public function getSeverity()
+ {
+ return $this->severity ?? $this->defaultSeverity;
+ }
protected function getProcessingTemplatePath()
{
@@ -76,8 +83,9 @@ class GenericInboxProcessor
$builder = new ViewBuilder();
$builder->disableAutoLayout()
->setClassName('Monad')
- ->setTemplate($processingTemplate);
- $view = $builder->build($viewVariables);
+ ->setTemplate($processingTemplate)
+ ->setVars($viewVariables);
+ $view = $builder->build();
$view->setRequest($serverRequest);
return $view->render();
}
@@ -191,7 +199,7 @@ class GenericInboxProcessor
{
$requestData['scope'] = $this->scope;
$requestData['action'] = $this->action;
- $requestData['description'] = $this->description;
+ $requestData['severity'] = $this->getSeverity();
$request = $this->generateRequest($requestData);
$savedRequest = $this->Inbox->createEntry($request);
return $this->genActionResult(
diff --git a/libraries/default/InboxProcessors/LocalToolInboxProcessor.php b/libraries/default/InboxProcessors/LocalToolInboxProcessor.php
index 2dacab8..b2d84b0 100644
--- a/libraries/default/InboxProcessors/LocalToolInboxProcessor.php
+++ b/libraries/default/InboxProcessors/LocalToolInboxProcessor.php
@@ -179,6 +179,7 @@ class IncomingConnectionRequestProcessor extends LocalToolInboxProcessor impleme
public function __construct() {
parent::__construct();
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase I of inter-connection when another cerebrate instance performs the request.');
}
@@ -291,6 +292,7 @@ class AcceptedRequestProcessor extends LocalToolInboxProcessor implements Generi
public function __construct() {
parent::__construct();
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase II of inter-connection when initial request has been accepted by the remote cerebrate.');
}
@@ -367,6 +369,7 @@ class DeclinedRequestProcessor extends LocalToolInboxProcessor implements Generi
public function __construct() {
parent::__construct();
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle Phase II of MISP inter-connection when initial request has been declined by the remote cerebrate.');
}
diff --git a/libraries/default/InboxProcessors/NotificationInboxProcessor.php b/libraries/default/InboxProcessors/NotificationInboxProcessor.php
new file mode 100644
index 0000000..081b424
--- /dev/null
+++ b/libraries/default/InboxProcessors/NotificationInboxProcessor.php
@@ -0,0 +1,97 @@
+description = __('Notify when data has been changed');
+ $this->severity = $this->Inbox::SEVERITY_PRIMARY;
+ }
+
+ protected function addValidatorRules($validator)
+ {
+ return $validator
+ ->requirePresence('original', 'changed')
+ ->isArray('original', __('The `original` data must be provided'))
+ ->isArray('changed', __('The `changed` data must be provided'));
+ }
+
+ public function create($requestData) {
+ $this->validateRequestData($requestData);
+ return parent::create($requestData);
+ }
+
+ public function getViewVariables($request)
+ {
+ $request = $this->__decodeMoreFields($request);
+ return [
+ 'data' => [
+ 'original' => $request->data['original'],
+ 'changed' => $request->data['changed'],
+ 'summaryTemplate' => $request->data['summaryTemplate'],
+ 'summaryMessage' => $request->data['summaryMessage'],
+ 'entityType' => $request->data['entityType'],
+ 'entityDisplayField' => $request->data['entityDisplayField'],
+ 'entityViewURL' => $request->data['entityViewURL'],
+ ],
+ ];
+ }
+
+ public function process($id, $requestData, $inboxRequest)
+ {
+ $this->discard($id, $requestData);
+ return $this->genActionResult(
+ [],
+ true,
+ __('Notification acknowledged'),
+ []
+ );
+ }
+
+ public function discard($id, $requestData)
+ {
+ return parent::discard($id, $requestData);
+ }
+
+ private function __decodeMoreFields($request)
+ {
+ $decodedRequest = $request;
+ $decodedRequest->data['original'] = $this->__decodeMoreChangedFields($request->data['original']);
+ $decodedRequest->data['changed'] = $this->__decodeMoreChangedFields($request->data['changed']);
+ return $decodedRequest;
+ }
+
+ private function __decodeMoreChangedFields(array $fieldData): array
+ {
+ array_walk($fieldData, function(&$fieldValue, $fieldName) {
+ if ($fieldName === 'meta_fields') {
+ $fieldValue = json_decode($fieldValue, true);
+ }
+ });
+ return $fieldData;
+ }
+}
\ No newline at end of file
diff --git a/libraries/default/InboxProcessors/SynchronisationInboxProcessor.php b/libraries/default/InboxProcessors/SynchronisationInboxProcessor.php
index 2a243cd..d95154e 100644
--- a/libraries/default/InboxProcessors/SynchronisationInboxProcessor.php
+++ b/libraries/default/InboxProcessors/SynchronisationInboxProcessor.php
@@ -28,6 +28,7 @@ class DataExchangeProcessor extends SynchronisationInboxProcessor implements Gen
public function __construct() {
parent::__construct();
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle exchange of data between two cerebrate instances');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
diff --git a/libraries/default/InboxProcessors/UserInboxProcessor.php b/libraries/default/InboxProcessors/UserInboxProcessor.php
index abb9550..96656c1 100644
--- a/libraries/default/InboxProcessors/UserInboxProcessor.php
+++ b/libraries/default/InboxProcessors/UserInboxProcessor.php
@@ -31,6 +31,7 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
public function __construct() {
parent::__construct();
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->description = __('Handle user account for this cerebrate instance');
}
diff --git a/libraries/default/InboxProcessors/templates/Notification/DataChange.php b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
new file mode 100644
index 0000000..952fae1
--- /dev/null
+++ b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
@@ -0,0 +1,59 @@
+%s',
+ h($data['entityViewURL']),
+ h($data['entityDisplayField'])
+ )
+ );
+} else {
+ $changedSummary = '';
+}
+
+$form = $this->element('genericElements/Form/genericForm', [
+ 'entity' => null,
+ 'ajax' => false,
+ 'raw' => true,
+ 'data' => [
+ 'model' => 'Inbox',
+ 'fields' => [],
+ 'submit' => [
+ 'action' => $this->request->getParam('action')
+ ]
+ ]
+]);
+
+echo $this->Bootstrap->modal([
+ 'title' => __('Acknowledge notification'),
+ 'size' => 'xl',
+ 'type' => 'confirm',
+ 'bodyHtml' => sprintf(
+ '
%s
+ ',
+ $form,
+ $changedSummary,
+ $this->Bootstrap->card([
+ 'headerText' => __('Original values'),
+ 'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['original']]])
+ ]),
+ $this->Bootstrap->card([
+ 'headerText' => __('Changed values'),
+ 'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
+ ])
+ ),
+ 'confirmText' => __('Acknowledge & Discard'),
+ 'confirmIcon' => 'check',
+]);
+?>
+
diff --git a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php
index 9bf79ae..8bde905 100644
--- a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php
+++ b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php
@@ -70,6 +70,7 @@ class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements Gene
public function __construct() {
parent::__construct();
$this->description = __('Handle re-sending messages that failed to be received from other cerebrate instances.');
+ $this->severity = $this->Inbox::SEVERITY_WARNING;
$this->Broods = TableRegistry::getTableLocator()->get('Broods');
$this->LocalTools = \Cake\ORM\TableRegistry::getTableLocator()->get('LocalTools');
}
diff --git a/libraries/default/OutboxProcessors/GenericOutboxProcessor.php b/libraries/default/OutboxProcessors/GenericOutboxProcessor.php
index c230465..6cd9cfb 100644
--- a/libraries/default/OutboxProcessors/GenericOutboxProcessor.php
+++ b/libraries/default/OutboxProcessors/GenericOutboxProcessor.php
@@ -20,9 +20,14 @@ class GenericOutboxProcessor
protected $validator;
protected $processingTemplate = '/genericTemplates/confirm';
protected $processingTemplatesDirectory = ROOT . '/libraries/default/OutboxProcessors/templates';
+ protected $defaultSeverity;
+ protected $severity;
+
public function __construct($registerActions=false) {
$this->Outbox = TableRegistry::getTableLocator()->get('Outbox');
+ $this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
+ $this->defaultSeverity = $this->Inbox::SEVERITY_INFO;
if ($registerActions) {
$this->registerActionInProcessor();
}
@@ -55,6 +60,10 @@ class GenericOutboxProcessor
{
return $this->description ?? '';
}
+ public function getSeverity()
+ {
+ return $this->severity ?? $this->defaultSeverity;
+ }
protected function getProcessingTemplatePath()
{
@@ -77,8 +86,9 @@ class GenericOutboxProcessor
$builder = new ViewBuilder();
$builder->disableAutoLayout()
->setClassName('Monad')
- ->setTemplate($processingTemplate);
- $view = $builder->build($viewVariables);
+ ->setTemplate($processingTemplate)
+ ->setVars($viewVariables);
+ $view = $builder->build();
$view->setRequest($serverRequest);
return $view->render();
}
@@ -193,7 +203,7 @@ class GenericOutboxProcessor
$user_id = Router::getRequest()->getSession()->read('Auth.id');
$requestData['scope'] = $this->scope;
$requestData['action'] = $this->action;
- $requestData['description'] = $this->description;
+ $requestData['severity'] = $this->getSeverity();
$requestData['user_id'] = $user_id;
$request = $this->generateRequest($requestData);
$savedRequest = $this->Outbox->createEntry($request);
diff --git a/plugins/Tags/src/Model/Behavior/TagBehavior.php b/plugins/Tags/src/Model/Behavior/TagBehavior.php
index 5772d82..5d69ed2 100644
--- a/plugins/Tags/src/Model/Behavior/TagBehavior.php
+++ b/plugins/Tags/src/Model/Behavior/TagBehavior.php
@@ -189,6 +189,8 @@ class TagBehavior extends Behavior
{
if (is_object($tag)) {
return $tag->name;
+ } else if (is_array($tag)) {
+ return $tag['name'];
} else {
return trim($tag);
}
diff --git a/src/Command/SummaryCommand.php b/src/Command/SummaryCommand.php
index 721d885..cf5debf 100644
--- a/src/Command/SummaryCommand.php
+++ b/src/Command/SummaryCommand.php
@@ -11,7 +11,7 @@ use Cake\Console\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
-use Cake\Filesystem\File;
+use Cake\Filesystem\Folder;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\Validation\Validator;
@@ -22,7 +22,7 @@ class SummaryCommand extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
- $parser->setDescription('Create a summary for data associated to the passed nationality that has been modified.');
+ $parser->setDescription('Create a summary for data associated to the passed nationality that has been modified. Summaries will be printed out in STDIN and written in individual `txt` files.');
$parser->addArgument('nationality', [
'short' => 'n',
'help' => 'The organisation nationality.',
@@ -33,6 +33,11 @@ class SummaryCommand extends Command
'help' => 'The amount of days to look back in the logs',
'default' => 7
]);
+ $parser->addOption('output', [
+ 'short' => 'o',
+ 'help' => 'The destination folder where to write the files',
+ 'default' => '/tmp'
+ ]);
return $parser;
}
@@ -49,15 +54,16 @@ class SummaryCommand extends Command
}
foreach ($nationalities as $nationality) {
$this->io->out(sprintf('Nationality: %s', $nationality));
- $this->_collectChangedForNationality($nationality, $days);
+ $this->_collectChangedForNationality($nationality, $days, $args->getOption('output'));
$this->io->out($io->nl(2));
$this->io->hr();
}
}
- protected function _collectChangedForNationality($nationality, $days)
+ protected function _collectChangedForNationality($nationality, $days, $folderPath)
{
- $filename = sprintf('/tmp/%s.txt', $nationality);
+ $folderPath = rtrim($folderPath, '/');
+ $filename = sprintf('%s/%s.txt', $folderPath, $nationality);
$file_input = fopen($filename, 'w');
$organisationIDsForNationality = $this->_fetchOrganisationsForNationality($nationality);
if (empty($organisationIDsForNationality)) {
diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php
index 9e8aef8..238dc46 100644
--- a/src/Controller/AppController.php
+++ b/src/Controller/AppController.php
@@ -122,6 +122,7 @@ class AppController extends Controller
$this->set('loggedUser', $this->ACL->getUser());
$this->set('roleAccess', $this->ACL->getRoleAccess(false, false));
}
+ Configure::write('loggedUser', $user);
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
diff --git a/src/Controller/AuditLogsController.php b/src/Controller/AuditLogsController.php
index fd1be94..e3e4e0c 100644
--- a/src/Controller/AuditLogsController.php
+++ b/src/Controller/AuditLogsController.php
@@ -8,11 +8,10 @@ use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Core\Configure;
-use PhpParser\Node\Stmt\Echo_;
class AuditLogsController extends AppController
{
- public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title'];
+ public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title', 'AuditLogs.created'];
public $quickFilterFields = ['model', 'request_action', 'model_title'];
public $containFields = ['Users'];
diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php
index 1808f42..8d2f08f 100644
--- a/src/Controller/Component/CRUDComponent.php
+++ b/src/Controller/Component/CRUDComponent.php
@@ -9,6 +9,7 @@ use Cake\Utility\Inflector;
use Cake\Utility\Text;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
+use Cake\ORM\Query;
use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
@@ -48,6 +49,8 @@ class CRUDComponent extends Component
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
foreach ($optionFilters as $i => $filter) {
$optionFilters[] = "{$filter} !=";
+ $optionFilters[] = "{$filter} >=";
+ $optionFilters[] = "{$filter} <=";
}
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
$params = $this->fakeContextFilter($options, $params);
@@ -112,6 +115,7 @@ class CRUDComponent extends Component
if ($this->metaFieldsSupported()) {
$query = $this->includeRequestedMetaFields($query);
}
+ $this->setRequestedEntryAmount();
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
if (isset($options['afterFind'])) {
$function = $options['afterFind'];
@@ -195,16 +199,26 @@ class CRUDComponent extends Component
$metaTemplates = $this->getMetaTemplates()->toArray();
$this->Controller->set('metaFieldsEnabled', true);
$this->Controller->set('metaTemplates', $metaTemplates);
+ $typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
+ $typeHandlersOperators = [];
+ foreach ($typeHandlers as $type => $handler) {
+ $typeHandlersOperators[$type] = $handler::OPERATORS;
+ }
+ $this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
} else {
$this->Controller->set('metaFieldsEnabled', false);
}
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
- $typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
- $typeHandlersOperators = [];
- foreach ($typeHandlers as $type => $handler) {
- $typeHandlersOperators[$type] = $handler::OPERATORS;
- }
- $this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
+ $typeMap = $this->Table->getSchema()->typeMap();
+ $associatedtypeMap = !empty($this->Controller->filterFields) ? $this->_getAssociatedTypeMap() : [];
+ $typeMap = array_merge(
+ $this->Table->getSchema()->typeMap(),
+ $associatedtypeMap
+ );
+ $typeMap = array_filter($typeMap, function ($field) use ($filters) {
+ return in_array($field, $filters);
+ }, ARRAY_FILTER_USE_KEY);
+ $this->Controller->set('typeMap', $typeMap);
$this->Controller->set('filters', $filters);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
@@ -466,6 +480,7 @@ class CRUDComponent extends Component
}
$entity->setDirty('meta_fields', true);
+ $entity->_metafields_to_delete = $metaFieldsToDelete;
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
}
@@ -506,7 +521,7 @@ class CRUDComponent extends Component
$params['contain'] = [$params['contain'], 'MetaFields'];
}
}
- $query = $this->Table->find()->where(['id' => $id]);
+ $query = $this->Table->find()->where(["{$this->TableAlias}.id" => $id]);
if (!empty($params['contain'])) {
$query->contain($params['contain']);
}
@@ -713,6 +728,15 @@ class CRUDComponent extends Component
]);
}
+ protected function setRequestedEntryAmount()
+ {
+ $user = $this->Controller->ACL->getUser();
+ $tableSettings = IndexSetting::getTableSetting($user, $this->Table);
+ if (!empty($tableSettings['number_of_element'])) {
+ $this->Controller->paginate['limit'] = intval($tableSettings['number_of_element']);
+ }
+ }
+
public function view(int $id, array $params = []): void
{
if (empty($id)) {
@@ -1309,7 +1333,7 @@ class CRUDComponent extends Component
{
if (empty($params['filteringLabel']) && !empty($options['contextFilters']['custom'])) {
foreach ($options['contextFilters']['custom'] as $contextFilter) {
- if (!empty($contextFilter['default'])) {
+ if (!empty($contextFilter['default']) && empty($params)) {
$params['filteringLabel'] = $contextFilter['label'];
$this->Controller->set('fakeFilteringLabel', $contextFilter['label']);
break;
@@ -1434,7 +1458,7 @@ class CRUDComponent extends Component
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
- $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationErrors);
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', [], $message, $validationErrors);
} else {
$this->Controller->Flash->error($message);
if (empty($params['redirect'])) {
@@ -1534,4 +1558,24 @@ class CRUDComponent extends Component
$view = $builder->build($data);
return $view->render();
}
+
+ protected function _getAssociatedTypeMap(): array
+ {
+ $typeMap = [];
+ foreach ($this->Controller->filterFields as $filter) {
+ $exploded = explode('.', $filter);
+ if (count($exploded) > 1) {
+ $model = $exploded[0];
+ $subField = $exploded[1];
+ if ($model == $this->Table->getAlias()) {
+ $typeMap[$filter] = $this->Table->getSchema()->typeMap()[$subField] ?? 'text';
+ } else {
+ $association = $this->Table->associations()->get($model);
+ $associatedTable = $association->getTarget();
+ $typeMap[$filter] = $associatedTable->getSchema()->typeMap()[$subField] ?? 'text';
+ }
+ }
+ }
+ return $typeMap;
+ }
}
diff --git a/src/Controller/Component/Navigation/Inbox.php b/src/Controller/Component/Navigation/Inbox.php
index 54da6c2..78eba2e 100644
--- a/src/Controller/Component/Navigation/Inbox.php
+++ b/src/Controller/Component/Navigation/Inbox.php
@@ -10,13 +10,13 @@ class InboxNavigation extends BaseNavigation
$this->bcf->addRoute('Inbox', 'index', $this->bcf->defaultCRUD('Inbox', 'index'));
$this->bcf->addRoute('Inbox', 'view', $this->bcf->defaultCRUD('Inbox', 'view'));
$this->bcf->addRoute('Inbox', 'discard', [
- 'label' => __('Discard request'),
+ 'label' => __('Discard message'),
'icon' => 'trash',
'url' => '/inbox/discard/{{id}}',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('Inbox', 'process', [
- 'label' => __('Process request'),
+ 'label' => __('Process message'),
'icon' => 'cogs',
'url' => '/inbox/process/{{id}}',
'url_vars' => ['id' => 'id'],
diff --git a/src/Controller/Component/Navigation/Outbox.php b/src/Controller/Component/Navigation/Outbox.php
index 8926ac4..39d4717 100644
--- a/src/Controller/Component/Navigation/Outbox.php
+++ b/src/Controller/Component/Navigation/Outbox.php
@@ -10,13 +10,13 @@ class OutboxNavigation extends BaseNavigation
$this->bcf->addRoute('Outbox', 'index', $this->bcf->defaultCRUD('Outbox', 'index'));
$this->bcf->addRoute('Outbox', 'view', $this->bcf->defaultCRUD('Outbox', 'view'));
$this->bcf->addRoute('Outbox', 'discard', [
- 'label' => __('Discard request'),
+ 'label' => __('Discard message'),
'icon' => 'trash',
'url' => '/outbox/discard/{{id}}',
'url_vars' => ['id' => 'id'],
]);
$this->bcf->addRoute('Outbox', 'process', [
- 'label' => __('Process request'),
+ 'label' => __('Process message'),
'icon' => 'cogs',
'url' => '/outbox/process/{{id}}',
'url_vars' => ['id' => 'id'],
diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php
index 0d07fb9..2abfb47 100644
--- a/src/Controller/InboxController.php
+++ b/src/Controller/InboxController.php
@@ -16,8 +16,8 @@ use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
- public $filterFields = ['scope', 'action', 'title', 'origin', 'comment'];
- public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
+ public $filterFields = ['scope', 'action', 'Inbox.created', 'severity', 'title', 'origin', 'message', 'Users.id', 'Users.username',];
+ public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin'];
public $containFields = ['Users'];
public $paginate = [
@@ -39,9 +39,102 @@ class InboxController extends AppController
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contextFilters' => [
- 'fields' => [
- 'scope',
- ]
+ 'custom' => [
+ [
+ 'default' => true,
+ 'label' => __('My Notifications'),
+ 'filterConditionFunction' => function ($query) {
+ return $query->where(function(QueryExpression $exp) {
+ return $exp->or(['user_id' => $this->ACL->getUser()['id']])
+ ->isNull('user_id');
+ });
+ }
+ ],
+ [
+ 'label' => __('User Registration'),
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'scope' => 'User',
+ 'action' => 'Registration',
+ ]);
+ }
+ ],
+ [
+ 'label' => __('Inter-connection Requests'),
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'scope' => 'LocalTool',
+ 'action IN' => ['IncomingConnectionRequest', 'AcceptedRequest', 'DeclinedRequest'],
+ ]);
+ }
+ ],
+ [
+ 'label' => __('Data changed'),
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'user_id' => $this->ACL->getUser()['id'], // Each admin get a message about data changes
+ 'scope' => 'Notification',
+ 'action' => 'DataChange',
+ ]);
+ }
+ ],
+ [
+ 'label' => 'severity:primary',
+ 'viewElement' => 'bootstrapUI',
+ 'viewElementParams' => [
+ 'element' => 'badge',
+ 'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
+ 'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_PRIMARY],
+ ],
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'severity' => $this->Inbox::SEVERITY_PRIMARY,
+ ]);
+ }
+ ],
+ [
+ 'label' => 'severity:info',
+ 'viewElement' => 'bootstrapUI',
+ 'viewElementParams' => [
+ 'element' => 'badge',
+ 'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
+ 'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_INFO],
+ ],
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'severity' => $this->Inbox::SEVERITY_INFO,
+ ]);
+ }
+ ],
+ [
+ 'label' => 'severity:warning',
+ 'viewElement' => 'bootstrapUI',
+ 'viewElementParams' => [
+ 'element' => 'badge',
+ 'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
+ 'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_WARNING],
+ ],
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'severity' => $this->Inbox::SEVERITY_WARNING,
+ ]);
+ }
+ ],
+ [
+ 'label' => 'severity:danger',
+ 'viewElement' => 'bootstrapUI',
+ 'viewElementParams' => [
+ 'element' => 'badge',
+ 'text' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
+ 'variant' => $this->Inbox->severityVariant[$this->Inbox::SEVERITY_DANGER],
+ ],
+ 'filterConditionFunction' => function ($query) {
+ return $query->where([
+ 'severity' => $this->Inbox::SEVERITY_DANGER,
+ ]);
+ }
+ ],
+ ],
],
'contain' => $this->containFields
]);
@@ -99,11 +192,11 @@ class InboxController extends AppController
}
}
}
- $this->set('deletionTitle', __('Discard request'));
+ $this->set('deletionTitle', __('Discard message'));
if (!empty($id)) {
- $this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id));
+ $this->set('deletionText', __('Are you sure you want to discard message #{0}?', $id));
} else {
- $this->set('deletionText', __('Are you sure you want to discard the selected requests?'));
+ $this->set('deletionText', __('Are you sure you want to discard the selected message?'));
}
$this->set('deletionConfirm', __('Discard'));
$this->CRUD->delete($id);
diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php
index 3106593..93ddfb9 100644
--- a/src/Controller/OrganisationsController.php
+++ b/src/Controller/OrganisationsController.php
@@ -20,46 +20,58 @@ class OrganisationsController extends AppController
public function index()
{
+ $customContextFilters = [
+ // [
+ // 'label' => __('ENISA Accredited'),
+ // 'filterCondition' => [
+ // 'MetaFields.field' => 'enisa-tistatus',
+ // 'MetaFields.value' => 'Accredited',
+ // 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
+ // ]
+ // ],
+ // [
+ // 'label' => __('ENISA not-Accredited'),
+ // 'filterCondition' => [
+ // 'MetaFields.field' => 'enisa-tistatus',
+ // 'MetaFields.value !=' => 'Accredited',
+ // 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
+ // ]
+ // ],
+ // [
+ // 'label' => __('ENISA CSIRT Network (GOV)'),
+ // 'filterConditionFunction' => function ($query) {
+ // return $this->CRUD->setParentConditionsForMetaFields($query, [
+ // 'ENISA CSIRT Network' => [
+ // [
+ // 'field' => 'constituency',
+ // 'value LIKE' => '%Government%',
+ // ],
+ // [
+ // 'field' => 'csirt-network-status',
+ // 'value' => 'Member',
+ // ],
+ // ]
+ // ]);
+ // }
+ // ],
+ ];
+
+ $loggedUserOrganisationNationality = $this->ACL->getUser()['organisation']['nationality'];
+ if (!empty($loggedUserOrganisationNationality)) {
+ $customContextFilters[] = [
+ 'label' => __('Country: {0}', $loggedUserOrganisationNationality),
+ 'filterCondition' => [
+ 'nationality' => $loggedUserOrganisationNationality,
+ ]
+ ];
+ }
+
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
'contextFilters' => [
- 'custom' => [
- [
- 'label' => __('ENISA Accredited'),
- 'filterCondition' => [
- 'MetaFields.field' => 'enisa-tistatus',
- 'MetaFields.value' => 'Accredited',
- 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
- ]
- ],
- [
- 'label' => __('ENISA not-Accredited'),
- 'filterCondition' => [
- 'MetaFields.field' => 'enisa-tistatus',
- 'MetaFields.value !=' => 'Accredited',
- 'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
- ]
- ],
- [
- 'label' => __('ENISA CSIRT Network (GOV)'),
- 'filterConditionFunction' => function($query) {
- return $this->CRUD->setParentConditionsForMetaFields($query, [
- 'ENISA CSIRT Network' => [
- [
- 'field' => 'constituency',
- 'value LIKE' => '%Government%',
- ],
- [
- 'field' => 'csirt-network-status',
- 'value' => 'Member',
- ],
- ]
- ]);
- }
- ]
- ],
+ 'custom' => $customContextFilters,
],
'contain' => $this->containFields,
'statisticsFields' => $this->statisticsFields,
diff --git a/src/Controller/OutboxController.php b/src/Controller/OutboxController.php
index 90db5ba..ad0033c 100644
--- a/src/Controller/OutboxController.php
+++ b/src/Controller/OutboxController.php
@@ -16,8 +16,8 @@ use Cake\Http\Exception\ForbiddenException;
class OutboxController extends AppController
{
- public $filterFields = ['scope', 'action', 'title', 'comment'];
- public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]];
+ public $filterFields = ['scope', 'action', 'title', 'message'];
+ public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true]];
public $containFields = ['Users'];
public function beforeFilter(EventInterface $event)
diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php
index 8de4c92..771672d 100644
--- a/src/Controller/UsersController.php
+++ b/src/Controller/UsersController.php
@@ -21,11 +21,23 @@ class UsersController extends AppController
if (empty($currentUser['role']['perm_admin'])) {
$conditions['organisation_id'] = $currentUser['organisation_id'];
}
+ $keycloakUsersParsed = null;
+ if (!empty(Configure::read('keycloak.enabled'))) {
+ // $keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
+ }
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
- 'conditions' => $conditions
+ 'conditions' => $conditions,
+ 'afterFind' => function($data) use ($keycloakUsersParsed) {
+ // TODO: We might want to uncomment this at some point Still need to evaluate the impact
+ // if (!empty(Configure::read('keycloak.enabled'))) {
+ // $keycloakUser = $keycloakUsersParsed[$data->username];
+ // $data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0];
+ // }
+ return $data;
+ }
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@@ -126,7 +138,7 @@ class UsersController extends AppController
'organisation' => $this->Users->Organisations->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => $org_conditions
- ])
+ ])->toArray()
];
$this->set(compact('dropdownData'));
$this->set('defaultRole', $defaultRole['id'] ?? null);
@@ -139,10 +151,18 @@ class UsersController extends AppController
if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
$id = $this->ACL->getUser()['id'];
}
+ $keycloakUsersParsed = null;
+ if (!empty(Configure::read('keycloak.enabled'))) {
+ $keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
+ }
$this->CRUD->view($id, [
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
- 'afterFind' => function($data) {
+ 'afterFind' => function($data) use ($keycloakUsersParsed) {
$data = $this->fetchTable('PermissionLimitations')->attachLimitations($data);
+ if (!empty(Configure::read('keycloak.enabled'))) {
+ $keycloakUser = $keycloakUsersParsed[$data->username];
+ $data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0];
+ }
return $data;
}
]);
@@ -150,7 +170,7 @@ class UsersController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
- $this->set('keycloakConfig', Configure::read('keycloak'));
+ $this->set('keycloakConfig', Configure::read('keycloak', ['enabled' => false]));
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
}
@@ -176,11 +196,6 @@ class UsersController extends AppController
$id = $currentUser['id'];
} else {
$id = intval($id);
- if ((empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
- if ($id !== $currentUser['id']) {
- throw new MethodNotAllowedException(__('You are not authorised to edit that user.'));
- }
- }
}
$params = [
@@ -189,13 +204,24 @@ class UsersController extends AppController
],
'fields' => [
'password', 'confirm_password'
- ]
+ ],
+ 'contain' => ['Roles', ],
];
if ($this->request->is(['get'])) {
$params['fields'] = array_merge($params['fields'], ['individual_id', 'role_id', 'disabled']);
if (!empty($this->ACL->getUser()['role']['perm_admin'])) {
$params['fields'][] = 'organisation_id';
}
+ if (!$currentUser['role']['perm_admin']) {
+ $params['afterFind'] = function ($user, &$params) use ($currentUser) {
+ if (!empty($user)) { // We don't have a 404
+ if (!$this->ACL->canEditUser($currentUser, $user)) {
+ throw new MethodNotAllowedException(__('You cannot edit the given user.'));
+ }
+ }
+ return $user;
+ };
+ }
}
if ($this->request->is(['post', 'put']) && !empty($this->ACL->getUser()['role']['perm_admin'])) {
$params['fields'][] = 'individual_id';
@@ -210,7 +236,7 @@ class UsersController extends AppController
if (!in_array($data['role_id'], array_keys($validRoles))) {
throw new MethodNotAllowedException(__('You cannot edit the given privileged user.'));
}
- if ($data['organisation_id'] !== $currentUser['organisation_id']) {
+ if (!$this->ACL->canEditUser($currentUser, $data)) {
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
}
return $data;
@@ -247,7 +273,7 @@ class UsersController extends AppController
'organisation' => $this->Users->Organisations->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => $org_conditions
- ])
+ ])->toArray()
];
$this->set(compact('dropdownData'));
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php
index 0a485dd..71af07b 100644
--- a/src/Model/Behavior/AuthKeycloakBehavior.php
+++ b/src/Model/Behavior/AuthKeycloakBehavior.php
@@ -284,27 +284,34 @@ class AuthKeycloakBehavior extends Behavior
{
$this->updateMappers();
$results = [];
- $data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
- [
- 'id',
- 'uuid',
- 'username',
- 'disabled',
- 'Individuals.email',
- 'Individuals.first_name',
- 'Individuals.last_name',
- 'Individuals.uuid',
- 'Roles.name',
- 'Roles.uuid',
- 'Organisations.name',
- 'Organisations.uuid'
- ]
- )->disableHydration()->toArray();
+ $data['Users'] = $this->getCerebrateUsers();
$clientId = $this->getClientId();
return $this->syncUsers($data['Users'], $clientId);
}
private function syncUsers(array $users, $clientId): array
+ {
+ $keycloakUsersParsed = $this->getParsedKeycloakUser();
+ $changes = [
+ 'created' => [],
+ 'modified' => [],
+ ];
+ foreach ($users as &$user) {
+ $changed = false;
+ if (empty($keycloakUsersParsed[$user['username']])) {
+ if ($this->createUser($user, $clientId)) {
+ $changes['created'][] = $user['username'];
+ }
+ } else {
+ if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
+ $changes['modified'][] = $user['username'];
+ }
+ }
+ }
+ return $changes;
+ }
+
+ public function getParsedKeycloakUser(): array
{
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
$keycloakUsers = json_decode($response->getStringBody(), true);
@@ -325,23 +332,25 @@ class AuthKeycloakBehavior extends Behavior
]
];
}
- $changes = [
- 'created' => [],
- 'modified' => [],
- ];
- foreach ($users as &$user) {
- $changed = false;
- if (empty($keycloakUsersParsed[$user['username']])) {
- if ($this->createUser($user, $clientId)) {
- $changes['created'][] = $user['username'];
- }
- } else {
- if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
- $changes['modified'][] = $user['username'];
- }
- }
- }
- return $changes;
+ return $keycloakUsersParsed;
+ }
+
+ private function getCerebrateUsers(): array
+ {
+ return $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select([
+ 'id',
+ 'uuid',
+ 'username',
+ 'disabled',
+ 'Individuals.email',
+ 'Individuals.first_name',
+ 'Individuals.last_name',
+ 'Individuals.uuid',
+ 'Roles.name',
+ 'Roles.uuid',
+ 'Organisations.name',
+ 'Organisations.uuid'
+ ])->disableHydration()->toArray();
}
private function checkAndUpdateUser(array $keycloakUser, array $user): bool
@@ -387,6 +396,63 @@ class AuthKeycloakBehavior extends Behavior
return false;
}
+ public function checkKeycloakStatus(array $users, array $keycloakUsers): array
+ {
+ $users = Hash::combine($users, '{n}.username', '{n}');
+ $keycloakUsersParsed = Hash::combine($keycloakUsers, '{n}.username', '{n}');
+ $status = [];
+ foreach ($users as $username => $user) {
+ $differences = [];
+ $requireUpdate = $this->checkKeycloakUserRequiresUpdate($keycloakUsersParsed[$username], $user, $differences);
+ $status[$user['id']] = [
+ 'require_update' => $requireUpdate,
+ 'differences' => $differences,
+ ];
+ }
+ return $status;
+ }
+
+ private function checkKeycloakUserRequiresUpdate(array $keycloakUser, array $user, array &$differences = []): bool
+ {
+
+ $cEnabled = $keycloakUser['enabled'] == $user['disabled'];
+ $cFn = $keycloakUser['firstName'] !== $user['individual']['first_name'];
+ $Ln = $keycloakUser['lastName'] !== $user['individual']['last_name'];
+ $cEmail = $keycloakUser['email'] !== $user['individual']['email'];
+ $cRolename = (empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']);
+ $cRoleuuid = (empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']);
+ $cOrgname = (empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']);
+ $cOrguuid = (empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid']);
+ if ($cEnabled || $cFn || $Ln || $cEmail || $cRolename || $cRoleuuid || $cOrgname || $cOrguuid) {
+ if ($cEnabled) {
+ $differences['enabled'] = ['kc' => $keycloakUser['enabled'], 'cerebrate' => $user['disabled']];
+ }
+ if ($cFn) {
+ $differences['first_name'] = ['kc' => $keycloakUser['firstName'], 'cerebrate' => $user['individual']['first_name']];
+ }
+ if ($Ln) {
+ $differences['last_name'] = ['kc' => $keycloakUser['lastName'], 'cerebrate' => $user['individual']['last_name']];
+ }
+ if ($cEmail) {
+ $differences['email'] = ['kc' => $keycloakUser['email'], 'cerebrate' => $user['individual']['email']];
+ }
+ if ($cRolename) {
+ $differences['role_name'] = ['kc' => $keycloakUser['attributes']['role_name'], 'cerebrate' => $user['role']['name']];
+ }
+ if ($cRoleuuid) {
+ $differences['role_uuid'] = ['kc' => $keycloakUser['attributes']['role_uuid'], 'cerebrate' => $user['role']['uuid']];
+ }
+ if ($cOrgname) {
+ $differences['org_name'] = ['kc' => $keycloakUser['attributes']['org_name'], 'cerebrate' => $user['organisation']['name']];
+ }
+ if ($cOrguuid) {
+ $differences['org_uuid'] = ['kc' => $keycloakUser['attributes']['org_uuid'], 'cerebrate' => $user['organisation']['uuid']];
+ }
+ return true;
+ }
+ return false;
+ }
+
private function createUser(array $user, string $clientId)
{
$newUser = [
diff --git a/src/Model/Behavior/NotifyAdminsBehavior.php b/src/Model/Behavior/NotifyAdminsBehavior.php
new file mode 100644
index 0000000..3bf9993
--- /dev/null
+++ b/src/Model/Behavior/NotifyAdminsBehavior.php
@@ -0,0 +1,332 @@
+ [
+ 'Model.afterSave' => 'afterSave',
+ 'Model.afterDelete' => 'afterDelete',
+ 'Model.beforeDelete' => 'beforeDelete',
+ ],
+ 'implementedMethods' => [
+ 'notifySiteAdmins' => 'notifySiteAdmins',
+ 'notifySiteAdminsForEntity' => 'notifySiteAdminsForEntity',
+ ],
+ ];
+
+ /** @var AuditLog|null */
+ private $Inbox;
+ /** @var User|null */
+ private $Users;
+ /** @var InboxProcessors|null */
+ private $InboxProcessors;
+
+ public function initialize(array $config): void
+ {
+ $this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
+ if($this->table() instanceof UsersTable) {
+ $this->Users = $this->table();
+ } else {
+ $this->Users = TableRegistry::getTableLocator()->get('Users');
+ }
+ }
+
+ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if (!$this->isNotificationAllowed($event, $entity, $options)) {
+ return;
+ }
+ $this->notifySiteAdminsForEntityChange($entity);
+ }
+
+ public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if ($entity->table()->hasBehavior('MetaFields') && !isset($entity->meta_fields)) {
+ $entity = $entity->table()->loadInto($entity, ['MetaFields']);
+ }
+ }
+
+ public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if (!$this->isNotificationAllowed($event, $entity, $options)) {
+ return;
+ }
+ $this->notifySiteAdminsForEntityDeletion($entity);
+ }
+
+ public function isNotificationAllowed(EventInterface $event, EntityInterface $entity, ArrayObject $options): bool
+ {
+ $loggedUser = Configure::read('loggedUser');
+ if (empty($loggedUser) || !empty($loggedUser['role']['perm_admin']) || !empty($loggedUser['role']['perm_sync'])) {
+ return false;
+ }
+ return true;
+ }
+
+ public function notifySiteAdminsForEntityChange(EntityInterface $entity): void
+ {
+ $watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
+ $originalFields = [];
+ $changedFields = $entity->extract($watchedFields);
+
+ $titleTemplate = 'New {0} `{1}` created';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __('New {0}', Inflector::singularize($entity->getSource()));
+
+ if (!$entity->isNew()) {
+ $originalFields = $this->_getOriginalFields($entity, $watchedFields);
+ $changedFields = $this->_getChangedFields($entity, $originalFields);
+
+ if (
+ $entity->table()->hasBehavior('Timestamp') &&
+ count($changedFields) == 1 && !empty($changedFields['modified'])
+ ) {
+ return; // A not watched field has changed
+ }
+
+ $changeAmount = $this->_computeChangeAmount($entity, $originalFields, $changedFields);
+ $titleTemplate = '{0} {1} modified';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __n(
+ '{0} field was updated',
+ '{0} fields were updated',
+ $changeAmount,
+ $changeAmount
+ );
+ }
+ if ($entity->table()->hasBehavior('MetaFields')) {
+ $originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields'] ?? []);
+ $changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields'] ?? []);
+ if (empty($originalFields['meta_fields']) && empty($changedFields['meta_fields'])) {
+ unset($originalFields['meta_fields']);
+ unset($changedFields['meta_fields']);
+ }
+ }
+ $originalFields = $entity->isNew() ? [] : $originalFields;
+ $data = [
+ 'original' => $this->_serializeFields($originalFields),
+ 'changed' => $this->_serializeFields($changedFields),
+ 'summaryTemplate' => $titleTemplate,
+ 'summaryMessage' => $message,
+ 'entityType' => Inflector::singularize($entity->getSource()),
+ 'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
+ 'entityViewURL' => Router::url([
+ 'controller' => Inflector::underscore($entity->getSource()),
+ 'action' => 'view',
+ $entity->id
+ ]),
+ ];
+ $this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
+ }
+
+ public function notifySiteAdminsForEntityDeletion(EntityInterface $entity): void
+ {
+ $watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible();
+ $originalFields = $entity->extract($watchedFields);
+ $changedFields = [];
+ $titleTemplate = 'Deleted {0} `{1}`';
+ $title = __(
+ $titleTemplate,
+ Inflector::singularize($entity->getSource()),
+ $entity->get($this->table()->getDisplayField())
+ );
+ $message = __('{0} deleted', Inflector::singularize($entity->getSource()));
+ $data = [
+ 'original' => $this->_serializeFields($originalFields),
+ 'changed' => $this->_serializeFields($changedFields),
+ 'summaryTemplate' => $titleTemplate,
+ 'summaryMessage' => $message,
+ 'entityType' => Inflector::singularize($entity->getSource()),
+ 'entityDisplayField' => $entity->get($this->table()->getDisplayField()),
+ 'entityViewURL' => Router::url([
+ 'controller' => Inflector::underscore($entity->getSource()),
+ 'action' => 'view',
+ $entity->id
+ ]),
+ ];
+ $this->notifySiteAdmins($entity->getSource(), $title, $message, $data);
+ }
+
+ /**
+ * Create a message in each site-admin users
+ *
+ * @param string $title A quick summary of what that notification is about
+ * @param string $origin The origin of the notification. For example, could be the model source
+ * @param string $message Even more free text
+ * @param array $data data used to generate the view when processing.
+ * Must contain: `original` array of the data before the change
+ * Must contain: `changed` array of the data after the change
+ * Optional: `summary` A text summarizing the change
+ * Optional: `summaryTemplate`, `summaryMessage`, `entityType`, `entityDisplayField`, `entityViewURL` text used to build a summary of the change
+ * @return void
+ */
+ public function notifySiteAdmins(
+ string $origin,
+ string $title,
+ string $message = '',
+ array $data
+ ): void {
+ $this->InboxProcessors = $this->InboxProcessors ?: TableRegistry::getTableLocator()->get('InboxProcessors');
+ $processor = $this->InboxProcessors->getProcessor('Notification', 'DataChange');
+ $siteAdmins = $this->_getSiteAdmins();
+ foreach ($siteAdmins as $siteAdmin) {
+ $notificationData = [
+ 'origin' => $origin,
+ 'title' => $title,
+ 'user_id' => $siteAdmin->id,
+ 'message' => $message,
+ 'data' => $data,
+ ];
+ $processor->create($notificationData);
+ }
+ }
+
+ protected function _getSiteAdmins(): array
+ {
+ return $this->Users->find()
+ ->matching('Roles', function(Query $q) {
+ return $q
+ ->where(['Roles.perm_admin' => true]);
+ })
+ ->all()->toList();
+ }
+
+ protected function _getOriginalFields(AppModel $entity, array $fields): array
+ {
+ $originalChangedFields = $entity->extractOriginalChanged($fields);
+ $originalChangedFields = array_map(function ($fieldValue) {
+ if (is_array($fieldValue)) {
+ return array_filter(array_map(function($fieldValue) {
+ if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
+ if (!empty($fieldValue->extractOriginalChanged($fieldValue->getVisible()))) {
+ return $fieldValue;
+ } else {
+ return null;
+ }
+ }
+ return $fieldValue;
+ }, $fieldValue), fn ($v) => !is_null($v));
+ } else if (is_subclass_of($fieldValue, 'App\Model\Entity\AppModel')) {
+ return $fieldValue->extractOriginalChanged($fieldValue);
+ }
+ return $fieldValue;
+ }, $originalChangedFields);
+ if ($entity->table()->hasBehavior('MetaFields')) {
+ // Include deleted meta-fields
+ $originalChangedFields['meta_fields'] = array_merge(
+ $originalChangedFields['meta_fields'] ?? [],
+ $this->_getDeletedMetafields($entity)
+ );
+ // Restore original values of meta-fields as the entity has been saved with the changes
+ if (!empty($entity->meta_fields)) {
+ $originalChangedFields['meta_fields'] = array_map(function ($metaField) {
+ $originalValues = $metaField->getOriginalValues($metaField->getVisible());
+ $originalMetafield = $metaField->table()->newEntity($metaField->toArray());
+ $originalMetafield->set($originalValues);
+ return $originalMetafield;
+ }, $originalChangedFields['meta_fields']);
+ }
+ }
+ return $originalChangedFields;
+ }
+
+ protected function _getChangedFields(AppModel $entity, array $originalFields): array
+ {
+ $changedFields = array_filter(
+ $entity->extract(array_keys($originalFields)),
+ fn ($v) => !is_null($v)
+ );
+ if ($entity->table()->hasBehavior('MetaFields')) {
+ $changedMetafields = $entity->extractOriginalChanged($entity->getVisible())['meta_fields'] ?? [];
+ $changedFields['meta_fields'] = array_filter($changedMetafields, function($metaField) {
+ return !empty($metaField->getDirty());
+ });
+ }
+ return $changedFields;
+ }
+
+ protected function _massageMetaField(AppModel $entity, array $metaFields): array
+ {
+ $massaged = [];
+ foreach ($metaFields as $metaField) {
+ foreach ($entity->MetaTemplates as $template) {
+ $templateDisplayName = sprintf('%s (v%s)', $template->name, $template->version);
+ foreach ($template['meta_template_fields'] as $field) {
+ if ($metaField->meta_template_id == $template->id && $metaField->meta_template_field_id == $field->id) {
+ if (!empty($massaged[$templateDisplayName][$field['field']])) {
+ if (!is_array($massaged[$templateDisplayName][$field['field']])) {
+ $massaged[$templateDisplayName][$field['field']] = [$massaged[$templateDisplayName][$field['field']]];
+ }
+ $massaged[$templateDisplayName][$field['field']][] = $metaField['value'];
+ } else {
+ $massaged[$templateDisplayName][$field['field']] = $metaField['value'];
+ }
+ break 2;
+ }
+ }
+ }
+ }
+ return $massaged;
+ }
+
+ protected function _getDeletedMetafields(AppModel $entity): array
+ {
+ return $entity->_metafields_to_delete ?? [];
+ }
+
+ protected function _computeChangeAmount(AppModel $entity, array $originalFields, array $changedFields): int
+ {
+ $amount = count($changedFields);
+ if ($entity->table()->hasBehavior('MetaFields')) {
+ $amount -= 1; // `meta_fields` key was counted without checking at the content
+ }
+ if (!empty($originalFields['meta_fields']) && !empty($changedFields['meta_fields'])) {
+ $amount += count(array_intersect_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add changed fields
+ $amount += count(array_diff_key($changedFields['meta_fields'] ?? [], $originalFields['meta_fields'] ?? [])); // Add new fields
+ $amount += count(array_diff_key($originalFields['meta_fields'] ?? [], $changedFields['meta_fields'] ?? [])); // Add deleted fields
+ }
+ return $amount;
+ }
+
+ protected function _serializeFields($fields): array
+ {
+ return array_map(function ($fieldValue) {
+ if (is_bool($fieldValue)) {
+ return empty($fieldValue) ? 0 : 1;
+ } else if (is_array($fieldValue)) {
+ return json_encode($fieldValue);
+ } else if (is_object($fieldValue)) {
+ switch (get_class($fieldValue)) {
+ case 'Cake\I18n\FrozenTime':
+ return $fieldValue->i18nFormat('yyyy-mm-dd HH:mm:ss');
+ }
+ } else {
+ return strval($fieldValue);
+ }
+ }, $fields);
+ }
+}
diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php
index 8dcd07f..3e70518 100644
--- a/src/Model/Entity/AppModel.php
+++ b/src/Model/Entity/AppModel.php
@@ -2,7 +2,9 @@
namespace App\Model\Entity;
+use App\Model\Table\AppTable;
use Cake\ORM\Entity;
+use Cake\ORM\TableRegistry;
class AppModel extends Entity
{
@@ -33,6 +35,11 @@ class AppModel extends Entity
return $this->_accessibleOnNew ?? [];
}
+ public function table(): AppTable
+ {
+ return TableRegistry::get($this->getSource());
+ }
+
public function rearrangeForAPI(): void
{
}
diff --git a/src/Model/Entity/Inbox.php b/src/Model/Entity/Inbox.php
index 820e29d..21a9b68 100644
--- a/src/Model/Entity/Inbox.php
+++ b/src/Model/Entity/Inbox.php
@@ -7,7 +7,7 @@ use Cake\ORM\Entity;
class Inbox extends AppModel
{
- protected $_virtual = ['local_tool_connector_name'];
+ protected $_virtual = ['local_tool_connector_name', 'severity_variant'];
protected function _getLocalToolConnectorName()
{
@@ -17,4 +17,9 @@ class Inbox extends AppModel
}
return $localConnectorName;
}
+
+ protected function _getSeverityVariant(): string
+ {
+ return $this->table()->severityVariant[$this->severity];
+ }
}
diff --git a/src/Model/Entity/MetaTemplateField.php b/src/Model/Entity/MetaTemplateField.php
index 7baf30a..958c3de 100644
--- a/src/Model/Entity/MetaTemplateField.php
+++ b/src/Model/Entity/MetaTemplateField.php
@@ -7,5 +7,45 @@ use Cake\ORM\Entity;
class MetaTemplateField extends AppModel
{
+ protected $_virtual = ['index_type', 'form_type', 'form_options', ];
+
+ protected function _getIndexType()
+ {
+ $indexType = 'text';
+ if ($this->type === 'boolean') {
+ $indexType = 'boolean';
+ } else if ($this->type === 'date') {
+ $indexType = 'datetime';
+ } else if ($this->type === 'ipv4' || $this->type === 'ipv6') {
+ $indexType = 'text';
+ }
+ return $indexType;
+ }
+
+ protected function _getFormType()
+ {
+ $formType = 'text';
+ if (!empty($this->sane_default) || !empty($this->values_list)) {
+ $formType = 'dropdown';
+ } else if ($this->type === 'boolean') {
+ $formType = 'checkbox';
+ }
+ return $formType;
+ }
+
+ protected function _getFormOptions()
+ {
+ $formOptions = [];
+ if ($this->formType === 'dropdown') {
+ $selectOptions = !empty($this->sane_default) ? $this->sane_default : $this->values_list;
+ $selectOptions = array_combine($selectOptions, $selectOptions);
+ if (!empty($this->sane_default)) {
+ $selectOptions[] = ['value' => '_custom', 'text' => __('-- custom value --'), 'class' => 'custom-value'];
+ }
+ $selectOptions[''] = __('-- no value --');
+ $formOptions['options'] = $selectOptions;
+ }
+ return $formOptions;
+ }
}
diff --git a/src/Model/Entity/Outbox.php b/src/Model/Entity/Outbox.php
index 51304e6..98cb40a 100644
--- a/src/Model/Entity/Outbox.php
+++ b/src/Model/Entity/Outbox.php
@@ -7,5 +7,10 @@ use Cake\ORM\Entity;
class Outbox extends AppModel
{
-
+ protected $_virtual = ['severity_variant'];
+
+ protected function _getSeverityVariant(): string
+ {
+ return $this->table()->severityVariant[$this->severity];
+ }
}
diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php
index 70ea76f..531714c 100644
--- a/src/Model/Table/AppTable.php
+++ b/src/Model/Table/AppTable.php
@@ -16,6 +16,7 @@ class AppTable extends Table
{
public function initialize(array $config): void
{
+ FrozenTime::setToStringFormat("yyyy-MM-dd HH:mm:ss");
}
public function getStatisticsUsageForModel(Object $table, array $scopes, array $options=[]): array
diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php
index 0617cea..a7626e7 100644
--- a/src/Model/Table/InboxTable.php
+++ b/src/Model/Table/InboxTable.php
@@ -8,6 +8,7 @@ use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use Cake\Http\Exception\NotFoundException;
+use Cake\ORM\ResultSet;
use App\Utility\UI\Notification;
@@ -15,6 +16,17 @@ Type::map('json', 'Cake\Database\Type\JsonType');
class InboxTable extends AppTable
{
+ public const SEVERITY_PRIMARY = 0,
+ SEVERITY_INFO = 1,
+ SEVERITY_WARNING = 2,
+ SEVERITY_DANGER = 3;
+
+ public $severityVariant = [
+ self::SEVERITY_PRIMARY => 'primary',
+ self::SEVERITY_INFO => 'info',
+ self::SEVERITY_WARNING => 'warning',
+ self::SEVERITY_DANGER => 'danger',
+ ];
public function initialize(array $config): void
{
@@ -97,8 +109,8 @@ class InboxTable extends AppTable
$allNotifications = [];
$inboxNotifications = $this->getNotificationsForUser($user);
foreach ($inboxNotifications as $notification) {
- $title = __('New message');
- $details = $notification->title;
+ $title = $notification->title;
+ $details = $notification->message;
$router = [
'controller' => 'inbox',
'action' => 'process',
@@ -109,7 +121,7 @@ class InboxTable extends AppTable
'icon' => 'envelope',
'details' => $details,
'datetime' => $notification->created,
- 'variant' => 'warning',
+ 'variant' => $notification->severity_variant,
'_useModal' => true,
'_sidebarId' => 'inbox',
]))->get();
@@ -117,18 +129,14 @@ class InboxTable extends AppTable
return $allNotifications;
}
- public function getNotificationsForUser(\App\Model\Entity\User $user): array
+ public function getNotificationsForUser(\App\Model\Entity\User $user): ResultSet
{
$query = $this->find();
- $conditions = [];
- if ($user['role']['perm_admin']) {
- // Admin will not see notifications if it doesn't belong to them. They can see process the message from the inbox
- $conditions['Inbox.user_id IS'] = null;
- } else {
- $conditions['Inbox.user_id'] = $user->id;
- }
+ $conditions = [
+ 'Inbox.user_id' => $user->id
+ ];
$query->where($conditions);
- $notifications = $query->all()->toArray();
+ $notifications = $query->all();
return $notifications;
}
}
diff --git a/src/Model/Table/MetaTemplateFieldsTable.php b/src/Model/Table/MetaTemplateFieldsTable.php
index 626a96c..857361c 100644
--- a/src/Model/Table/MetaTemplateFieldsTable.php
+++ b/src/Model/Table/MetaTemplateFieldsTable.php
@@ -27,6 +27,8 @@ class MetaTemplateFieldsTable extends AppTable
$this->hasMany('MetaFields');
$this->setDisplayField('field');
+ $this->getSchema()->setColumnType('sane_default', 'json');
+ $this->getSchema()->setColumnType('values_list', 'json');
$this->loadTypeHandlers();
}
diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php
index 723b46a..f56f36b 100644
--- a/src/Model/Table/OrganisationsTable.php
+++ b/src/Model/Table/OrganisationsTable.php
@@ -16,6 +16,9 @@ class OrganisationsTable extends AppTable
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->addBehavior('AuditLog');
+ $this->addBehavior('NotifyAdmins', [
+ 'fields' => ['uuid', 'name', 'url', 'nationality', 'sector', 'type', 'contacts', 'modified', 'meta_fields'],
+ ]);
$this->hasMany(
'Alignments',
[
diff --git a/src/Model/Table/OutboxTable.php b/src/Model/Table/OutboxTable.php
index a78e0c6..eaf7358 100644
--- a/src/Model/Table/OutboxTable.php
+++ b/src/Model/Table/OutboxTable.php
@@ -8,11 +8,13 @@ use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
use Cake\Http\Exception\NotFoundException;
+use Cake\ORM\TableRegistry;
Type::map('json', 'Cake\Database\Type\JsonType');
class OutboxTable extends AppTable
{
+ public $severityVariant;
public function initialize(array $config): void
{
@@ -22,6 +24,9 @@ class OutboxTable extends AppTable
$this->belongsTo('Users');
$this->addBehavior('AuditLog');
$this->setDisplayField('title');
+
+ $this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
+ $this->severityVariant = $this->Inbox->severityVariant;
}
protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
diff --git a/src/Model/Table/PermissionLimitationsTable.php b/src/Model/Table/PermissionLimitationsTable.php
index 1b5f193..abb8543 100644
--- a/src/Model/Table/PermissionLimitationsTable.php
+++ b/src/Model/Table/PermissionLimitationsTable.php
@@ -22,6 +22,7 @@ class PermissionLimitationsTable extends AppTable
$validator
->notEmptyString('permission')
->notEmptyString('scope')
+ ->naturalNumber('max_occurrence', __('That field can only hold non-negative values.'))
->requirePresence(['permission', 'scope', 'max_occurrence'], 'create');
return $validator;
}
@@ -44,6 +45,14 @@ class PermissionLimitationsTable extends AppTable
'limit' => $entry['max_occurrence']
];
}
+ foreach ($limitations as $i => $permissions) { // Make sure global and organisations permission are mirror in the case where one of the two is not defined
+ if (!isset($permissions['global']['limit'])) {
+ $limitations[$i]['global']['limit'] = $permissions['organisation']['limit'];
+ }
+ if (!isset($permissions['organisation']['limit'])) {
+ $limitations[$i]['organisation']['limit'] = $permissions['global']['limit'];
+ }
+ }
foreach ($limitations as $field => $data) {
if (isset($data['global'])) {
$limitations[$field]['global']['current'] = $MetaFields->find('all', [
diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php
index a2ebfeb..8f74390 100644
--- a/src/Model/Table/UsersTable.php
+++ b/src/Model/Table/UsersTable.php
@@ -25,6 +25,9 @@ class UsersTable extends AppTable
$this->addBehavior('UUID');
$this->addBehavior('MetaFields');
$this->addBehavior('AuditLog');
+ $this->addBehavior('NotifyAdmins', [
+ 'fields' => ['role_id', 'individual_id', 'organisation_id', 'disabled', 'modified', 'meta_fields'],
+ ]);
$this->initAuthBehaviors();
$this->belongsTo(
'Individuals',
@@ -66,6 +69,7 @@ class UsersTable extends AppTable
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
+ $success = true;
if (!$entity->isNew()) {
$success = $this->handleUserUpdateRouter($entity);
}
@@ -84,22 +88,33 @@ class UsersTable extends AppTable
if (!isset($this->PermissionLimitations)) {
$this->PermissionLimitations = TableRegistry::get('PermissionLimitations');
}
- $new = $entity->isNew();
$permissions = $this->PermissionLimitations->getListOfLimitations($entity);
foreach ($permissions as $permission_name => $permission) {
foreach ($permission as $scope => $permission_data) {
- if (!empty($entity['meta_fields'])) {
- $enabled = false;
+ $valueToCompareTo = $permission_data['current'];
+
+ $enabled = false;
+ if (!empty($entity->meta_fields)) {
foreach ($entity['meta_fields'] as $metaField) {
if ($metaField['field'] === $permission_name) {
$enabled = true;
+ if ($metaField->isNew()) {
+ $valueToCompareTo += !empty($metaField->value) ? 1 : 0;
+ } else {
+ $valueToCompareTo += !empty($metaField->value) ? 0 : -1;
+ }
}
}
- if (!$enabled) {
- continue;
+ }
+
+ if (!$enabled && !empty($entity->_metafields_to_delete)) {
+ foreach ($entity->_metafields_to_delete as $metaFieldToDelete) {
+ if ($metaFieldToDelete['field'] === $permission_name) {
+ $valueToCompareTo += !empty($metaFieldToDelete->value) ? -1 : 0;
+ }
}
}
- $valueToCompareTo = $permission_data['current'] + ($new ? 1 : 0);
+
if ($valueToCompareTo > $permission_data['limit']) {
return [
$permission_name =>
@@ -168,18 +183,6 @@ class UsersTable extends AppTable
return $rules;
}
- public function test()
- {
- $this->Roles = TableRegistry::get('Roles');
- $role = $this->Roles->newEntity([
- 'name' => 'admin',
- 'perm_admin' => 1,
- 'perm_org_admin' => 1,
- 'perm_sync' => 1
- ]);
- $this->Roles->save($role);
- }
-
public function checkForNewInstance(): bool
{
if (empty($this->find()->first())) {
@@ -263,7 +266,7 @@ class UsersTable extends AppTable
{
if (!empty(Configure::read('keycloak'))) {
$success = $this->handleUserUpdate($user);
- //return $success !== false;
+ // return $success;
}
return true;
}
diff --git a/src/Utility/UI/Notification.php b/src/Utility/UI/Notification.php
index a6bf05b..b7e3f17 100644
--- a/src/Utility/UI/Notification.php
+++ b/src/Utility/UI/Notification.php
@@ -25,7 +25,7 @@ class Notification
foreach ($options as $key => $value) {
$this->{$key} = $value;
}
- $this->validate();
+ $this->_validate();
}
public function get(): array
@@ -36,7 +36,7 @@ class Notification
return null;
}
- private function validate()
+ protected function _validate()
{
$validator = new Validator();
diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php
index f8d5024..5b487c6 100644
--- a/src/View/Helper/BootstrapHelper.php
+++ b/src/View/Helper/BootstrapHelper.php
@@ -1077,6 +1077,7 @@ class BoostrapModal extends BootstrapGeneric
'bodyHtml' => false,
'footerHtml' => false,
'confirmText' => 'Confirm',
+ 'confirmIcon' => false,
'cancelText' => 'Cancel',
'modalClass' => [''],
'headerClass' => [''],
@@ -1218,6 +1219,7 @@ class BoostrapModal extends BootstrapGeneric
$buttonConfirm = (new BoostrapButton([
'variant' => $variant,
'text' => h($this->options['confirmText']),
+ 'icon' => h($this->options['confirmIcon']),
'class' => 'modal-confirm-button',
'params' => [
// 'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
@@ -1953,7 +1955,7 @@ class BoostrapDropdownMenu extends BootstrapGeneric
'aria-expanded' => 'false',
]
];
- $options = array_merge($this->options['toggle-button'], $defaultOptions);
+ $options = array_merge_recursive($this->options['toggle-button'], $defaultOptions);
return $this->btHelper->button($options);
}
diff --git a/templates/AuditLogs/index.php b/templates/AuditLogs/index.php
index e46892c..52526f6 100644
--- a/templates/AuditLogs/index.php
+++ b/templates/AuditLogs/index.php
@@ -28,6 +28,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'request_ip',
'data_path' => 'request_ip',
],
+ [
+ 'name' => 'created',
+ 'sort' => 'created',
+ 'data_path' => 'created',
+ 'element' => 'datetime'
+ ],
[
'name' => __('Username'),
'sort' => 'user.username',
diff --git a/templates/Inbox/index.php b/templates/Inbox/index.php
index 6576ec3..bee8244 100644
--- a/templates/Inbox/index.php
+++ b/templates/Inbox/index.php
@@ -12,9 +12,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
'type' => 'multi_select_actions',
'children' => [
[
- 'text' => __('Discard requests'),
+ 'text' => __('Discard message'),
'variant' => 'danger',
- 'onclick' => 'discardRequests',
+ 'onclick' => 'discardMessages',
]
],
'data' => [
@@ -34,6 +34,10 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
+ ],
+ [
+ 'type' => 'table_action',
+ 'table_setting_id' => 'inbox_index',
]
]
],
@@ -49,6 +53,18 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'created',
'element' => 'datetime'
],
+ [
+ 'name' => 'severity',
+ 'sort' => 'severity',
+ 'data_path' => 'severity',
+ 'element' => 'function',
+ 'function' => function ($entry, $context) {
+ return $context->Bootstrap->badge([
+ 'text' => $entry->severity_variant,
+ 'variant' => $entry->severity_variant,
+ ]);
+ }
+ ],
[
'name' => 'scope',
'sort' => 'scope',
@@ -76,14 +92,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
'element' => 'user'
],
[
- 'name' => 'description',
- 'sort' => 'description',
- 'data_path' => 'description',
- ],
- [
- 'name' => 'comment',
- 'sort' => 'comment',
- 'data_path' => 'comment',
+ 'name' => 'message',
+ 'sort' => 'message',
+ 'data_path' => 'message',
],
],
'title' => __('Inbox'),
@@ -105,7 +116,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
'open_modal' => '/inbox/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash',
- 'title' => __('Discard request')
+ 'title' => __('Discard message')
],
]
]
@@ -113,7 +124,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/fieldScaffold.php b/templates/element/genericElements/Form/fieldScaffold.php
index 32b339b..18e6000 100644
--- a/templates/element/genericElements/Form/fieldScaffold.php
+++ b/templates/element/genericElements/Form/fieldScaffold.php
@@ -1,11 +1,10 @@
meta_template_fields as $metaTemplateField) {
$metaField = reset($metaTemplateField->metaFields);
$fieldData = [
'label' => $metaTemplateField->label,
+ 'type' => $metaTemplateField->formType,
];
+ if (!empty($metaTemplateField->formOptions)) {
+ $fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
+ }
if (isset($metaField->id)) {
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, $metaField->id);
} else {
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
}
- if ($metaTemplateField->type === 'boolean') {
- $fieldData['type'] = 'checkbox';
- }
$this->Form->setTemplates($backupTemplates);
$fieldsHtml .= $this->element(
'genericElements/Form/fieldScaffold',
@@ -66,9 +67,10 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
$fieldData = [
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
'label' => $metaTemplateField->label,
+ 'type' => $metaTemplateField->formType,
];
- if ($metaTemplateField->type === 'boolean') {
- $fieldData['type'] = 'checkbox';
+ if (!empty($metaTemplateField->formOptions)) {
+ $fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
$fieldsHtml .= $this->element(
'genericElements/Form/fieldScaffold',
@@ -80,4 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
}
}
}
-echo $fieldsHtml;
\ No newline at end of file
+$fieldContainer = $this->Bootstrap->genNode('div', [
+ 'class' => [],
+], $fieldsHtml);
+echo $fieldContainer;
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/multiFieldButton.php b/templates/element/genericElements/Form/multiFieldButton.php
index c7c778b..ddc17ca 100644
--- a/templates/element/genericElements/Form/multiFieldButton.php
+++ b/templates/element/genericElements/Form/multiFieldButton.php
@@ -45,6 +45,8 @@ $seed = 'mfb-' . mt_rand();
$clonedContainer
.removeClass('has-error')
.find('.error-message ').remove()
+ $clonedContainer
+ .find('label.form-label').text('')
const $clonedInput = $clonedContainer.find('input, select')
if ($clonedInput.length > 0) {
const injectedTemplateId = $clicked.closest('.multi-metafields-container').find('.new-metafield').length
@@ -54,18 +56,25 @@ $seed = 'mfb-' . mt_rand();
}
}
- function adjustClonedInputAttr($input, injectedTemplateId) {
- let explodedPath = $input.attr('field').split('.').splice(0, 5)
- explodedPath.push('new', injectedTemplateId)
- dottedPathStr = explodedPath.join('.')
- brackettedPathStr = explodedPath.map((elem, i) => {
- return i == 0 ? elem : `[${elem}]`
- }).join('')
- $input.attr('id', dottedPathStr)
- .attr('field', dottedPathStr)
- .attr('name', brackettedPathStr)
- .val('')
- .removeClass('is-invalid')
+ function adjustClonedInputAttr($inputs, injectedTemplateId) {
+ $inputs.each(function() {
+ const $input = $(this)
+ let explodedPath = $input.attr('field').split('.').splice(0, 5)
+ explodedPath.push('new', injectedTemplateId)
+ dottedPathStr = explodedPath.join('.')
+ brackettedPathStr = explodedPath.map((elem, i) => {
+ return i == 0 ? elem : `[${elem}]`
+ }).join('')
+ const attrs = ['id', 'field', 'name']
+ attrs.forEach((attr) => {
+ if ($input.attr(attr) !== undefined) {
+ $input.attr(attr, attr === 'name' ? brackettedPathStr : dottedPathStr)
+ }
+ })
+ $input
+ .val('')
+ .removeClass('is-invalid')
+ })
}
})()
\ No newline at end of file
diff --git a/templates/element/genericElements/Form/multiFieldScaffold.php b/templates/element/genericElements/Form/multiFieldScaffold.php
index 3a73125..3099813 100644
--- a/templates/element/genericElements/Form/multiFieldScaffold.php
+++ b/templates/element/genericElements/Form/multiFieldScaffold.php
@@ -22,6 +22,7 @@ if (!empty($metaFieldsEntities)) {
$metaFieldsEntity->meta_template_field_id,
$metaFieldsEntity->id
),
+ 'type' => $metaTemplateField->formType,
];
if($metaFieldsEntity->isNew()) {
$fieldData['field'] = sprintf(
@@ -33,7 +34,10 @@ if (!empty($metaFieldsEntities)) {
$fieldData['class'] = 'new-metafield';
}
if ($labelPrintedOnce) { // Only the first input can have a label
- $fieldData['label'] = false;
+ $fieldData['label'] = ['text' => ''];
+ }
+ if ($metaTemplateField->formType === 'dropdown') {
+ $fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
}
$labelPrintedOnce = true;
$fieldsHtml .= $this->element(
@@ -49,14 +53,19 @@ if (!empty($metaTemplateField) && !empty($multiple)) { // Add multiple field but
$metaTemplateField->label = Inflector::humanize($metaTemplateField->field);
$emptyMetaFieldInput = '';
if (empty($metaFieldsEntities)) { // Include editable field for meta-template not containing a meta-field
+ $fieldData = [
+ 'label' => $metaTemplateField->label,
+ 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
+ 'class' => 'new-metafield',
+ 'type' => $metaTemplateField->formType,
+ ];
+ if ($metaTemplateField->formType === 'dropdown') {
+ $fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
+ }
$emptyMetaFieldInput = $this->element(
'genericElements/Form/fieldScaffold',
[
- 'fieldData' => [
- 'label' => $metaTemplateField->label,
- 'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
- 'class' => 'new-metafield',
- ],
+ 'fieldData' => $fieldData,
'form' => $form,
]
);
diff --git a/templates/element/genericElements/IndexTable/Fields/keycloak_status.php b/templates/element/genericElements/IndexTable/Fields/keycloak_status.php
new file mode 100644
index 0000000..bc2b95b
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Fields/keycloak_status.php
@@ -0,0 +1,14 @@
+Hash->get($row, $field['data_path']);
+ if (is_null($data)) {
+ echo '';
+ } else if (!empty($data['require_update'])) {
+ echo sprintf(
+ '%s',
+ sprintf('Fields having differences: %s', (implode(', ', array_keys($data['differences'])))),
+ $this->Bootstrap->icon('times', ['class' => 'text-danger', ])
+ );
+ } else {
+ echo $this->Bootstrap->icon('check', ['class' => 'text-success',]);
+ }
+?>
diff --git a/templates/element/genericElements/IndexTable/index_table.php b/templates/element/genericElements/IndexTable/index_table.php
index dc7453c..956fcf8 100644
--- a/templates/element/genericElements/IndexTable/index_table.php
+++ b/templates/element/genericElements/IndexTable/index_table.php
@@ -25,10 +25,12 @@ if (!empty($requestedMetaFields)) { // Create mapping for new index table fields
foreach ($requestedMetaFields as $requestedMetaField) {
$template_id = $requestedMetaField['template_id'];
$meta_template_field_id = $requestedMetaField['meta_template_field_id'];
+ $viewElementCandidate = $meta_templates[$template_id]['meta_template_fields'][$meta_template_field_id]['index_type'];
+ $viewElementCandidatePath = '/genericElements/IndexTable/Fields/' . $viewElementCandidate;
$newMetaFields[] = [
'name' => $meta_templates[$template_id]['meta_template_fields'][$meta_template_field_id]['field'],
'data_path' => "MetaTemplates.{$template_id}.meta_template_fields.{$meta_template_field_id}.metaFields.{n}.value",
- 'element' => 'generic_field',
+ 'element' => $this->elementExists($viewElementCandidatePath) ? $viewElementCandidate : 'generic_field',
'_metafield' => true,
'_automatic_field' => true,
];
diff --git a/templates/element/genericElements/ListTopBar/group_context_filters.php b/templates/element/genericElements/ListTopBar/group_context_filters.php
index e441515..71b97e6 100644
--- a/templates/element/genericElements/ListTopBar/group_context_filters.php
+++ b/templates/element/genericElements/ListTopBar/group_context_filters.php
@@ -21,7 +21,7 @@
} else {
$currentFilteringContext = $filteringContext['filterCondition'];
}
- $contextArray[] = [
+ $contextItem = [
'active' => (
(
$currentQuery == $currentFilteringContext && // query conditions match
@@ -42,9 +42,17 @@
"#table-container-${tableRandomValue}",
"#table-container-${tableRandomValue} table.table",
],
- 'text' => $filteringContext['label'],
'class' => 'btn-sm'
];
+ if (!empty($filteringContext['viewElement'])) {
+ $contextItem['html'] = $this->element(
+ $filteringContext['viewElement'],
+ $filteringContext['viewElementParams'] ?? []
+ );
+ } else {
+ $contextItem['text'] = $filteringContext['label'];
+ }
+ $contextArray[] = $contextItem;
}
$dataGroup = [
diff --git a/templates/element/genericElements/ListTopBar/group_table_action.php b/templates/element/genericElements/ListTopBar/group_table_action.php
index fdb799c..fe538b2 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action.php
@@ -8,6 +8,7 @@ if (empty($data['table_setting_id']) && empty($model)) {
$data['table_setting_id'] = !empty($data['table_setting_id']) ? $data['table_setting_id'] : IndexSetting::getIDFromTable($model);
$tableSettings = IndexSetting::getTableSetting($loggedUser, $data['table_setting_id']);
$compactDisplay = !empty($tableSettings['compact_display']);
+$numberOfElement = $tableSettings['number_of_element'] ?? 20;
$availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_action/hiddenColumns', [
'table_data' => $table_data,
@@ -51,7 +52,13 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
'table_data' => $table_data,
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
- 'compactDisplay' => $compactDisplay
+ 'compactDisplay' => $compactDisplay,
+]);
+$numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_action/numberOfElement', [
+ 'table_data' => $table_data,
+ 'tableSettings' => $tableSettings,
+ 'table_setting_id' => $data['table_setting_id'],
+ 'numberOfElement' => $numberOfElement,
]);
?>
@@ -63,6 +70,7 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
'toggle-button' => [
'icon' => 'sliders-h',
'variant' => 'primary',
+ 'class' => ['table_setting_dropdown_button'],
],
'submenu_alignment' => 'end',
'submenu_direction' => 'start',
@@ -79,8 +87,22 @@ $compactDisplayHtml = $this->element('/genericElements/ListTopBar/group_table_ac
],
[
'html' => $compactDisplayHtml,
+ ],
+ [
+ 'html' => $numberOfElementHtml,
]
]
]);
?>
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php b/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php
index 312ad37..28eaa4d 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action/compactDisplay.php
@@ -41,6 +41,7 @@ $compactDisplayInputSeed = 'seed-' . mt_rand();
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
toggleCompactState($checkbox[0].checked, $table)
+ registerDebouncedFunction($container, debouncedCompactSaver)
})
})()
\ No newline at end of file
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
index 3a95b82..8e14c6d 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
@@ -40,7 +40,8 @@ echo $availableColumnsHtml;
const debouncedHiddenColumnSaverWithReload = debounce(mergeAndSaveSettingsWithReload, 2000)
function attachListeners() {
- let debouncedFunctionWithReload = false, debouncedFunction = false // used to flush debounce function if dropdown menu gets closed
+ let debouncedFunctionWithReload = false,
+ debouncedFunction = false // used to flush debounce function if dropdown menu gets closed
$('form.visible-column-form, form.visible-meta-column-form').find('input').change(function() {
const $dropdownMenu = $(this).closest(`[data-table-random-value]`)
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
@@ -108,24 +109,6 @@ echo $availableColumnsHtml;
return tableSetting
}
- function mergeAndSaveSettingsWithReload(table_setting_id, tableSettings, $table) {
- mergeAndSaveSettings(table_setting_id, tableSettings, false).then((apiResult) => {
- const theToast = UI.toast({
- variant: 'success',
- title: apiResult.message,
- bodyHtml: $('').append(
- $('').text('= __('The table needs to be reloaded for the new fields to be included.') ?>'),
- $('').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('= __('Reload table') ?>').click(function() {
- const reloadUrl = $table.data('reload-url');
- UI.reload(reloadUrl, $table.closest('div[id^="table-container-"]'), $(this)).then(() => {
- theToast.removeToast()
- })
- }),
- ),
- })
- })
- }
-
$(document).ready(function() {
addSupportOfNestedDropdown();
const $form = $('form.visible-column-form, form.visible-meta-column-form')
@@ -138,6 +121,8 @@ echo $availableColumnsHtml;
toggleColumn(this.getAttribute('data-columnname'), this.checked, $table)
})
attachListeners()
+ registerDebouncedFunction($container, debouncedHiddenColumnSaver)
+ registerDebouncedFunction($container, debouncedHiddenColumnSaverWithReload)
})
})()
\ No newline at end of file
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/numberOfElement.php b/templates/element/genericElements/ListTopBar/group_table_action/numberOfElement.php
new file mode 100644
index 0000000..6b69c50
--- /dev/null
+++ b/templates/element/genericElements/ListTopBar/group_table_action/numberOfElement.php
@@ -0,0 +1,42 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php b/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php
new file mode 100644
index 0000000..efac939
--- /dev/null
+++ b/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php
@@ -0,0 +1,21 @@
+%s
%s',
+ $this->Bootstrap->icon('exclamation-triangle') . __(' This user is not synchronise with Keycloak. Differences:'),
+ $this->Html->nestedList($differencesRearranged, ['class' => ''])
+ );
+ } else {
+ echo $this->Bootstrap->icon('check', ['class' => 'text-success',]);
+ }
+?>
diff --git a/templates/element/layouts/header/header-notification-item.php b/templates/element/layouts/header/header-notification-item.php
index 59a3df8..4457e68 100644
--- a/templates/element/layouts/header/header-notification-item.php
+++ b/templates/element/layouts/header/header-notification-item.php
@@ -21,7 +21,7 @@ $variant = empty($notification['variant']) ? 'primary' : $notification['variant'
-
+
= $this->ValueGetter->get($notification['text']) ?>
= h($notification['datetime']->format('Y-m-d\TH:i:s')) ?>
diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php
index 33fb379..a193721 100644
--- a/templates/genericTemplates/filters.php
+++ b/templates/genericTemplates/filters.php
@@ -7,6 +7,7 @@ $tableItems = array_map(function ($fieldName) {
'fieldname' => $fieldName,
];
}, $filters);
+$formTypeMap = $this->Form->getConfig('typeMap');
$filteringForm = $this->Bootstrap->table(
[
@@ -23,11 +24,19 @@ $filteringForm = $this->Bootstrap->table(
}
],
[
- 'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) {
+ 'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
+ $fieldName = $row['fieldname'];
+ $type = $typeMap[$fieldName] ?? 'text';
$options = [
sprintf('
', '=', '='),
sprintf('
', '!=', '!='),
];
+ if ($type === 'datetime') {
+ $options = [
+ sprintf('
', '>=', '>='),
+ sprintf('
', '<=', '<='),
+ ];
+ }
return sprintf('
', implode('', $options));
}
],
@@ -38,8 +47,21 @@ $filteringForm = $this->Bootstrap->table(
__('Value'),
sprintf('
', __('Supports strict matches and LIKE matches with the `%` character.
Example: `%.com`'))
),
- 'formatter' => function ($field, $row) {
- return sprintf('');
+ 'formatter' => function ($field, $row) use ($typeMap, $formTypeMap) {
+ $fieldName = $row['fieldname'];
+ $formType = $formTypeMap[$typeMap[$fieldName]] ?? 'text';
+ $this->Form->setTemplates([
+ 'formGroup' => '{{input}}
',
+ ]);
+ return $this->element('genericElements/Form/fieldScaffold', [
+ 'fieldData' => [
+ 'field' => $fieldName,
+ 'type' => $formType,
+ 'label' => '',
+ 'class' => 'fieldValue form-control-sm'
+ ],
+ 'params' => []
+ ]);
}
],
],
@@ -102,8 +124,8 @@ echo $this->Bootstrap->modal([
$rows.each(function() {
const rowData = getDataFromRow($(this))
let fullFilter = rowData['name']
- if (rowData['operator'] == '!=') {
- fullFilter += ' !='
+ if (rowData['operator'] != '=') {
+ fullFilter += ` ${rowData['operator']}`
}
if (rowData['value'].length > 0) {
activeFilters[fullFilter] = rowData['value']
@@ -111,14 +133,13 @@ echo $this->Bootstrap->modal([
})
if (modalObject.$modal.find('table.indexMetaFieldsFilteringTable').length > 0) {
let metaFieldFilters = modalObject.$modal.find('table.indexMetaFieldsFilteringTable')[0].getFiltersFunction()
- // activeFilters['filteringMetaFields'] = metaFieldFilters !== undefined ? metaFieldFilters : [];
metaFieldFilters = metaFieldFilters !== undefined ? metaFieldFilters : []
for (let [metaFieldPath, metaFieldValue] of Object.entries(metaFieldFilters)) {
activeFilters[metaFieldPath] = metaFieldValue
}
}
$selectTag = modalObject.$modal.find('.tag-container select.select2-input')
- activeFilters['filteringTags'] = $selectTag.select2('data').map(tag => tag.text)
+ activeFilters['filteringTags'] = $selectTag.length > 0 ? $selectTag.select2('data').map(tag => tag.text) : []
const searchParam = jQuery.param(activeFilters);
const url = `/${controller}/${action}?${searchParam}`
@@ -138,8 +159,8 @@ echo $this->Bootstrap->modal([
for (let [field, value] of Object.entries(activeFilters)) {
const fieldParts = field.split(' ')
let operator = '='
- if (fieldParts.length == 2 && fieldParts[1] == '!=') {
- operator = '!='
+ if (fieldParts.length == 2) {
+ operator = fieldParts[1]
field = fieldParts[0]
} else if (fieldParts.length > 2) {
console.error('Field contains multiple spaces. ' + field)
@@ -167,14 +188,24 @@ echo $this->Bootstrap->modal([
return $(this).data('fieldname') == field
}).closest('tr')
$row.find('.fieldOperator').val(operator)
- $row.find('.fieldValue').val(value)
+ 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')
rowData['operator'] = $row.find('select.fieldOperator').val()
- rowData['value'] = $row.find('input.fieldValue').val()
+ const $formElement = $row.find('.fieldValue');
+ if ($formElement.attr('type') === 'datetime-local') {
+ rowData['value'] = $formElement.val().length > 0 ? moment($formElement.val()).toISOString() : $formElement.val()
+ } else {
+ rowData['value'] = $formElement.val()
+ }
return rowData
}
diff --git a/templates/layout/default.php b/templates/layout/default.php
index 1509aea..e810136 100644
--- a/templates/layout/default.php
+++ b/templates/layout/default.php
@@ -88,7 +88,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa
-
+