diff --git a/config/Migrations/20211005163854_UserSettings.php b/config/Migrations/20211005163854_UserSettings.php new file mode 100644 index 0000000..2049680 --- /dev/null +++ b/config/Migrations/20211005163854_UserSettings.php @@ -0,0 +1,63 @@ +table('user_settings', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + $table + ->addColumn('id', 'integer', [ + 'autoIncrement' => true, + 'limit' => 10, + 'signed' => false, + ]) + ->addPrimaryKey('id') + ->addColumn('name', 'string', [ + 'default' => null, + 'null' => false, + 'limit' => 255, + 'comment' => 'The name of the user setting', + ]) + ->addColumn('value', 'text', [ + 'default' => null, + 'null' => true, + 'limit' => MysqlAdapter::TEXT_LONG, + 'comment' => 'The value of the user setting', + ]) + ->addColumn('user_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10, + ]) + ->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]) + ->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + + $table->addForeignKey('user_id', 'users', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']); + + $table->addIndex('name') + ->addIndex('user_id') + ->addIndex('created') + ->addIndex('modified'); + + $table->create(); + } +} diff --git a/src/Command/ImporterCommand.php b/src/Command/ImporterCommand.php index c79ba76..ecbf6ac 100644 --- a/src/Command/ImporterCommand.php +++ b/src/Command/ImporterCommand.php @@ -128,7 +128,9 @@ class ImporterCommand extends Command $this->loadModel('MetaFields'); $entities = []; if (is_null($primary_key)) { - $entities = $table->newEntities($data); + $entities = $table->newEntities($data, [ + 'accessibleFields' => ($table->newEmptyEntity())->getAccessibleFieldForNew() + ]); } else { foreach ($data as $i => $item) { $entity = null; @@ -145,7 +147,9 @@ class ImporterCommand extends Command $this->lockAccess($entity); } if (!is_null($entity)) { - $entity = $table->patchEntity($entity, $item); + $entity = $table->patchEntity($entity, $item, [ + 'accessibleFields' => $entity->getAccessibleFieldForNew() + ]); $entities[] = $entity; } } diff --git a/src/Command/config/config-misp-format-organisation.json b/src/Command/config/config-misp-format-organisation.json new file mode 100644 index 0000000..7e591c0 --- /dev/null +++ b/src/Command/config/config-misp-format-organisation.json @@ -0,0 +1,11 @@ +{ + "format": "json", + "mapping": { + "name": "{n}.Organisation.name", + "uuid": "{n}.Organisation.uuid", + "nationality": "{n}.Organisation.nationality" + }, + "sourceHeaders": { + "Authorization": "~~YOUR_API_KEY_HERE~~" + } +} \ No newline at end of file diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 34ab727..b6d0e29 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -102,7 +102,7 @@ class AppController extends Controller $this->ACL->setPublicInterfaces(); if (!empty($this->request->getAttribute('identity'))) { $user = $this->Users->get($this->request->getAttribute('identity')->getIdentifier(), [ - 'contain' => ['Roles', 'Individuals' => 'Organisations'] + 'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings'] ]); if (!empty($user['disabled'])) { $this->Authentication->logout(); @@ -112,6 +112,8 @@ class AppController extends Controller unset($user['password']); $this->ACL->setUser($user); $this->isAdmin = $user['role']['perm_admin']; + $this->set('menu', $this->ACL->getMenu()); + $this->set('loggedUser', $this->ACL->getUser()); } else if ($this->ParamHandler->isRest()) { throw new MethodNotAllowedException(__('Invalid user credentials.')); } @@ -126,12 +128,16 @@ class AppController extends Controller } $this->ACL->checkAccess(); - $this->set('menu', $this->ACL->getMenu()); $this->set('breadcrumb', $this->Navigation->getBreadcrumb()); $this->set('ajax', $this->request->is('ajax')); $this->request->getParam('prefix'); $this->set('baseurl', Configure::read('App.fullBaseUrl')); - $this->set('bsTheme', Configure::read('Cerebrate')['ui.bsTheme']); + + if (!empty($user) && !empty($user->user_settings_by_name_with_fallback['ui.bsTheme']['value'])) { + $this->set('bsTheme', $user->user_settings_by_name_with_fallback['ui.bsTheme']['value']); + } else { + $this->set('bsTheme', Configure::read('Cerebrate')['ui.bsTheme']); + } if ($this->modelClass == 'Tags.Tags') { $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate'); diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 58c19fc..1376d96 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -457,6 +457,9 @@ class ACLComponent extends Component { $menu = $this->Navigation->getSideMenu(); foreach ($menu as $group => $subMenu) { + if ($group == '__bookmarks') { + continue; + } foreach ($subMenu as $subMenuElementName => $subMenuElement) { if (!empty($subMenuElement['url']) && !$this->checkAccessUrl($subMenuElement['url'], true) === true) { unset($menu[$group][$subMenuElementName]); diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 827ac1c..585eeba 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -196,6 +196,7 @@ class CRUDComponent extends Component } } } + $this->Controller->entity = $data; $this->Controller->set('entity', $data); } @@ -317,6 +318,7 @@ class CRUDComponent extends Component } } } + $this->Controller->entity = $data; $this->Controller->set('entity', $data); } diff --git a/src/Controller/Component/Navigation/Broods.php b/src/Controller/Component/Navigation/Broods.php new file mode 100644 index 0000000..105c34a --- /dev/null +++ b/src/Controller/Component/Navigation/Broods.php @@ -0,0 +1,13 @@ +bcf->addLink('Broods', 'view', 'LocalTools', 'broodTools'); + $this->bcf->addLink('Broods', 'edit', 'LocalTools', 'broodTools'); + } +} diff --git a/src/Controller/Component/Navigation/EncryptionKeys.php b/src/Controller/Component/Navigation/EncryptionKeys.php new file mode 100644 index 0000000..e11c137 --- /dev/null +++ b/src/Controller/Component/Navigation/EncryptionKeys.php @@ -0,0 +1,8 @@ +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'), + 'icon' => 'trash', + 'url' => '/inbox/discard/{{id}}', + 'url_vars' => ['id' => 'id'], + ]); + $this->bcf->addRoute('Inbox', 'process', [ + 'label' => __('Process request'), + 'icon' => 'cogs', + 'url' => '/inbox/process/{{id}}', + 'url_vars' => ['id' => 'id'], + ]); + } + + public function addParents() + { + $this->bcf->addParent('Inbox', 'view', 'Inbox', 'index'); + $this->bcf->addParent('Inbox', 'discard', 'Inbox', 'index'); + $this->bcf->addParent('Inbox', 'process', 'Inbox', 'index'); + } + + public function addLinks() + { + $this->bcf->addSelfLink('Inbox', 'view'); + } + + public function addActions() + { + $this->bcf->addAction('Inbox', 'view', 'Inbox', 'process'); + $this->bcf->addAction('Inbox', 'view', 'Inbox', 'discard'); + + } +} diff --git a/src/Controller/Component/Navigation/Individuals.php b/src/Controller/Component/Navigation/Individuals.php new file mode 100644 index 0000000..e592e13 --- /dev/null +++ b/src/Controller/Component/Navigation/Individuals.php @@ -0,0 +1,8 @@ +bcf->addRoute('Instance', 'home', [ + 'label' => __('Home'), + 'url' => '/', + 'icon' => 'home' + ]); + $this->bcf->addRoute('Instance', 'settings', [ + 'label' => __('Settings'), + 'url' => '/instance/settings', + 'icon' => 'cogs' + ]); + $this->bcf->addRoute('Instance', 'migrationIndex', [ + 'label' => __('Database Migration'), + 'url' => '/instance/migrationIndex', + 'icon' => 'database' + ]); + } +} diff --git a/src/Controller/Component/Navigation/LocalTools.php b/src/Controller/Component/Navigation/LocalTools.php new file mode 100644 index 0000000..7ab2e72 --- /dev/null +++ b/src/Controller/Component/Navigation/LocalTools.php @@ -0,0 +1,49 @@ +bcf->addRoute('LocalTools', 'viewConnector', [ + 'label' => __('View'), + 'textGetter' => 'connector', + 'url' => '/localTools/viewConnector/{{connector}}', + 'url_vars' => ['connector' => 'connector'], + ]); + $this->bcf->addRoute('LocalTools', 'broodTools', [ + 'label' => __('Brood Tools'), + 'url' => '/localTools/broodTools/{{id}}', + 'url_vars' => ['id' => 'id'], + ]); + } + + public function addParents() + { + $this->bcf->addParent('LocalTools', 'viewConnector', 'LocalTools', 'index'); + } + + public function addLinks() + { + $passedData = $this->request->getParam('pass'); + if (!empty($passedData[0])) { + $brood_id = $passedData[0]; + $this->bcf->addParent('LocalTools', 'broodTools', 'Broods', 'view', [ + 'textGetter' => [ + 'path' => 'name', + 'varname' => 'broodEntity', + ], + 'url' => "/broods/view/{$brood_id}", + ]); + $this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'view', [ + 'url' => "/broods/view/{$brood_id}", + ]); + $this->bcf->addLink('LocalTools', 'broodTools', 'Broods', 'edit', [ + 'url' => "/broods/view/{$brood_id}", + ]); + } + $this->bcf->addSelfLink('LocalTools', 'broodTools'); + } +} diff --git a/src/Controller/Component/Navigation/MetaTemplates.php b/src/Controller/Component/Navigation/MetaTemplates.php new file mode 100644 index 0000000..91ba4eb --- /dev/null +++ b/src/Controller/Component/Navigation/MetaTemplates.php @@ -0,0 +1,42 @@ +bcf->addRoute('MetaTemplates', 'index', $this->bcf->defaultCRUD('MetaTemplates', 'index')); + $this->bcf->addRoute('MetaTemplates', 'view', $this->bcf->defaultCRUD('MetaTemplates', 'view')); + $this->bcf->addRoute('MetaTemplates', 'enable', [ + 'label' => __('Enable'), + 'icon' => 'check', + 'url' => '/metaTemplates/enable/{{id}}/enabled', + 'url_vars' => ['id' => 'id'], + ]); + $this->bcf->addRoute('MetaTemplates', 'set_default', [ + 'label' => __('Set as default'), + 'icon' => 'check', + 'url' => '/metaTemplates/toggle/{{id}}/default', + 'url_vars' => ['id' => 'id'], + ]); + } + + public function addParents() + { + $this->bcf->addParent('MetaTemplates', 'view', 'MetaTemplates', 'index'); + } + + public function addLinks() + { + $this->bcf->addSelfLink('MetaTemplates', 'view'); + } + + public function addActions() + { + $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'enable'); + $this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'set_default'); + } +} diff --git a/src/Controller/Component/Navigation/Organisations.php b/src/Controller/Component/Navigation/Organisations.php new file mode 100644 index 0000000..3df07c1 --- /dev/null +++ b/src/Controller/Component/Navigation/Organisations.php @@ -0,0 +1,8 @@ +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'), + 'icon' => 'trash', + 'url' => '/outbox/discard/{{id}}', + 'url_vars' => ['id' => 'id'], + ]); + $this->bcf->addRoute('Outbox', 'process', [ + 'label' => __('Process request'), + 'icon' => 'cogs', + 'url' => '/outbox/process/{{id}}', + 'url_vars' => ['id' => 'id'], + ]); + } + + public function addParents() + { + $this->bcf->addParent('Outbox', 'view', 'Outbox', 'index'); + $this->bcf->addParent('Outbox', 'discard', 'Outbox', 'index'); + $this->bcf->addParent('Outbox', 'process', 'Outbox', 'index'); + } + + public function addLinks() + { + $this->bcf->addSelfLink('Outbox', 'view'); + } + + public function addActions() + { + $this->bcf->addAction('Outbox', 'view', 'Outbox', 'process'); + $this->bcf->addAction('Outbox', 'view', 'Outbox', 'discard'); + } +} diff --git a/src/Controller/Component/Navigation/Roles.php b/src/Controller/Component/Navigation/Roles.php new file mode 100644 index 0000000..b0501cb --- /dev/null +++ b/src/Controller/Component/Navigation/Roles.php @@ -0,0 +1,8 @@ +bcf; + $request = $this->request; + $this->bcf->addLink('UserSettings', 'index', 'Users', 'view', function ($config) use ($bcf, $request) { + if (!empty($request->getQuery('Users_id'))) { + $user_id = h($request->getQuery('Users_id')); + $linkData = [ + 'label' => __('View user [{0}]', h($user_id)), + 'url' => sprintf('/users/view/%s', h($user_id)) + ]; + return $linkData; + } + return null; + }); + $this->bcf->addLink('UserSettings', 'index', 'Users', 'edit', function ($config) use ($bcf, $request) { + if (!empty($request->getQuery('Users_id'))) { + $user_id = h($request->getQuery('Users_id')); + $linkData = [ + 'label' => __('Edit user [{0}]', h($user_id)), + 'url' => sprintf('/users/edit/%s', h($user_id)) + ]; + return $linkData; + } + return null; + }); + if (!empty($request->getQuery('Users_id'))) { + $this->bcf->addSelfLink('UserSettings', 'index'); + } + if ($this->request->getParam('controller') == 'UserSettings' && $this->request->getParam('action') == 'index') { + if (!empty($this->request->getQuery('Users_id'))) { + $user_id = $this->request->getQuery('Users_id'); + $this->bcf->addParent('UserSettings', 'index', 'Users', 'view', [ + 'textGetter' => [ + 'path' => 'username', + 'varname' => 'settingsForUser', + ], + 'url' => "/users/view/{$user_id}" + ]); + } + } + } +} diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php new file mode 100644 index 0000000..7e228db --- /dev/null +++ b/src/Controller/Component/Navigation/Users.php @@ -0,0 +1,87 @@ +bcf->addRoute('Users', 'settings', [ + 'label' => __('User settings'), + 'url' => '/users/settings/', + 'icon' => 'user-cog' + ]); + } + + public function addParents() + { + // $this->bcf->addParent('Users', 'settings', 'Users', 'view'); + } + + public function addLinks() + { + $bcf = $this->bcf; + $request = $this->request; + $passedData = $this->request->getParam('pass'); + $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) { + if (!empty($passedData[0])) { + $user_id = $passedData[0]; + $linkData = [ + 'label' => __('Account settings', h($user_id)), + 'url' => sprintf('/users/settings/%s', h($user_id)) + ]; + return $linkData; + } + return []; + }); + $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) { + if (!empty($passedData[0])) { + $user_id = $passedData[0]; + $linkData = [ + 'label' => __('User Setting [{0}]', h($user_id)), + 'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id)) + ]; + return $linkData; + } + return []; + }); + $this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) { + if (!empty($passedData[0])) { + $user_id = $passedData[0]; + $linkData = [ + 'label' => __('Account settings', h($user_id)), + 'url' => sprintf('/users/settings/%s', h($user_id)) + ]; + return $linkData; + } + return []; + }); + $this->bcf->addLink('Users', 'edit', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) { + if (!empty($passedData[0])) { + $user_id = $passedData[0]; + $linkData = [ + 'label' => __('User Setting [{0}]', h($user_id)), + 'url' => sprintf('/user-settings/index?Users.id=%s', h($user_id)) + ]; + return $linkData; + } + return []; + }); + + $this->bcf->addLink('Users', 'settings', 'Users', 'view', function ($config) use ($bcf, $request, $passedData) { + if (!empty($passedData[0])) { + $user_id = $passedData[0]; + $linkData = [ + 'label' => __('View user', h($user_id)), + 'url' => sprintf('/users/view/%s', h($user_id)) + ]; + return $linkData; + } + return []; + }); + $this->bcf->addSelfLink('Users', 'settings', [ + 'label' => __('Account settings') + ]); + } +} diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php new file mode 100644 index 0000000..84dde96 --- /dev/null +++ b/src/Controller/Component/Navigation/base.php @@ -0,0 +1,19 @@ +bcf = $bcf; + $this->request = $request; + } + + public function addRoutes() {} + public function addParents() {} + public function addLinks() {} + public function addActions() {} +} \ No newline at end of file diff --git a/src/Controller/Component/Navigation/sidemenu.php b/src/Controller/Component/Navigation/sidemenu.php new file mode 100644 index 0000000..e56ac7f --- /dev/null +++ b/src/Controller/Component/Navigation/sidemenu.php @@ -0,0 +1,147 @@ +iconTable = $iconTable; + $this->request = $request; + } + + public function get(): array + { + return [ + __('ContactDB') => [ + 'Individuals' => [ + 'label' => __('Individuals'), + 'icon' => $this->iconTable['Individuals'], + 'url' => '/individuals/index', + ], + 'Organisations' => [ + 'label' => __('Organisations'), + 'icon' => $this->iconTable['Organisations'], + 'url' => '/organisations/index', + ], + 'EncryptionKeys' => [ + 'label' => __('Encryption keys'), + 'icon' => $this->iconTable['EncryptionKeys'], + 'url' => '/encryptionKeys/index', + ] + ], + __('Trust Circles') => [ + 'SharingGroups' => [ + 'label' => __('Sharing Groups'), + 'icon' => $this->iconTable['SharingGroups'], + 'url' => '/sharingGroups/index', + ] + ], + __('Synchronisation') => [ + 'Broods' => [ + 'label' => __('Broods'), + 'icon' => $this->iconTable['Broods'], + 'url' => '/broods/index', + ] + ], + __('Administration') => [ + 'Roles' => [ + 'label' => __('Roles'), + 'icon' => $this->iconTable['Roles'], + 'url' => '/roles/index', + ], + 'Users' => [ + 'label' => __('Users'), + 'icon' => $this->iconTable['Users'], + 'url' => '/users/index', + ], + 'UserSettings' => [ + 'label' => __('Users Settings'), + 'icon' => $this->iconTable['UserSettings'], + 'url' => '/user-settings/index', + ], + 'Messages' => [ + 'label' => __('Messages'), + 'icon' => $this->iconTable['Inbox'], + 'url' => '/inbox/index', + 'children' => [ + 'index' => [ + 'url' => '/inbox/index', + 'label' => __('Inbox') + ], + 'outbox' => [ + 'url' => '/outbox/index', + 'label' => __('Outbox') + ], + ] + ], + 'Add-ons' => [ + 'label' => __('Add-ons'), + 'icon' => 'puzzle-piece', + 'children' => [ + 'MetaTemplates.index' => [ + 'label' => __('Meta Field Templates'), + 'icon' => $this->iconTable['MetaTemplates'], + 'url' => '/metaTemplates/index', + ], + 'LocalTools.index' => [ + 'label' => __('Local Tools'), + 'icon' => $this->iconTable['LocalTools'], + 'url' => '/localTools/index', + ], + 'Tags.index' => [ + 'label' => __('Tags'), + 'icon' => $this->iconTable['Tags'], + 'url' => '/tags/index', + ], + ] + ], + 'Instance' => [ + 'label' => __('Instance'), + 'icon' => $this->iconTable['Instance'], + 'children' => [ + 'Settings' => [ + 'label' => __('Settings'), + 'url' => '/instance/settings', + 'icon' => 'cogs', + ], + 'Database' => [ + 'label' => __('Database'), + 'url' => '/instance/migrationIndex', + 'icon' => 'database', + ], + ] + ], + ], + 'Open' => [ + 'Organisations' => [ + 'label' => __('Organisations'), + 'icon' => $this->iconTable['Organisations'], + 'url' => '/open/organisations/index', + 'children' => [ + 'index' => [ + 'url' => '/open/organisations/index', + 'label' => __('List organisations') + ], + ], + 'open' => in_array('organisations', Configure::read('Cerebrate.open')) + ], + 'Individuals' => [ + 'label' => __('Individuals'), + 'icon' => $this->iconTable['Individuals'], + 'url' => '/open/individuals/index', + 'children' => [ + 'index' => [ + 'url' => '/open/individuals/index', + 'label' => __('List individuals') + ], + ], + 'open' => in_array('individuals', Configure::read('Cerebrate.open')) + ] + ] + ]; + } +} \ No newline at end of file diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php index a183b7e..f0dd453 100644 --- a/src/Controller/Component/NavigationComponent.php +++ b/src/Controller/Component/NavigationComponent.php @@ -4,10 +4,16 @@ namespace App\Controller\Component; use Cake\Controller\Component; use Cake\Core\Configure; +use Cake\Core\App; use Cake\Utility\Inflector; use Cake\Utility\Hash; +use Cake\Filesystem\Folder; use Cake\Routing\Router; use Cake\ORM\TableRegistry; +use Exception; + +use SidemenuNavigation\Sidemenu; +require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'sidemenu.php'); class NavigationComponent extends Component { @@ -21,6 +27,7 @@ class NavigationComponent extends Component 'Broods' => 'network-wired', 'Roles' => 'id-badge', 'Users' => 'users', + 'UserSettings' => 'user-cog', 'Inbox' => 'inbox', 'Outbox' => 'inbox', 'MetaTemplates' => 'object-group', @@ -42,389 +49,340 @@ class NavigationComponent extends Component public function getSideMenu(): array { - return [ - 'ContactDB' => [ - 'Individuals' => [ - 'label' => __('Individuals'), - 'icon' => $this->iconToTableMapping['Individuals'], - 'url' => '/individuals/index', - ], - 'Organisations' => [ - 'label' => __('Organisations'), - 'icon' => $this->iconToTableMapping['Organisations'], - 'url' => '/organisations/index', - ], - 'EncryptionKeys' => [ - 'label' => __('Encryption keys'), - 'icon' => $this->iconToTableMapping['EncryptionKeys'], - 'url' => '/encryptionKeys/index', - ] - ], - 'Trust Circles' => [ - 'SharingGroups' => [ - 'label' => __('Sharing Groups'), - 'icon' => $this->iconToTableMapping['SharingGroups'], - 'url' => '/sharingGroups/index', - ] - ], - 'Sync' => [ - 'Broods' => [ - 'label' => __('Broods'), - 'icon' => $this->iconToTableMapping['Broods'], - 'url' => '/broods/index', - ] - ], - 'Administration' => [ - 'Roles' => [ - 'label' => __('Roles'), - 'icon' => $this->iconToTableMapping['Roles'], - 'url' => '/roles/index', - ], - 'Users' => [ - 'label' => __('Users'), - 'icon' => $this->iconToTableMapping['Users'], - 'url' => '/users/index', - ], - 'Messages' => [ - 'label' => __('Messages'), - 'icon' => $this->iconToTableMapping['Inbox'], - 'url' => '/inbox/index', - 'children' => [ - 'index' => [ - 'url' => '/inbox/index', - 'label' => __('Inbox') - ], - 'outbox' => [ - 'url' => '/outbox/index', - 'label' => __('Outbox') - ], - ] - ], - 'Add-ons' => [ - 'label' => __('Add-ons'), - 'icon' => 'puzzle-piece', - 'children' => [ - 'MetaTemplates.index' => [ - 'label' => __('Meta Field Templates'), - 'icon' => $this->iconToTableMapping['MetaTemplates'], - 'url' => '/metaTemplates/index', - ], - 'LocalTools.index' => [ - 'label' => __('Local Tools'), - 'icon' => $this->iconToTableMapping['LocalTools'], - 'url' => '/localTools/index', - ], - 'Tags.index' => [ - 'label' => __('Tags'), - 'icon' => $this->iconToTableMapping['Tags'], - 'url' => '/tags/index', - ], - ] - ], - 'Instance' => [ - 'label' => __('Instance'), - 'icon' => $this->iconToTableMapping['Instance'], - 'children' => [ - 'Settings' => [ - 'label' => __('Settings'), - 'url' => '/instance/settings', - 'icon' => 'cogs', - ], - 'Database' => [ - 'label' => __('Database'), - 'url' => '/instance/migrationIndex', - 'icon' => 'database', - ], - ] - ], - ], - 'Open' => [ - 'Organisations' => [ - 'label' => __('Organisations'), - 'icon' => $this->iconToTableMapping['Organisations'], - 'url' => '/open/organisations/index', - 'children' => [ - 'index' => [ - 'url' => '/open/organisations/index', - 'label' => __('List organisations') - ], - ], - 'open' => in_array('organisations', Configure::read('Cerebrate.open')) - ], - 'Individuals' => [ - 'label' => __('Individuals'), - 'icon' => $this->iconToTableMapping['Individuals'], - 'url' => '/open/individuals/index', - 'children' => [ - 'index' => [ - 'url' => '/open/individuals/index', - 'label' => __('List individuals') - ], - ], - 'open' => in_array('individuals', Configure::read('Cerebrate.open')) - ] - ] - ]; + $sidemenu = new Sidemenu($this->iconToTableMapping, $this->request); + $sidemenu = $sidemenu->get(); + $sidemenu = $this->addUserBookmarks($sidemenu); + return $sidemenu; } + + public function addUserBookmarks($sidemenu): array + { + $bookmarks = $this->getUserBookmarks(); + $sidemenu = array_merge([ + '__bookmarks' => $bookmarks + ], $sidemenu); + return $sidemenu; + } + + public function getUserBookmarks(): array + { + $userSettingTable = TableRegistry::getTableLocator()->get('UserSettings'); + $setting = $userSettingTable->getSettingByName($this->request->getAttribute('identity'), 'ui.bookmarks'); + $bookmarks = is_null($setting) ? [] : json_decode($setting->value, true); + + $links = array_map(function($bookmark) { + return [ + 'name' => $bookmark['name'], + 'label' => $bookmark['label'], + 'url' => $bookmark['url'], + ]; + }, $bookmarks); + return $links; + } + public function getBreadcrumb(): array { $controller = $this->request->getParam('controller'); $action = $this->request->getParam('action'); - if (empty($this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"])) { + if (empty($this->fullBreadcrumb[$controller][$action])) { return [[ 'label' => $controller, 'url' => Router::url(['controller' => $controller, 'action' => $action]), ]]; // no breadcrumb defined for this endpoint } - $currentRoute = $this->fullBreadcrumb[$controller]['routes']["{$controller}:{$action}"]; - $breadcrumbPath = $this->getBreadcrumbPath("{$controller}:{$action}", $currentRoute); - return $breadcrumbPath['objects']; + $currentRoute = $this->fullBreadcrumb[$controller][$action]; + $breadcrumbPath = $this->getBreadcrumbPath($currentRoute); + return $breadcrumbPath; } - public function getBreadcrumbPath(string $startRoute, array $currentRoute): array + public function getBreadcrumbPath(array $currentRoute): array { - $route = $startRoute; - $path = [ - 'routes' => [], - 'objects' => [], - ]; - $visited = []; - while (empty($visited[$route])) { - $visited[$route] = true; - $path['routes'][] = $route; - $path['objects'][] = $currentRoute; + $path = []; + $visitedURL = []; + while (empty($visitedURL[$currentRoute['url']])) { + $visitedURL[$currentRoute['url']] = true; + $path[] = $currentRoute; if (!empty($currentRoute['after'])) { - $route = $currentRoute['after']; - $split = explode(':', $currentRoute['after']); - $currentRoute = $this->fullBreadcrumb[$split[0]]['routes'][$currentRoute['after']]; + if (is_callable($currentRoute['after'])) { + $route = $currentRoute['after'](); + } else { + $route = $currentRoute['after']; + } + if (empty($route)) { + continue; + } + $currentRoute = $route; } } - $path['routes'] = array_reverse($path['routes']); - $path['objects'] = array_reverse($path['objects']); + $path = array_reverse($path); return $path; } - private function insertInheritance(array $config, array $fullConfig): array - { - if (!empty($config['routes'])) { - foreach ($config['routes'] as $routeName => $value) { - $config['routes'][$routeName]['route_path'] = $routeName; - if (!empty($value['inherit'])) { - $default = $config['defaults'][$value['inherit']] ?? []; - $config['routes'][$routeName] = array_merge($config['routes'][$routeName], $default); - unset($config['routes'][$routeName]['inherit']); - } - } - } - return $config; - } - - private function insertRelated(array $config, array $fullConfig): array - { - if (!empty($config['routes'])) { - foreach ($config['routes'] as $routeName => $value) { - if (!empty($value['links'])) { - foreach ($value['links'] as $i => $linkedRoute) { - $split = explode(':', $linkedRoute); - if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) { - $linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute]; - if (!empty($linkedRouteObject)) { - $config['routes'][$routeName]['links'][$i] = $linkedRouteObject; - continue; - } - } - unset($config['routes'][$routeName]['links'][$i]); - } - } - if (!empty($value['actions'])) { - foreach ($value['actions'] as $i => $linkedRoute) { - $split = explode(':', $linkedRoute); - if (!empty($fullConfig[$split[0]]['routes'][$linkedRoute])) { - $linkedRouteObject = $fullConfig[$split[0]]['routes'][$linkedRoute]; - if (!empty($linkedRouteObject)) { - $config['routes'][$routeName]['actions'][$i] = $linkedRouteObject; - continue; - } - } - unset($config['routes'][$routeName]['actions'][$i]); - } - } - } - } - return $config; - } - - public function getDefaultCRUDConfig(string $controller, array $overrides=[], array $merges=[]): array - { - $table = TableRegistry::getTableLocator()->get($controller); - $default = [ - 'defaults' => [ - 'depth-1' => [ - 'after' => "{$controller}:index", - 'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id', - 'links' => [ - "{$controller}:view", - "{$controller}:edit", - ], - 'actions' => [ - "{$controller}:delete", - ], - ] - ], - 'routes' => [ - "{$controller}:index" => [ - 'label' => Inflector::humanize($controller), - 'url' => "/{$controller}/index", - 'icon' => $this->iconToTableMapping[$controller] - ], - "{$controller}:view" => [ - 'label' => __('View'), - 'icon' => 'eye', - 'inherit' => 'depth-1', - 'url' => "/{$controller}/view/{{id}}", - 'url_vars' => ['id' => 'id'], - ], - "{$controller}:edit" => [ - 'label' => __('Edit'), - 'icon' => 'edit', - 'inherit' => 'depth-1', - 'url' => "/{$controller}/edit/{{id}}", - 'url_vars' => ['id' => 'id'], - ], - "{$controller}:delete" => [ - 'label' => __('Delete'), - 'icon' => 'trash', - 'inherit' => 'depth-1', - 'url' => "/{$controller}/delete/{{id}}", - 'url_vars' => ['id' => 'id'], - ], - ] - ]; - $merged = array_merge_recursive($default, $merges); - $overridden = array_replace_recursive($merged, $overrides); - return $overridden; - } - public function genBreadcrumb(): array { - $fullConfig = [ - 'Individuals' => $this->getDefaultCRUDConfig('Individuals'), - 'Organisations' => $this->getDefaultCRUDConfig('Organisations'), - 'EncryptionKeys' => $this->getDefaultCRUDConfig('EncryptionKeys'), - 'SharingGroups' => $this->getDefaultCRUDConfig('SharingGroups'), - 'Broods' => $this->getDefaultCRUDConfig('Broods', [], [ - 'defaults' => ['depth-1' => ['links' => 'LocalTools:brood_tools']] - ]), - 'Roles' => $this->getDefaultCRUDConfig('Roles'), - 'Users' => $this->getDefaultCRUDConfig('Users'), - 'Inbox' => $this->getDefaultCRUDConfig('Inbox', [ - 'defaults' => ['depth-1' => [ - 'links' => ['Inbox:view', 'Inbox:process'], - 'actions' => ['Inbox:process', 'Inbox:delete'], - ]] - ], [ - 'routes' => [ - 'Inbox:discard' => [ - 'label' => __('Discard request'), - 'inherit' => 'depth-1', - 'url' => '/inbox/discard/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - 'Inbox:process' => [ - 'label' => __('Process request'), - 'inherit' => 'depth-1', - 'url' => '/inbox/process/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - ] - ]), - 'Outbox' => $this->getDefaultCRUDConfig('Outbox', [ - 'defaults' => ['depth-1' => [ - 'links' => ['Outbox:view', 'Outbox:process'], - 'actions' => ['Outbox:process', 'Outbox:delete'], - ]] - ], [ - 'routes' => [ - 'Outbox:discard' => [ - 'label' => __('Discard request'), - 'inherit' => 'depth-1', - 'url' => '/outbox/discard/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - 'Outbox:process' => [ - 'label' => __('Process request'), - 'inherit' => 'depth-1', - 'url' => '/outbox/process/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - ] - ]), - 'MetaTemplates' => $this->getDefaultCRUDConfig('MetaTemplates', [ - 'defaults' => ['depth-1' => [ - 'links' => ['MetaTemplates:view', ''], // '' to remove leftovers. Related to https://www.php.net/manual/en/function.array-replace-recursive.php#124705 - 'actions' => ['MetaTemplates:toggle'], - ]] - ], [ - 'routes' => [ - 'MetaTemplates:toggle' => [ - 'label' => __('Toggle Meta-template'), - 'inherit' => 'depth-1', - 'url' => '/MetaTemplates/toggle/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - ] - ]), - 'Tags' => $this->getDefaultCRUDConfig('Tags', [ - 'defaults' => ['depth-1' => ['textGetter' => 'name']] - ]), - 'LocalTools' => [ - 'routes' => [ - 'LocalTools:index' => [ - 'label' => __('Local Tools'), - 'url' => '/localTools/index', - 'icon' => $this->iconToTableMapping['LocalTools'], - ], - 'LocalTools:viewConnector' => [ - 'label' => __('View'), - 'textGetter' => 'name', - 'url' => '/localTools/viewConnector/{{connector}}', - 'url_vars' => ['connector' => 'connector'], - 'after' => 'LocalTools:index', - ], - 'LocalTools:broodTools' => [ - 'label' => __('Brood Tools'), - 'url' => '/localTools/broodTools/{{id}}', - 'url_vars' => ['id' => 'id'], - ], - ] - ], - 'Instance' => [ - 'routes' => [ - 'Instance:home' => [ - 'label' => __('Home'), - 'url' => '/', - 'icon' => 'home' - ], - 'Instance:settings' => [ - 'label' => __('Settings'), - 'url' => '/instance/settings', - 'icon' => 'cogs' - ], - 'Instance:migrationIndex' => [ - 'label' => __('Database Migration'), - 'url' => '/instance/migrationIndex', - 'icon' => 'database' - ], - ] - ] - ]; - foreach ($fullConfig as $controller => $config) { - $fullConfig[$controller] = $this->insertInheritance($config, $fullConfig); - } - foreach ($fullConfig as $controller => $config) { - $fullConfig[$controller] = $this->insertRelated($config, $fullConfig); - } + $request = $this->request; + $bcf = new BreadcrumbFactory($this->iconToTableMapping); + $fullConfig = $this->getFullConfig($bcf, $this->request); return $fullConfig; } -} \ No newline at end of file + + private function loadNavigationClasses($bcf, $request) + { + $navigationClasses = []; + $navigationDir = new Folder(APP . DS . 'Controller' . DS . 'Component' . DS . 'Navigation'); + $navigationFiles = $navigationDir->find('.*\.php', true); + foreach ($navigationFiles as $navigationFile) { + if ($navigationFile == 'base.php' || $navigationFile == 'sidemenu.php') { + continue; + } + $navigationClassname = str_replace('.php', '', $navigationFile); + require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . $navigationFile); + $reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation"); + $navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request); + } + return $navigationClasses; + } + + public function getFullConfig($bcf, $request) + { + $navigationClasses = $this->loadNavigationClasses($bcf, $request); + $CRUDControllers = [ + 'Individuals', + 'Organisations', + 'EncryptionKeys', + 'SharingGroups', + 'Broods', + 'Roles', + 'Users', + 'Tags', + 'LocalTools', + 'UserSettings', + ]; + foreach ($CRUDControllers as $controller) { + $bcf->setDefaultCRUDForModel($controller); + } + + foreach ($navigationClasses as $className => $class) { + $class->addRoutes(); + } + foreach ($navigationClasses as $className => $class) { + $class->addParents(); + } + foreach ($navigationClasses as $className => $class) { + $class->addLinks(); + } + foreach ($navigationClasses as $className => $class) { + $class->addActions(); + } + return $bcf->getEndpoints(); + } +} + +class BreadcrumbFactory +{ + private $endpoints = []; + private $iconToTableMapping = []; + + public function __construct($iconToTableMapping) + { + $this->iconToTableMapping = $iconToTableMapping; + } + + public function defaultCRUD(string $controller, string $action, array $overrides = []): array + { + $table = TableRegistry::getTableLocator()->get($controller); + $item = []; + if ($action === 'index') { + $item = $this->genRouteConfig($controller, $action, [ + 'label' => __('{0} index', Inflector::humanize($controller)), + 'url' => "/{$controller}/index", + 'icon' => $this->iconToTableMapping[$controller] + ]); + } else if ($action === 'view') { + $item = $this->genRouteConfig($controller, $action, [ + 'label' => __('View'), + 'icon' => 'eye', + 'url' => "/{$controller}/view/{{id}}", + 'url_vars' => ['id' => 'id'], + 'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id', + ]); + } else if ($action === 'add') { + $item = $this->genRouteConfig($controller, $action, [ + 'label' => __('[new {0}]', $controller), + 'icon' => 'plus', + 'url' => "/{$controller}/add", + ]); + } else if ($action === 'edit') { + $item = $this->genRouteConfig($controller, $action, [ + 'label' => __('Edit'), + 'icon' => 'edit', + 'url' => "/{$controller}/edit/{{id}}", + 'url_vars' => ['id' => 'id'], + 'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id', + ]); + } else if ($action === 'delete') { + $item = $this->genRouteConfig($controller, $action, [ + 'label' => __('Delete'), + 'icon' => 'trash', + 'url' => "/{$controller}/delete/{{id}}", + 'url_vars' => ['id' => 'id'], + 'textGetter' => !empty($table->getDisplayField()) ? $table->getDisplayField() : 'id', + ]); + } + $item['route_path'] = "{$controller}:{$action}"; + $item = array_merge($item, $overrides); + return $item; + } + + public function genRouteConfig($controller, $action, $config = []) + { + $routeConfig = [ + 'controller' => $controller, + 'action' => $action, + 'route_path' => "{$controller}:{$action}", + ]; + $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'url'); + $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'url_vars'); + $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'icon'); + $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'label'); + $routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'textGetter'); + return $routeConfig; + } + + private function addIfNotEmpty($arr, $data, $key, $default = null) + { + if (!empty($data[$key])) { + $arr[$key] = $data[$key]; + } else { + if (!is_null($default)) { + $arr[$key] = $default; + } + } + return $arr; + } + + public function addRoute($controller, $action, $config = []) { + $this->endpoints[$controller][$action] = $this->genRouteConfig($controller, $action, $config); + } + + public function setDefaultCRUDForModel($controller) + { + $this->addRoute($controller, 'index', $this->defaultCRUD($controller, 'index')); + $this->addRoute($controller, 'view', $this->defaultCRUD($controller, 'view')); + $this->addRoute($controller, 'add', $this->defaultCRUD($controller, 'add')); + $this->addRoute($controller, 'edit', $this->defaultCRUD($controller, 'edit')); + $this->addRoute($controller, 'delete', $this->defaultCRUD($controller, 'delete')); + + $this->addParent($controller, 'view', $controller, 'index'); + $this->addParent($controller, 'add', $controller, 'index'); + $this->addParent($controller, 'edit', $controller, 'index'); + $this->addParent($controller, 'delete', $controller, 'index'); + + $this->addSelfLink($controller, 'view'); + $this->addLink($controller, 'view', $controller, 'edit'); + $this->addLink($controller, 'edit', $controller, 'view'); + $this->addSelfLink($controller, 'edit'); + + $this->addAction($controller, 'view', $controller, 'add'); + $this->addAction($controller, 'view', $controller, 'delete'); + $this->addAction($controller, 'edit', $controller, 'add'); + $this->addAction($controller, 'edit', $controller, 'delete'); + } + + public function get($controller, $action) + { + if (empty($this->endpoints[$controller]) || empty($this->endpoints[$controller][$action])) { + throw new \Exception(sprintf("Tried to add a reference to %s:%s which does not exists", $controller, $action), 1); + } + return $this->endpoints[$controller][$action]; + } + + public function getEndpoints() + { + return $this->endpoints; + } + + public function addParent(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = []) + { + $routeSourceConfig = $this->get($sourceController, $sourceAction); + $routeTargetConfig = $this->get($targetController, $targetAction); + $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig); + if (!is_array($overrides)) { + throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1); + } + $routeTargetConfig = array_merge($routeTargetConfig, $overrides); + $parents = array_merge($routeSourceConfig['after'] ?? [], $routeTargetConfig); + $this->endpoints[$sourceController][$sourceAction]['after'] = $parents; + } + + public function addSelfLink(string $controller, string $action, array $options=[]) + { + $this->addLink($controller, $action, $controller, $action, array_merge($options, [ + 'selfLink' => true, + ])); + } + + public function addLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = []) + { + $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true); + $routeTargetConfig = $this->getRouteConfig($targetController, $targetAction); + $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig); + if (is_null($overrides)) { + // Overrides is null, the link should not be added + return; + } + if (!is_array($overrides)) { + throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1); + } + $routeTargetConfig = array_merge($routeTargetConfig, $overrides); + $links = array_merge($routeSourceConfig['links'] ?? [], [$routeTargetConfig]); + $this->endpoints[$sourceController][$sourceAction]['links'] = $links; + } + + public function addAction(string $sourceController, string $sourceAction, string $targetController, string $targetAction, $overrides = []) + { + $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true); + $routeTargetConfig = $this->getRouteConfig($targetController, $targetAction); + $overrides = $this->execClosureIfNeeded($overrides, $routeSourceConfig); + if (!is_array($overrides)) { + throw new \Exception(sprintf("Override closure for %s:%s -> %s:%s must return an array", $sourceController, $sourceAction, $targetController, $targetAction), 1); + } + $routeTargetConfig = array_merge($routeTargetConfig, $overrides); + $links = array_merge($routeSourceConfig['actions'] ?? [], [$routeTargetConfig]); + $this->endpoints[$sourceController][$sourceAction]['actions'] = $links; + } + + public function removeLink(string $sourceController, string $sourceAction, string $targetController, string $targetAction) + { + $routeSourceConfig = $this->getRouteConfig($sourceController, $sourceAction, true); + if (!empty($routeSourceConfig['links'])) { + foreach ($routeSourceConfig['links'] as $i => $routeConfig) { + if ($routeConfig['controller'] == $targetController && $routeConfig['action'] == $targetAction) { + unset($routeSourceConfig['links'][$i]); + $this->endpoints[$sourceController][$sourceAction]['links'] = $routeSourceConfig['links']; + break; + } + } + } + } + + public function getRouteConfig($controller, $action, $fullRoute = false) + { + $routeConfig = $this->get($controller, $action); + if (empty($fullRoute)) { + unset($routeConfig['after']); + unset($routeConfig['links']); + unset($routeConfig['actions']); + } + return $routeConfig; + } + + private function execClosureIfNeeded($closure, $routeConfig=[]) + { + if (is_callable($closure)) { + return $closure($routeConfig); + } + return $closure; + } +} diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php index 901407a..55340f4 100644 --- a/src/Controller/LocalToolsController.php +++ b/src/Controller/LocalToolsController.php @@ -280,6 +280,8 @@ class LocalToolsController extends AppController return $this->RestResponse->viewData($tools, 'json'); } $this->set('id', $id); + $brood = $this->Broods->get($id); + $this->set('broodEntity', $brood); $this->set('data', $tools); $this->set('metaGroup', 'Administration'); } diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php new file mode 100644 index 0000000..3d73707 --- /dev/null +++ b/src/Controller/UserSettingsController.php @@ -0,0 +1,203 @@ + true], ['value' => true]]; + public $filterFields = ['name', 'value', 'Users.id']; + public $containFields = ['Users']; + + public function index() + { + $conditions = []; + $this->CRUD->index([ + 'conditions' => [], + 'contain' => $this->containFields, + 'filters' => $this->filterFields, + 'quickFilters' => $this->quickFilterFields, + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + if (!empty($this->request->getQuery('Users_id'))) { + $settingsForUser = $this->UserSettings->Users->find()->where([ + 'id' => $this->request->getQuery('Users_id') + ])->first(); + $this->set('settingsForUser', $settingsForUser); + } + } + + public function view($id) + { + $this->CRUD->view($id, [ + 'contain' => ['Users'] + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + + public function add($user_id = false) + { + $this->CRUD->add([ + 'redirect' => ['action' => 'index', $user_id], + 'beforeSave' => function($data) use ($user_id) { + $data['user_id'] = $user_id; + return $data; + } + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $dropdownData = [ + 'user' => $this->UserSettings->Users->find('list', [ + 'sort' => ['username' => 'asc'] + ]), + ]; + $this->set(compact('dropdownData')); + $this->set('user_id', $user_id); + } + + public function edit($id) + { + $entity = $this->UserSettings->find()->where([ + 'id' => $id + ])->first(); + $entity = $this->CRUD->edit($id, [ + 'redirect' => ['action' => 'index', $entity->user_id] + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $dropdownData = [ + 'user' => $this->UserSettings->Users->find('list', [ + 'sort' => ['username' => 'asc'] + ]), + ]; + $this->set(compact('dropdownData')); + $this->set('user_id', $this->entity->user_id); + $this->render('add'); + } + + public function delete($id) + { + $this->CRUD->delete($id); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + + public function getSettingByName($settingsName) + { + $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName); + if (is_null($setting)) { + throw new NotFoundException(__('Invalid {0} for user {1}.', __('User setting'), $this->ACL->getUser()->username)); + } + $this->CRUD->view($setting->id, [ + 'contain' => ['Users'] + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + $this->render('view'); + } + + public function setSetting($settingsName = false) + { + if (!$this->request->is('get')) { + $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName); + if (is_null($setting)) { // setting not found, create it + $result = $this->UserSettings->createSetting($this->ACL->getUser(), $settingsName, $this->request->getData()['value']); + } else { + $result = $this->UserSettings->editSetting($this->ACL->getUser(), $settingsName, $this->request->getData()['value']); + } + $success = !empty($result); + $message = $success ? __('Setting saved') : __('Could not save setting'); + $this->CRUD->setResponseForController('setSetting', $success, $message, $result); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + $this->set('settingName', $settingsName); + } + + public function saveSetting() + { + if ($this->request->is('post')) { + $data = $this->ParamHandler->harvestParams([ + 'name', + 'value' + ]); + $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $data['name']); + if (is_null($setting)) { // setting not found, create it + $result = $this->UserSettings->createSetting($this->ACL->getUser(), $data['name'], $data['value']); + } else { + $result = $this->UserSettings->editSetting($this->ACL->getUser(), $data['name'], $data['value']); + } + $success = !empty($result); + $message = $success ? __('Setting saved') : __('Could not save setting'); + $this->CRUD->setResponseForController('setSetting', $success, $message, $result); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + } + + public function getBookmarks($forSidebar=false) + { + $bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME); + $bookmarks = json_decode($bookmarks['value'], true); + $this->set('user_id', $this->ACL->getUser()->id); + $this->set('bookmarks', $bookmarks); + $this->set('forSidebar', $forSidebar); + $this->render('/element/UserSettings/saved-bookmarks'); + } + + public function saveBookmark() + { + if (!$this->request->is('get')) { + $result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData()); + $success = !empty($result); + $message = $success ? __('Bookmark saved') : __('Could not save bookmark'); + $this->CRUD->setResponseForController('saveBookmark', $success, $message, $result); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + $this->set('user_id', $this->ACL->getUser()->id); + } + + public function deleteBookmark() + { + if (!$this->request->is('get')) { + $result = $this->UserSettings->deleteBookmark($this->ACL->getUser(), $this->request->getData()); + $success = !empty($result); + $message = $success ? __('Bookmark deleted') : __('Could not delete bookmark'); + $this->CRUD->setResponseForController('deleteBookmark', $success, $message, $result); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + $this->set('user_id', $this->ACL->getUser()->id); + } + +} \ No newline at end of file diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index f3059b2..2205f08 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -11,7 +11,7 @@ class UsersController extends AppController { public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name']; public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email']; - public $containFields = ['Individuals', 'Roles']; + public $containFields = ['Individuals', 'Roles', 'UserSettings']; public function index() { @@ -148,6 +148,16 @@ class UsersController extends AppController } } + public function settings() + { + $this->set('user', $this->ACL->getUser()); + $all = $this->Users->UserSettings->getSettingsFromProviderForUser($this->ACL->getUser()['id'], true); + $this->set('settingsProvider', $all['settingsProvider']); + $this->set('settings', $all['settings']); + $this->set('settingsFlattened', $all['settingsFlattened']); + $this->set('notices', $all['notices']); + } + public function register() { $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php index a14e339..15ea1f2 100644 --- a/src/Model/Entity/Individual.php +++ b/src/Model/Entity/Individual.php @@ -16,4 +16,14 @@ class Individual extends AppModel protected $_accessibleOnNew = [ 'uuid' => true, ]; + + protected $_virtual = ['full_name']; + + protected function _getFullName() + { + if (empty($this->first_name) && empty($this->last_name)) { + return $this->username; + } + return sprintf("%s %s", $this->first_name, $this->last_name); + } } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index fe31e50..0e685f5 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -6,9 +6,42 @@ use App\Model\Entity\AppModel; use Cake\ORM\Entity; use Authentication\PasswordHasher\DefaultPasswordHasher; +require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php'); +use App\Settings\SettingsProvider\UserSettingsProvider; + class User extends AppModel { protected $_hidden = ['password', 'confirm_password']; + + protected $_virtual = ['user_settings_by_name', 'user_settings_by_name_with_fallback']; + + protected function _getUserSettingsByName() + { + $settingsByName = []; + if (!empty($this->user_settings)) { + foreach ($this->user_settings as $i => $setting) { + $settingsByName[$setting->name] = $setting; + } + } + return $settingsByName; + } + + protected function _getUserSettingsByNameWithFallback() + { + if (!isset($this->SettingsProvider)) { + $this->SettingsProvider = new UserSettingsProvider(); + } + $settingsByNameWithFallback = []; + if (!empty($this->user_settings)) { + foreach ($this->user_settings as $i => $setting) { + $settingsByNameWithFallback[$setting->name] = $setting->value; + } + } + $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settingsByNameWithFallback); + $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider); + return $settingsFlattened; + } + protected function _setPassword(string $password) : ?string { if (strlen($password) > 0) { diff --git a/src/Model/Entity/UserSetting.php b/src/Model/Entity/UserSetting.php new file mode 100644 index 0000000..e5d12f3 --- /dev/null +++ b/src/Model/Entity/UserSetting.php @@ -0,0 +1,9 @@ +settingsConfiguration = $this->generateSettingsConfiguration(); - $this->setTable(false); $this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.'); $this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.'); $this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.'); - $this->settingValidator = new SettingValidator(); + if (!isset($this->settingValidator)) { + $this->settingValidator = new SettingValidator(); + } } /** @@ -46,151 +46,9 @@ class SettingsProviderTable extends AppTable * redacted [optional]: Should the setting value be redacted. FIXME: To implement * cli_only [optional]: Should this setting be modified only via the CLI. */ - private function generateSettingsConfiguration() + protected function generateSettingsConfiguration() { - return [ - 'Application' => [ - 'General' => [ - 'Essentials' => [ - '_description' => __('Ensentials settings required for the application to run normally.'), - '_icon' => 'user-cog', - 'app.baseurl' => [ - 'name' => __('Base URL'), - 'type' => 'string', - 'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'), - 'default' => '', - 'severity' => 'critical', - 'test' => 'testBaseURL', - ], - 'app.uuid' => [ - 'name' => 'UUID', - 'type' => 'string', - 'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'), - 'default' => '', - 'severity' => 'critical', - 'test' => 'testUuid', - ], - ], - 'Miscellaneous' => [ - 'sc2.hero' => [ - 'description' => 'The true hero', - 'default' => 'Sarah Kerrigan', - 'name' => 'Hero', - 'options' => [ - 'Jim Raynor' => 'Jim Raynor', - 'Sarah Kerrigan' => 'Sarah Kerrigan', - 'Artanis' => 'Artanis', - 'Zeratul' => 'Zeratul', - ], - 'type' => 'select' - ], - 'sc2.antagonists' => [ - 'description' => 'The bad guys', - 'default' => 'Amon', - 'name' => 'Antagonists', - 'options' => function($settingsProviders) { - return [ - 'Amon' => 'Amon', - 'Sarah Kerrigan' => 'Sarah Kerrigan', - 'Narud' => 'Narud', - ]; - }, - 'severity' => 'warning', - 'type' => 'multi-select' - ], - ], - 'floating-setting' => [ - 'description' => 'floaringSetting', - // 'default' => 'A default value', - 'name' => 'Uncategorized Setting', - // 'severity' => 'critical', - 'severity' => 'warning', - // 'severity' => 'info', - 'type' => 'integer' - ], - ], - 'Network' => [ - 'Proxy' => [ - 'proxy.host' => [ - 'name' => __('Host'), - 'type' => 'string', - 'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'), - 'test' => 'testHostname', - ], - 'proxy.port' => [ - 'name' => __('Port'), - 'type' => 'integer', - 'description' => __('The TCP port for the HTTP proxy.'), - 'test' => 'testForRangeXY', - ], - 'proxy.user' => [ - 'name' => __('User'), - 'type' => 'string', - 'description' => __('The authentication username for the HTTP proxy.'), - 'default' => 'admin', - 'dependsOn' => 'proxy.host', - ], - 'proxy.password' => [ - 'name' => __('Password'), - 'type' => 'string', - 'description' => __('The authentication password for the HTTP proxy.'), - 'default' => '', - 'dependsOn' => 'proxy.host', - ], - ], - ], - 'UI' => [ - 'General' => [ - 'ui.bsTheme' => [ - 'description' => 'The Bootstrap theme to use for the application', - 'default' => 'default', - 'name' => 'UI Theme', - 'options' => function($settingsProviders) { - $instanceTable = TableRegistry::getTableLocator()->get('Instance'); - $themes = $instanceTable->getAvailableThemes(); - return array_combine($themes, $themes); - }, - 'severity' => 'info', - 'type' => 'select' - ], - ], - ], - ], - 'Security' => [ - 'Development' => [ - 'Debugging' => [ - 'security.debug' => [ - 'name' => __('Debug Level'), - 'type' => 'select', - 'description' => __('The debug level of the instance'), - 'default' => 0, - 'options' => [ - 0 => __('Debug Off'), - 1 => __('Debug On'), - 2 => __('Debug On + SQL Dump'), - ], - 'test' => function($value, $setting, $validator) { - $validator->range('value', [0, 3]); - return testValidator($value, $validator); - }, - ], - ], - ] - ], - 'Features' => [ - 'Demo Settings' => [ - 'demo.switch' => [ - 'name' => __('Switch'), - 'type' => 'boolean', - 'description' => __('A switch acting as a checkbox'), - 'default' => false, - 'test' => function() { - return 'Fake error'; - }, - ], - ] - ], - ]; + return []; } /** @@ -214,7 +72,7 @@ class SettingsProviderTable extends AppTable * @param array $settings the settings * @return void */ - private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array + protected function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array { foreach ($settingConf as $key => $value) { if ($this->isSettingMetaKey($key)) { @@ -277,12 +135,12 @@ class SettingsProviderTable extends AppTable return $notices; } - private function isLeaf($setting) + protected function isLeaf($setting) { return !empty($setting['name']) && !empty($setting['type']); } - private function evaluateLeaf($setting, $settingSection) + protected function evaluateLeaf($setting, $settingSection) { $skipValidation = false; if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') { @@ -353,12 +211,6 @@ class SettingsProviderTable extends AppTable } } -function testValidator($value, $validator) -{ - $errors = $validator->validate(['value' => $value]); - return !empty($errors) ? implode(', ', $errors['value']) : true; -} - class SettingValidator { @@ -383,23 +235,5 @@ class SettingValidator public function testForEmpty($value, &$setting) { return !empty($value) ? true : __('Cannot be empty'); - } - - public function testBaseURL($value, &$setting) - { - if (empty($value)) { - return __('Cannot be empty'); - } - if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) { - return __('Invalid URL, please make sure that the protocol is set.'); - } - return true; - } - - public function testUuid($value, &$setting) { - if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) { - return __('Invalid UUID.'); - } - return true; - } + } } \ No newline at end of file diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php new file mode 100644 index 0000000..ac50614 --- /dev/null +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -0,0 +1,196 @@ +settingValidator = new CerebrateSettingValidator(); + parent::__construct(); + } + + protected function generateSettingsConfiguration() + { + return [ + 'Application' => [ + 'General' => [ + 'Essentials' => [ + '_description' => __('Ensentials settings required for the application to run normally.'), + '_icon' => 'user-cog', + 'app.baseurl' => [ + 'name' => __('Base URL'), + 'type' => 'string', + 'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'), + 'default' => '', + 'severity' => 'critical', + 'test' => 'testBaseURL', + ], + 'app.uuid' => [ + 'name' => 'UUID', + 'type' => 'string', + 'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'), + 'default' => '', + 'severity' => 'critical', + 'test' => 'testUuid', + ], + ], + 'Miscellaneous' => [ + 'sc2.hero' => [ + 'description' => 'The true hero', + 'default' => 'Sarah Kerrigan', + 'name' => 'Hero', + 'options' => [ + 'Jim Raynor' => 'Jim Raynor', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Artanis' => 'Artanis', + 'Zeratul' => 'Zeratul', + ], + 'type' => 'select' + ], + 'sc2.antagonists' => [ + 'description' => 'The bad guys', + 'default' => 'Amon', + 'name' => 'Antagonists', + 'options' => function ($settingsProviders) { + return [ + 'Amon' => 'Amon', + 'Sarah Kerrigan' => 'Sarah Kerrigan', + 'Narud' => 'Narud', + ]; + }, + 'severity' => 'warning', + 'type' => 'multi-select' + ], + ], + 'floating-setting' => [ + 'description' => 'floaringSetting', + // 'default' => 'A default value', + 'name' => 'Uncategorized Setting', + // 'severity' => 'critical', + 'severity' => 'warning', + // 'severity' => 'info', + 'type' => 'integer' + ], + ], + 'Network' => [ + 'Proxy' => [ + 'proxy.host' => [ + 'name' => __('Host'), + 'type' => 'string', + 'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'), + 'test' => 'testHostname', + ], + 'proxy.port' => [ + 'name' => __('Port'), + 'type' => 'integer', + 'description' => __('The TCP port for the HTTP proxy.'), + 'test' => 'testForRangeXY', + ], + 'proxy.user' => [ + 'name' => __('User'), + 'type' => 'string', + 'description' => __('The authentication username for the HTTP proxy.'), + 'default' => 'admin', + 'dependsOn' => 'proxy.host', + ], + 'proxy.password' => [ + 'name' => __('Password'), + 'type' => 'string', + 'description' => __('The authentication password for the HTTP proxy.'), + 'default' => '', + 'dependsOn' => 'proxy.host', + ], + ], + ], + 'UI' => [ + 'General' => [ + 'ui.bsTheme' => [ + 'description' => 'The Bootstrap theme to use for the application', + 'default' => 'default', + 'name' => 'UI Theme', + 'options' => function ($settingsProviders) { + $instanceTable = TableRegistry::getTableLocator()->get('Instance'); + $themes = $instanceTable->getAvailableThemes(); + return array_combine($themes, $themes); + }, + 'severity' => 'info', + 'type' => 'select' + ], + ], + ], + ], + 'Security' => [ + 'Development' => [ + 'Debugging' => [ + 'security.debug' => [ + 'name' => __('Debug Level'), + 'type' => 'select', + 'description' => __('The debug level of the instance'), + 'default' => 0, + 'options' => [ + 0 => __('Debug Off'), + 1 => __('Debug On'), + 2 => __('Debug On + SQL Dump'), + ], + 'test' => function ($value, $setting, $validator) { + $validator->range('value', [0, 3]); + return testValidator($value, $validator); + }, + ], + ], + ] + ], + 'Features' => [ + 'Demo Settings' => [ + 'demo.switch' => [ + 'name' => __('Switch'), + 'type' => 'boolean', + 'description' => __('A switch acting as a checkbox'), + 'default' => false, + 'test' => function () { + return 'Fake error'; + }, + ], + ] + ], + ]; + } +} + +function testValidator($value, $validator) +{ + $errors = $validator->validate(['value' => $value]); + return !empty($errors) ? implode(', ', $errors['value']) : true; +} + +class CerebrateSettingValidator extends SettingValidator +{ + public function testUuid($value, &$setting) + { + if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) { + return __('Invalid UUID.'); + } + return true; + } + + + public function testBaseURL($value, &$setting) + { + if (empty($value)) { + return __('Cannot be empty'); + } + if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) { + return __('Invalid URL, please make sure that the protocol is set.'); + } + return true; + } +} \ No newline at end of file diff --git a/src/Model/Table/SettingProviders/UserSettingsProvider.php b/src/Model/Table/SettingProviders/UserSettingsProvider.php new file mode 100644 index 0000000..baeca50 --- /dev/null +++ b/src/Model/Table/SettingProviders/UserSettingsProvider.php @@ -0,0 +1,50 @@ + [ + __('User Interface') => [ + 'ui.bsTheme' => [ + 'description' => 'The Bootstrap theme to use for the application', + 'default' => 'default', + 'name' => 'UI Theme', + 'options' => (function () { + $instanceTable = TableRegistry::getTableLocator()->get('Instance'); + $themes = $instanceTable->getAvailableThemes(); + return array_combine($themes, $themes); + })(), + 'severity' => 'info', + 'type' => 'select' + ], + 'ui.sidebar.expanded' => [ + 'name' => __('Sidebar expanded'), + 'type' => 'boolean', + 'description' => __('Should the left navigation sidebar expanded and locked.'), + 'default' => false, + 'severity' => 'info', + ], + 'ui.sidebar.include_bookmarks' => [ + 'name' => __('Include bookmarks in the sidebar'), + 'type' => 'boolean', + 'description' => __('Should bookmarks links included in the sidebar.'), + 'default' => false, + 'severity' => 'info', + ], + ] + ], + __('Account Security') => [ + ] + ]; + } +} \ No newline at end of file diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php index 0235cdc..0459ce7 100644 --- a/src/Model/Table/SettingsTable.php +++ b/src/Model/Table/SettingsTable.php @@ -3,9 +3,10 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; -use Cake\Validation\Validator; use Cake\Core\Configure; -use Cake\ORM\TableRegistry; + +require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'CerebrateSettingsProvider.php'); +use App\Settings\SettingsProvider\CerebrateSettingsProvider; class SettingsTable extends AppTable { @@ -16,7 +17,7 @@ class SettingsTable extends AppTable { parent::initialize($config); $this->setTable(false); - $this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider'); + $this->SettingsProvider = new CerebrateSettingsProvider(); } public function getSettings($full=false): array diff --git a/src/Model/Table/UserSettingsTable.php b/src/Model/Table/UserSettingsTable.php new file mode 100644 index 0000000..4c7e708 --- /dev/null +++ b/src/Model/Table/UserSettingsTable.php @@ -0,0 +1,138 @@ +addBehavior('Timestamp'); + $this->belongsTo( + 'Users' + ); + $this->setDisplayField('name'); + + $this->SettingsProvider = new UserSettingsProvider(); + } + + public function validationDefault(Validator $validator): Validator + { + $validator + ->requirePresence(['name', 'user_id'], 'create') + ->notEmptyString('name', __('Please fill this field')) + ->notEmptyString('user_id', __('Please supply the user id to which this setting belongs to')); + return $validator; + } + + public function getSettingsFromProviderForUser($user_id, $full = false): array + { + $settingsTmp = $this->getSettingsForUser($user_id)->toArray(); + $settings = []; + foreach ($settingsTmp as $setting) { + $settings[$setting->name] = $setting->value; + } + if (empty($full)) { + return $settings; + } else { + $settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings); + $settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider); + $notices = $this->SettingsProvider->getNoticesFromSettingsConfiguration($settingsProvider, $settings); + return [ + 'settings' => $settings, + 'settingsProvider' => $settingsProvider, + 'settingsFlattened' => $settingsFlattened, + 'notices' => $notices, + ]; + } + } + + public function getSettingsForUser($user_id) + { + return $this->find()->where([ + 'user_id' => $user_id, + ])->all(); + } + + public function getSettingByName($user, $name) + { + return $this->find()->where([ + 'user_id' => $user->id, + 'name' => $name, + ])->first(); + } + + public function createSetting($user, $name, $value) + { + $setting = $this->newEmptyEntity(); + $data = [ + 'name' => $name, + 'value' => $value, + 'user_id' => $user->id, + ]; + $setting = $this->patchEntity($setting, $data); + $savedData = $this->save($setting); + return $savedData; + } + + public function editSetting($user, $name, $value) + { + $setting = $this->getSettingByName($user, $name); + $setting = $this->patchEntity($setting, [ + 'value' => $value + ]); + $savedData = $this->save($setting); + return $savedData; + } + + public function saveBookmark($user, $data) + { + $setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME); + $bookmarkData = [ + 'label' => $data['bookmark_label'], + 'name' => $data['bookmark_name'], + 'url' => $data['bookmark_url'], + ]; + if (is_null($setting)) { // setting not found, create it + $bookmarksData = json_encode([$bookmarkData]); + $result = $this->createSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData); + } else { + $bookmarksData = json_decode($setting->value); + $bookmarksData[] = $bookmarkData; + $bookmarksData = json_encode($bookmarksData); + $result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData); + } + return $result; + } + + public function deleteBookmark($user, $data) + { + $setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME); + $bookmarkData = [ + 'name' => $data['bookmark_name'], + 'url' => $data['bookmark_url'], + ]; + if (is_null($setting)) { // Can't delete something that doesn't exist + return null; + } else { + $bookmarksData = json_decode($setting->value, true); + foreach ($bookmarksData as $i => $savedBookmark) { + if ($savedBookmark['name'] == $bookmarkData['name'] && $savedBookmark['url'] == $bookmarkData['url']) { + unset($bookmarksData[$i]); + } + } + $bookmarksData = json_encode($bookmarksData); + $result = $this->editSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData); + } + return $result; + } +} diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 585295d..f3c7097 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -34,6 +34,13 @@ class UsersTable extends AppTable 'cascadeCallbacks' => false ] ); + $this->hasMany( + 'UserSettings', + [ + 'dependent' => true, + 'cascadeCallbacks' => true + ] + ); $this->setDisplayField('username'); } diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 287696f..6d9275f 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -139,6 +139,12 @@ class BootstrapHelper extends Helper $bsSwitch = new BoostrapSwitch($options, $this); return $bsSwitch->switch(); } + + public function dropdownMenu($options) + { + $bsDropdownMenu = new BoostrapDropdownMenu($options, $this); + return $bsDropdownMenu->dropdownMenu(); + } } class BootstrapGeneric @@ -652,21 +658,21 @@ class BoostrapTable extends BootstrapGeneric { $key = $field; } $cellValue = Hash::get($row, $key); - $html .= $this->genCell($cellValue, $field, $row); + $html .= $this->genCell($cellValue, $field, $row, $i); } } else { // indexed array foreach ($row as $cellValue) { - $html .= $this->genCell($cellValue, $field, $row); + $html .= $this->genCell($cellValue, $field, $row, $i); } } $html .= $this->closeNode('tr'); return $html; } - private function genCell($value, $field=[], $row=[]) + private function genCell($value, $field=[], $row=[], $i=0) { if (isset($field['formatter'])) { - $cellContent = $field['formatter']($value, $row); + $cellContent = $field['formatter']($value, $row, $i); } else if (isset($field['element'])) { $cellContent = $this->btHelper->getView()->element($field['element'], [ 'data' => [$value], @@ -909,7 +915,7 @@ class BoostrapButton extends BootstrapGeneric { { if (!empty($this->options['icon'])) { $bsIcon = new BoostrapIcon($this->options['icon'], [ - 'class' => [(!empty($this->options['title']) ? 'me-1' : '')] + 'class' => [(!empty($this->options['text']) ? 'me-1' : '')] ]); return $bsIcon->icon(); } @@ -1663,4 +1669,138 @@ class BootstrapListGroup extends BootstrapGeneric } return !empty($item['body']) ? h($item['body']) : ''; } +} + + +class BoostrapDropdownMenu extends BootstrapGeneric +{ + private $defaultOptions = [ + 'dropdown-class' => [], + 'toggle-button' => [], + 'menu' => [], + 'direction' => 'end', + 'alignment' => 'start', + 'submenu_alignment' => 'start', + 'submenu_direction' => 'end', + 'submenu_classes' => [], + ]; + + function __construct($options, $btHelper) { + $this->allowedOptionValues = [ + 'direction' => ['start', 'end', 'up', 'down'], + 'alignment' => ['start', 'end'], + ]; + $this->processOptions($options); + $this->menu = $this->options['menu']; + $this->btHelper = $btHelper; + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + if (!empty($this->options['dropdown-class']) && !is_array($this->options['dropdown-class'])) { + $this->options['dropdown-class'] = [$this->options['dropdown-class']]; + } + $this->checkOptionValidity(); + } + + public function dropdownMenu() + { + return $this->fullDropdown(); + } + + public function fullDropdown() + { + return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu)); + } + + public function genDropdownWrapper($toggle='', $menu='', $direction=null, $classes=null) + { + $classes = !is_null($classes) ? $classes : $this->options['dropdown-class']; + $direction = !is_null($direction) ? $direction : $this->options['direction']; + $content = $toggle . $menu; + $html = $this->genNode('div', array_merge( + $this->options['params'], + [ + 'class' => array_merge( + $classes, + [ + 'dropdown', + "drop{$direction}" + ] + ) + ] + ), $content); + return $html; + } + + public function genDropdownToggleButton() + { + $defaultOptions = [ + 'class' => ['dropdown-toggle'], + 'params' => [ + 'data-bs-toggle' => 'dropdown', + 'aria-expanded' => 'false', + ] + ]; + $options = array_merge($this->options['toggle-button'], $defaultOptions); + return $this->btHelper->button($options); + } + + private function genDropdownMenu($entries, $alignment=null) + { + $alignment = !is_null($alignment) ? $alignment : $this->options['alignment']; + $html = $this->genNode('div', [ + 'class' => ['dropdown-menu', "dropdown-menu-{$alignment}"], + ], $this->genEntries($entries)); + return $html; + } + + private function genEntries($entries) + { + $html = ''; + foreach ($entries as $entry) { + $link = $this->genEntry($entry); + if (!empty($entry['menu'])) { + $html .= $this->genDropdownWrapper($link, $this->genDropdownMenu($entry['menu']), $this->options['submenu_direction'], $this->options['submenu_classes']); + } else { + $html .= $link; + } + } + return $html; + } + + private function genEntry($entry) + { + if (!empty($entry['html'])) { + return $entry['html']; + } + + $classes = ['dropdown-item']; + $params = ['href' => '#']; + $icon = ''; + if (!empty($entry['icon'])) { + $icon = $this->btHelper->icon($entry['icon']); + } + + if (!empty($entry['menu'])) { + $classes[] = 'dropdown-toggle'; + $params['data-bs-toggle'] = 'dropdown'; + $params['aria-haspopup'] = 'true'; + $params['aria-expanded'] = 'false'; + if (!empty($entry['keepOpen'])) { + $classes[] = 'open-form'; + } + $params['data-open-form-id'] = mt_rand(); + } + + $label = $this->genNode('span', [ + 'class' => ['ms-2',], + ], h($entry['text'])); + $content = $icon . $label; + + return $this->genNode('a', array_merge([ + 'class' => $classes, + ], $params), $content); + } } \ No newline at end of file diff --git a/templates/Individuals/index.php b/templates/Individuals/index.php index 4930899..84ed353 100644 --- a/templates/Individuals/index.php +++ b/templates/Individuals/index.php @@ -25,6 +25,10 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => '', 'searchKey' => 'value', 'allowFilering' => true + ], + [ + 'type' => 'table_action', + 'table_setting_id' => 'individual_index', ] ] ], @@ -90,4 +94,4 @@ echo $this->element('genericElements/IndexTable/index_table', [ ] ]); echo ''; -?> +?> \ No newline at end of file diff --git a/templates/Instance/home.php b/templates/Instance/home.php index d576185..d80491a 100644 --- a/templates/Instance/home.php +++ b/templates/Instance/home.php @@ -1,30 +1,57 @@ text($md); +$bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.bookmarks']['value'], true) : []; ?> -

+

+ Bootstrap->icon('bookmark', [ + 'class' => ['fa-fw'] + ]); ?> + +

- $statistics): ?> + + + +

+ +
+ +

+ Bootstrap->icon('chart-bar', [ + 'class' => ['fa-fw'] + ]); ?> + +

+
+ $statistics) : ?>
Html->link( - h($modelForDisplay), - $this->Url->build([ - 'controller' => $modelForDisplay, - 'action' => 'index', - ]), - ['class' => 'text-white text-decoration-none fw-light stretched-link'] - ); - echo $this->element('widgets/highlight-panel', [ - 'titleHtml' => $panelTitle, - 'number' => $statistics['amount'], - 'variation' => $statistics['variation'] ?? '', - 'chartData' => $statistics['timeline'] ?? [] - ]); + $exploded = explode('.', $modelName); + $modelForDisplay = $exploded[count($exploded) - 1]; + $panelTitle = $this->Html->link( + h($modelForDisplay), + $this->Url->build([ + 'controller' => $modelForDisplay, + 'action' => 'index', + ]), + ['class' => 'text-white text-decoration-none fw-light stretched-link'] + ); + echo $this->element('widgets/highlight-panel', [ + 'titleHtml' => $panelTitle, + 'number' => $statistics['amount'], + 'variation' => $statistics['variation'] ?? '', + 'chartData' => $statistics['timeline'] ?? [] + ]); ?>
-
+ \ No newline at end of file diff --git a/templates/Instance/save_setting.php b/templates/Instance/save_setting.php index 6609749..7be19d8 100644 --- a/templates/Instance/save_setting.php +++ b/templates/Instance/save_setting.php @@ -1,7 +1,7 @@ element('genericElements/Form/genericForm', [ 'data' => [ - 'description' => __('Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier.'), + 'description' => __('Application setting form'), 'fields' => [ [ 'field' => 'name', diff --git a/templates/Instance/settings.php b/templates/Instance/settings.php index 0e9adf6..334b5c5 100644 --- a/templates/Instance/settings.php +++ b/templates/Instance/settings.php @@ -5,294 +5,69 @@ $variantFromSeverity = [ 'warning' => 'warning', 'info' => 'info', ]; -$this->set('variantFromSeverity', $variantFromSeverity); -$settingTable = genNavcard($settingsProvider, $this); + +$navLinks = []; +$tabContents = []; + +foreach ($settingsProvider as $settingTitle => $settingContent) { + $navLinks[] = h($settingTitle); + $tabContents[] = $this->element('Settings/category', [ + 'settings' => $settingContent, + 'includeScrollspy' => true, + ]); +} + +array_unshift($navLinks, __('Settings Diagnostic')); +$notice = $this->element('Settings/notice', [ + 'variantFromSeverity' => $variantFromSeverity, + 'notices' => $notices, +]); +array_unshift($tabContents, $notice); ?>
element('Settings/search', [ - ]); + $this->element('Settings/search', [ + 'settingsFlattened' => $settingsFlattened, + ]); ?>
- -
- - $sectionSettings) { - if (!empty($sectionSettings)) { - $cardContent[] = genContentForNav($sectionSettings, $appView); - } else { - $cardContent[] = __('No Settings available yet'); - } - } - array_unshift($cardNavs, __('Settings Diagnostic')); - $notice = $appView->element('Settings/notice', [ - 'variantFromSeverity' => $appView->get('variantFromSeverity'), - ]); - array_unshift($cardContent, $notice); - $tabsOptions0 = [ - // 'vertical' => true, - // 'vertical-size' => 2, + false, 'pills' => false, 'justify' => 'center', 'nav-class' => ['settings-tabs'], 'data' => [ - 'navs' => $cardNavs, - 'content' => $cardContent + 'navs' => $navLinks, + 'content' => $tabContents ] ]; - $table0 = $appView->Bootstrap->tabs($tabsOptions0); - return $table0; -} - -function genContentForNav($sectionSettings, $appView) -{ - $groupedContent = []; - $groupedSetting = []; - foreach ($sectionSettings as $sectionName => $subSectionSettings) { - if (!empty($subSectionSettings)) { - $groupedContent[] = genSection($sectionName, $subSectionSettings, $appView); - } else { - $groupedContent[] = ''; - } - if (!isLeaf($subSectionSettings)) { - $groupedSetting[$sectionName] = array_filter( // only show grouped settings - array_keys($subSectionSettings), - function ($settingGroupName) use ($subSectionSettings) { - return !isLeaf($subSectionSettings[$settingGroupName]) && !empty($subSectionSettings[$settingGroupName]); - } - ); - } - } - $contentHtml = implode('', $groupedContent); - $scrollspyNav = $appView->element('Settings/scrollspyNav', [ - 'groupedSetting' => $groupedSetting - ]); - $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)'; - $container = '
'; - $container .= "
{$scrollspyNav}
"; - $container .= "
{$contentHtml}
"; - $container .= '
'; - return $container; -} - -function genSection($sectionName, $subSectionSettings, $appView) -{ - $sectionContent = []; - $sectionContent[] = '
'; - if (isLeaf($subSectionSettings)) { - $panelHTML = $appView->element('Settings/panel', [ - 'sectionName' => $sectionName, - 'panelName' => $sectionName, - 'panelSettings' => $subSectionSettings, - ]); - $sectionContent[] = $panelHTML; - } else { - if (count($subSectionSettings) > 0) { - $sectionContent[] = sprintf('

%s

', getResolvableID($sectionName), h($sectionName)); - } - foreach ($subSectionSettings as $panelName => $panelSettings) { - if (!empty($panelSettings)) { - $panelHTML = $appView->element('Settings/panel', [ - 'sectionName' => $sectionName, - 'panelName' => $panelName, - 'panelSettings' => $panelSettings, - ]); - $sectionContent[] = $panelHTML; - } else { - $sectionContent[] = ''; - } - } - } - $sectionContent[] = '
'; - return implode('', $sectionContent); -} - -function isLeaf($setting) -{ - return !empty($setting['name']) && !empty($setting['type']); -} - -function getResolvableID($sectionName, $panelName=false) -{ - $id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName))); - if (!empty($panelName)) { - $id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName)); - } - return $id; -} -?> - - + echo $this->Bootstrap->tabs($tabsOptions); + echo $this->Html->script('settings'); + ?> +