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) : []; ?> -
= __('No bookmarks') ?>
+ +