From a64e62a3fb02f408a3d91e54faa80efb243d5f0b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Oct 2022 10:22:32 +0200 Subject: [PATCH 01/69] chg: [inboxProcessor:generic] Updated to not rely on deprecated parameters anymore --- libraries/default/InboxProcessors/GenericInboxProcessor.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/default/InboxProcessors/GenericInboxProcessor.php b/libraries/default/InboxProcessors/GenericInboxProcessor.php index 1ca84ea..a212fd6 100644 --- a/libraries/default/InboxProcessors/GenericInboxProcessor.php +++ b/libraries/default/InboxProcessors/GenericInboxProcessor.php @@ -76,8 +76,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(); } From 745340adff1fa9e47f72e7165beea4b8b64f7903 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Oct 2022 10:23:11 +0200 Subject: [PATCH 02/69] fix: [component:CRUD] Only show metafields filters wjen the model has the behavior --- src/Controller/Component/CRUDComponent.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index fdb0d60..410b571 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -186,16 +186,16 @@ 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); $this->Controller->set('filters', $filters); $this->Controller->viewBuilder()->setLayout('ajax'); $this->Controller->render('/genericTemplates/filters'); @@ -1421,7 +1421,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'])) { From 726dab255e205fd3c680397af4e27e824abaceb4 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Oct 2022 10:24:01 +0200 Subject: [PATCH 03/69] chg: [inbox:index] Changed quick filter to show `my notification` by default --- src/Controller/InboxController.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 0d07fb9..4ed1845 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -39,9 +39,17 @@ 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([ + 'user_id' => $this->ACL->getUser()['id'], + ]); + } + ], + ], ], 'contain' => $this->containFields ]); From 8d7e2b0df2b899b798c099f19fa3118b97087d0c Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Oct 2022 10:26:03 +0200 Subject: [PATCH 04/69] chg: [inboxes:UI] Renamed `request` into `message` --- src/Controller/Component/Navigation/Inbox.php | 4 ++-- src/Controller/Component/Navigation/Outbox.php | 4 ++-- src/Controller/InboxController.php | 6 +++--- templates/Inbox/index.php | 13 +++++++++---- templates/Outbox/index.php | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) 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 4ed1845..715d060 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -107,11 +107,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/templates/Inbox/index.php b/templates/Inbox/index.php index 6576ec3..07d8c2e 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', ] ] ], @@ -105,7 +109,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 +117,7 @@ echo $this->element('genericElements/IndexTable/index_table', [ ?> + + \ No newline at end of file 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/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 602f637..7d7e612 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -457,6 +457,7 @@ class CRUDComponent extends Component } $entity->setDirty('meta_fields', true); + $entity->_metafields_to_delete = $metaFieldsToDelete; return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete]; } diff --git a/src/Model/Behavior/NotifyAdminsBehavior.php b/src/Model/Behavior/NotifyAdminsBehavior.php new file mode 100644 index 0000000..62db8fb --- /dev/null +++ b/src/Model/Behavior/NotifyAdminsBehavior.php @@ -0,0 +1,270 @@ + [ + '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 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->_getOriginalChangedFields($entity, $watchedFields); + if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) { + $originalFields['meta_fields'] = array_merge( + $originalFields['meta_fields'], + $this->_getDeletedMetafields($entity) + ); + } + $changedFields = $entity->extract(array_keys($originalFields)); + if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) { + $originalFields['meta_fields'] = array_map(function($metaField) { + $originalValues = $metaField->getOriginalValues($metaField->getVisible()); + $metaField->set($originalValues); + return $metaField; + }, $originalFields['meta_fields']); + $originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields']); + $changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields']); + } + $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', + count($changedFields), + count($changedFields) + ); + } + $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 _getOriginalChangedFields($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); + return $originalChangedFields; + } + + 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 _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); + } +} From ee5adaf971fa2fb3d0244e238fe895169792b647 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 10:12:06 +0200 Subject: [PATCH 14/69] chg: [behavior:adminNotification] Added support of watched fields and improved metafield integration --- src/Model/Behavior/NotifyAdminsBehavior.php | 87 ++++++++++++++++----- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/src/Model/Behavior/NotifyAdminsBehavior.php b/src/Model/Behavior/NotifyAdminsBehavior.php index 62db8fb..6016684 100644 --- a/src/Model/Behavior/NotifyAdminsBehavior.php +++ b/src/Model/Behavior/NotifyAdminsBehavior.php @@ -72,6 +72,7 @@ class NotifyAdminsBehavior extends Behavior $watchedFields = !empty($this->getConfig()['fields']) ? $this->getConfig()['fields'] : $entity->getVisible(); $originalFields = []; $changedFields = $entity->extract($watchedFields); + $titleTemplate = 'New {0} `{1}` created'; $title = __( $titleTemplate, @@ -79,24 +80,19 @@ class NotifyAdminsBehavior extends Behavior $entity->get($this->table()->getDisplayField()) ); $message = __('New {0}', Inflector::singularize($entity->getSource())); + if (!$entity->isNew()) { - $originalFields = $this->_getOriginalChangedFields($entity, $watchedFields); - if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) { - $originalFields['meta_fields'] = array_merge( - $originalFields['meta_fields'], - $this->_getDeletedMetafields($entity) - ); - } - $changedFields = $entity->extract(array_keys($originalFields)); - if ($entity->table()->hasBehavior('MetaFields') && !empty($entity->meta_fields)) { - $originalFields['meta_fields'] = array_map(function($metaField) { - $originalValues = $metaField->getOriginalValues($metaField->getVisible()); - $metaField->set($originalValues); - return $metaField; - }, $originalFields['meta_fields']); - $originalFields['meta_fields'] = $this->_massageMetaField($entity, $originalFields['meta_fields']); - $changedFields['meta_fields'] = $this->_massageMetaField($entity, $changedFields['meta_fields']); + $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, @@ -106,10 +102,18 @@ class NotifyAdminsBehavior extends Behavior $message = __n( '{0} field was updated', '{0} fields were updated', - count($changedFields), - count($changedFields) + $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']); + } + } $data = [ 'original' => $this->_serializeFields($originalFields), 'changed' => $this->_serializeFields($changedFields), @@ -198,7 +202,7 @@ class NotifyAdminsBehavior extends Behavior ->all()->toList(); } - protected function _getOriginalChangedFields($entity, array $fields): array + protected function _getOriginalFields(AppModel $entity, array $fields): array { $originalChangedFields = $entity->extractOriginalChanged($fields); $originalChangedFields = array_map(function ($fieldValue) { @@ -218,9 +222,40 @@ class NotifyAdminsBehavior extends Behavior } 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 = []; @@ -250,6 +285,20 @@ class NotifyAdminsBehavior extends Behavior 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) { From 8345fabb50c919fd2f03adea70d9b5c752c9e023 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 10:12:45 +0200 Subject: [PATCH 15/69] fix: [behavior:tag] Get identifier if tag data is an array --- plugins/Tags/src/Model/Behavior/TagBehavior.php | 2 ++ 1 file changed, 2 insertions(+) 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); } From 1919e6703f040040fd74f360ad491f67a7455194 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 10:13:09 +0200 Subject: [PATCH 16/69] fix: [inboxProcessor:dataChange] Template clean-up --- .../templates/Notification/DataChange.php | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/libraries/default/InboxProcessors/templates/Notification/DataChange.php b/libraries/default/InboxProcessors/templates/Notification/DataChange.php index 8cf79b8..952fae1 100644 --- a/libraries/default/InboxProcessors/templates/Notification/DataChange.php +++ b/libraries/default/InboxProcessors/templates/Notification/DataChange.php @@ -54,26 +54,6 @@ echo $this->Bootstrap->modal([ ), 'confirmText' => __('Acknowledge & Discard'), 'confirmIcon' => 'check', - // 'confirmFunction' => 'submitAck' ]); ?> - - - - \ No newline at end of file From d0119b2dba781943ed63c1c448efe09611efe3e1 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 10:14:09 +0200 Subject: [PATCH 17/69] new: [user] Added `notifyAdmin` behavior --- src/Model/Table/UsersTable.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 789c9aa..b8a4782 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -24,6 +24,9 @@ class UsersTable extends AppTable $this->addBehavior('Timestamp'); $this->addBehavior('UUID'); $this->addBehavior('AuditLog'); + $this->addBehavior('NotifyAdmins', [ + 'fields' => ['role_id', 'individual_id', 'organisation_id', 'disabled', 'modified'], + ]); $this->initAuthBehaviors(); $this->belongsTo( 'Individuals', @@ -221,7 +224,7 @@ class UsersTable extends AppTable { if (!empty(Configure::read('keycloak'))) { $success = $this->handleUserUpdate($user); - return $success; + // return $success; } return true; } From 225913f9c65505bfd0751fd2d61f49b4e32b5be3 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 10:14:57 +0200 Subject: [PATCH 18/69] new: [organisation] Added `notifyAdmin` behavior. Might be removed later on if needed --- src/Model/Table/OrganisationsTable.php | 3 +++ 1 file changed, 3 insertions(+) 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', [ From dde7bbe75fb1f8658666fbb23c8e2f6089a07186 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 11:07:21 +0200 Subject: [PATCH 19/69] chg: [behavior:notifyAdmin] Small refactor to better handle deletions --- src/Model/Behavior/NotifyAdminsBehavior.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Model/Behavior/NotifyAdminsBehavior.php b/src/Model/Behavior/NotifyAdminsBehavior.php index 6016684..8497d3b 100644 --- a/src/Model/Behavior/NotifyAdminsBehavior.php +++ b/src/Model/Behavior/NotifyAdminsBehavior.php @@ -19,10 +19,15 @@ class NotifyAdminsBehavior extends Behavior { /** @var array */ protected $_defaultConfig = [ + 'implementedEvents' => [ + 'Model.afterSave' => 'afterSave', + 'Model.afterDelete' => 'afterDelete', + 'Model.beforeDelete' => 'beforeDelete', + ], 'implementedMethods' => [ 'notifySiteAdmins' => 'notifySiteAdmins', 'notifySiteAdminsForEntity' => 'notifySiteAdminsForEntity', - ] + ], ]; /** @var AuditLog|null */ @@ -50,6 +55,13 @@ class NotifyAdminsBehavior extends Behavior $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)) { @@ -114,6 +126,7 @@ class NotifyAdminsBehavior extends Behavior unset($changedFields['meta_fields']); } } + $originalFields = $entity->isNew() ? [] : $entity->isNew(); $data = [ 'original' => $this->_serializeFields($originalFields), 'changed' => $this->_serializeFields($changedFields), From e1499fb705d84db94bd63a420668d0af7ca028f2 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 11:22:55 +0200 Subject: [PATCH 20/69] chg: [inbox:index] Added quick filter on scope --- src/Controller/InboxController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 2003213..4a161bd 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -17,7 +17,7 @@ use Cake\Http\Exception\ForbiddenException; class InboxController extends AppController { public $filterFields = ['scope', 'action', 'title', 'origin', 'message', 'Users.id', 'Users.username']; - public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true]]; + public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin']; public $containFields = ['Users']; public $paginate = [ From aeda393bbab5b6fb0ab21493ea5eb9ce39b094b4 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 15:56:39 +0200 Subject: [PATCH 21/69] chg: [component:CRUD] Improved filtering to support form type based on database column type --- src/Controller/Component/CRUDComponent.php | 32 ++++++++++++++ templates/genericTemplates/filters.php | 51 +++++++++++++++++----- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 7d7e612..a11937e 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -48,6 +48,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); @@ -196,6 +198,16 @@ class CRUDComponent extends Component $this->Controller->set('metaFieldsEnabled', false); } $filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : []; + $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'); @@ -1522,4 +1534,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/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index b101e09..d900104 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,7 +133,6 @@ 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 @@ -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'] = moment($formElement.val()).toISOString() + } else { + rowData['value'] = $formElement.val() + } return rowData } From 0db625ce45ec58a81525d8220f53696988a8df50 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 27 Oct 2022 15:57:35 +0200 Subject: [PATCH 22/69] chg: [inbox:index] Added filtering on `created` time --- src/Controller/InboxController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 4a161bd..388dbfa 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -16,7 +16,7 @@ use Cake\Http\Exception\ForbiddenException; class InboxController extends AppController { - public $filterFields = ['scope', 'action', 'title', 'origin', 'message', 'Users.id', 'Users.username']; + public $filterFields = ['scope', 'action', 'Inbox.created', 'title', 'origin', 'message', 'Users.id', 'Users.username',]; public $quickFilterFields = ['scope', 'action', ['title' => true], ['message' => true], 'origin']; public $containFields = ['Users']; From 4c401e6e2945bd75e9d73486ae5fb778925882f3 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 28 Oct 2022 09:10:26 +0200 Subject: [PATCH 23/69] chg: [ui:layout] Added spacing between toast --- templates/layout/default.php | 2 +- webroot/css/layout.css | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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 -
+
diff --git a/webroot/css/layout.css b/webroot/css/layout.css index 9762b1b..95c1655 100644 --- a/webroot/css/layout.css +++ b/webroot/css/layout.css @@ -207,6 +207,15 @@ main.content { min-width: 25px; } +.main-toast-container { + position: absolute; + top: 15px; + right: 15px; + z-index: 1080; + display: flex; + flex-direction: column; + row-gap: 0.5em; +} /* sidebar */ .sidebar-wrapper { From 351b90d8436a297aeaec84e7271937389ced34f0 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 28 Oct 2022 09:11:21 +0200 Subject: [PATCH 24/69] fix: [helper:boostrap] Make sure all properties are passed to the button component --- 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 e1b4c46..b600061 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -1947,7 +1947,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); } From 67eb9de05a72e1784b2ed11da18f9e158accac1c Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 28 Oct 2022 09:12:30 +0200 Subject: [PATCH 25/69] new: [ui:index_table] Fire pending debounced functions on dropdown hidden --- .../group_table_action/compactDisplay.php | 1 + .../group_table_action/hiddenColumns.php | 23 +++--------- webroot/js/table-settings.js | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) 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(''), - $('
'; + echo '
'; + echo $this->Form->control('org_name', ['label' => __('Organisation Name'), 'class' => 'form-control']); + echo $this->Form->control('org_uuid', [ + 'label' => __('UUID'), + 'class' => 'form-control form-control-sm mb-2', + 'style' => 'font-size: 0.815rem;', + 'div' => ['class' => 'test'], + 'templates' => [ + 'inputContainer' => '
{{content}}
', + 'label' => '', + ], + ]); + echo '
'; + echo $this->Form->control('password', ['type' => 'password', 'label' => __('Password'), 'class' => 'form-control mb-4']); echo $this->Form->control(__('Sign up'), ['type' => 'submit', 'class' => 'btn btn-primary']); @@ -42,4 +56,16 @@ use Cake\Core\Configure; echo ''; echo $this->Form->end(); ?> - \ No newline at end of file + + + \ No newline at end of file diff --git a/templates/element/genericElements/Form/formLayouts/formDefault.php b/templates/element/genericElements/Form/formLayouts/formDefault.php index 56a439a..1658889 100644 --- a/templates/element/genericElements/Form/formLayouts/formDefault.php +++ b/templates/element/genericElements/Form/formLayouts/formDefault.php @@ -4,12 +4,12 @@ - +
- +
-
+
diff --git a/templates/element/genericElements/Form/formLayouts/formRaw.php b/templates/element/genericElements/Form/formLayouts/formRaw.php index 1e445f7..8b66bae 100644 --- a/templates/element/genericElements/Form/formLayouts/formRaw.php +++ b/templates/element/genericElements/Form/formLayouts/formRaw.php @@ -1,6 +1,6 @@ - +
- +
From 178a5b658f531ef2947d83ca83eb9fe7b0c1a232 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 12 Dec 2022 16:49:52 +0100 Subject: [PATCH 67/69] chg: [behavior:keycloak] Perform case insensitive comparison For both cerebrate and keycloak users --- src/Model/Behavior/AuthKeycloakBehavior.php | 28 +++++++-------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 622c510..6b8904b 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -52,7 +52,7 @@ class AuthKeycloakBehavior extends Behavior ->where(['username' => $profile_payload[$fields['username']]]) ->contain('Individuals') ->first(); - if ($existingUser['individual']['email'] !== $profile_payload[$fields['email']]) { + if (mb_strtolower($existingUser['individual']['email']) !== mb_strtolower($profile_payload[$fields['email']])) { return false; } return $existingUser; @@ -297,7 +297,6 @@ class AuthKeycloakBehavior extends Behavior 'modified' => [], ]; foreach ($users as &$user) { - $changed = false; if (empty($keycloakUsersParsed[$user['username']])) { if ($this->createUser($user, $clientId)) { $changes['created'][] = $user['username']; @@ -355,16 +354,7 @@ class AuthKeycloakBehavior extends Behavior private function checkAndUpdateUser(array $keycloakUser, array $user): bool { - if ( - $keycloakUser['enabled'] == $user['disabled'] || - $keycloakUser['firstName'] !== $user['individual']['first_name'] || - $keycloakUser['lastName'] !== $user['individual']['last_name'] || - $keycloakUser['email'] !== $user['individual']['email'] || - (empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']) || - (empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']) || - (empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']) || - (empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid']) - ) { + if ($this->checkKeycloakUserRequiresUpdate($keycloakUser, $user)) { $change = [ 'enabled' => !$user['disabled'], 'firstName' => $user['individual']['first_name'], @@ -416,13 +406,13 @@ class AuthKeycloakBehavior extends Behavior { $condEnabled = $keycloakUser['enabled'] == $user['disabled']; - $condFirstname = $keycloakUser['firstName'] !== $user['individual']['first_name']; - $condLastname = $keycloakUser['lastName'] !== $user['individual']['last_name']; - $condEmail = $keycloakUser['email'] !== $user['individual']['email']; - $condRolename = (empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']); - $condRoleuuid = (empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']); - $condOrgname = (empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']); - $condOrguuid = (empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid']); + $condFirstname = mb_strtolower($keycloakUser['firstName']) !== mb_strtolower($user['individual']['first_name']); + $condLastname = mb_strtolower($keycloakUser['lastName']) !== mb_strtolower($user['individual']['last_name']); + $condEmail = mb_strtolower($keycloakUser['email']) !== mb_strtolower($user['individual']['email']); + $condRolename = (empty($keycloakUser['attributes']['role_name']) || mb_strtolower($keycloakUser['attributes']['role_name']) !== mb_strtolower($user['role']['name'])); + $condRoleuuid = (empty($keycloakUser['attributes']['role_uuid']) || mb_strtolower($keycloakUser['attributes']['role_uuid']) !== mb_strtolower($user['role']['uuid'])); + $condOrgname = (empty($keycloakUser['attributes']['org_name']) || mb_strtolower($keycloakUser['attributes']['org_name']) !== mb_strtolower($user['organisation']['name'])); + $condOrguuid = (empty($keycloakUser['attributes']['org_uuid']) || mb_strtolower($keycloakUser['attributes']['org_uuid']) !== mb_strtolower($user['organisation']['uuid'])); if ($condEnabled || $condFirstname || $condLastname || $condEmail || $condRolename || $condRoleuuid || $condOrgname || $condOrguuid) { if ($condEnabled) { $differences['enabled'] = ['keycloak' => $keycloakUser['enabled'], 'cerebrate' => $user['disabled']]; From d293cb52f80529372bdd21279bc6d1f2db8db534 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 12 Dec 2022 16:56:51 +0100 Subject: [PATCH 68/69] chg: [behavior:keycloak] Gracefully handle issues while syncing with keycloak --- src/Model/Behavior/AuthKeycloakBehavior.php | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 6b8904b..60bebbb 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -297,14 +297,26 @@ class AuthKeycloakBehavior extends Behavior 'modified' => [], ]; foreach ($users as &$user) { - 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']; + try { + 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']; + } } + } catch (\Exception $e) { + $this->_table->auditLogs()->insert([ + 'request_action' => 'syncUsers', + 'model' => 'User', + 'model_id' => 0, + 'model_title' => __('Failed to create or modify user ({0}) in keycloak', $user['username']), + 'changed' => [ + 'message' => $e->getMessage(), + ] + ]); } } return $changes; From c700800d8c4b430c8e32ec680357cf82efb2b78f Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 13 Dec 2022 09:45:09 +0100 Subject: [PATCH 69/69] chg: [version] bump --- src/VERSION.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VERSION.json b/src/VERSION.json index a51d7e0..dd92104 100644 --- a/src/VERSION.json +++ b/src/VERSION.json @@ -1,4 +1,4 @@ { - "version": "1.8", + "version": "1.9", "application": "Cerebrate" }